From 2a222271bd835d10b206337c2948a366b379fef6 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 11 Nov 2023 19:50:48 +0100 Subject: [PATCH 01/46] [Dependencies] Dependency preparation --- app/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ad542c3..865b7dca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,10 +120,14 @@ dependencies { implementation("com.opencsv:opencsv:5.8") implementation("androidx.room:room-runtime:2.6.0") + implementation("androidx.room:room-ktx:2.6.0") ksp("androidx.room:room-compiler:2.6.0") implementation("androidx.room:room-rxjava3:2.6.0") androidTestImplementation("androidx.room:room-testing:2.6.0") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") //Used to convert flow to livedata -> TODO delete when migrated Dashboard to kotlin + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") From e072769b459e4fe2d401ae8ae0aaf61c50731c7d Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 11 Nov 2023 20:08:23 +0100 Subject: [PATCH 02/46] [SecureElementDetails] Moved to database package --- .../database/entities/details/ElementDetail.java | 10 ++++++++++ .../details}/creditcard/CreditCardDetails.java | 2 +- .../entities/details}/creditcard/Name.java | 2 +- .../entities/details}/password/PasswordDetails.java | 2 +- .../entities/details}/password/Strength.java | 2 +- .../security/element/ElementDetail.java | 9 --------- 6 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/details/ElementDetail.java rename app/src/main/java/de/davis/passwordmanager/{security/element => database/entities/details}/creditcard/CreditCardDetails.java (97%) rename app/src/main/java/de/davis/passwordmanager/{security/element => database/entities/details}/creditcard/Name.java (95%) rename app/src/main/java/de/davis/passwordmanager/{security/element => database/entities/details}/password/PasswordDetails.java (96%) rename app/src/main/java/de/davis/passwordmanager/{security/element => database/entities/details}/password/Strength.java (96%) delete mode 100644 app/src/main/java/de/davis/passwordmanager/security/element/ElementDetail.java diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/ElementDetail.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/ElementDetail.java new file mode 100644 index 00000000..ce1e1b5e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/ElementDetail.java @@ -0,0 +1,10 @@ +package de.davis.passwordmanager.database.entities.details; + +import java.io.Serializable; + +import de.davis.passwordmanager.database.ElementType; + +public interface ElementDetail extends Serializable { + + ElementType getElementType(); +} diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java similarity index 97% rename from app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java rename to app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java index 8fb8309b..5485733f 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/CreditCardDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.security.element.creditcard; +package de.davis.passwordmanager.database.entities.details.creditcard; import java.io.Serial; import java.util.Objects; diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/Name.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/Name.java similarity index 95% rename from app/src/main/java/de/davis/passwordmanager/security/element/creditcard/Name.java rename to app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/Name.java index a1b2ee34..691b6cfe 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/creditcard/Name.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/Name.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.security.element.creditcard; +package de.davis.passwordmanager.database.entities.details.creditcard; import java.io.Serial; import java.io.Serializable; diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java similarity index 96% rename from app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java rename to app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java index 877f6782..51952fbb 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.security.element.password; +package de.davis.passwordmanager.database.entities.details.password; import java.io.Serial; import java.util.Objects; diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/password/Strength.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java similarity index 96% rename from app/src/main/java/de/davis/passwordmanager/security/element/password/Strength.java rename to app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java index 20afb7f8..9a612417 100644 --- a/app/src/main/java/de/davis/passwordmanager/security/element/password/Strength.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.security.element.password; +package de.davis.passwordmanager.database.entities.details.password; import android.content.Context; import android.graphics.Color; diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/ElementDetail.java b/app/src/main/java/de/davis/passwordmanager/security/element/ElementDetail.java deleted file mode 100644 index c8ef4583..00000000 --- a/app/src/main/java/de/davis/passwordmanager/security/element/ElementDetail.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.davis.passwordmanager.security.element; - -import java.io.Serializable; - -public interface ElementDetail extends Serializable { - - @SecureElement.ElementType - int getType(); -} From c0efff4048405e5b21707173e2f3927bb767cf77 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 11 Nov 2023 20:49:40 +0100 Subject: [PATCH 03/46] Rename .java to .kt --- .../database/{KeyGoDatabaseTest.java => KeyGoDatabaseTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/androidTest/java/de/davis/passwordmanager/database/{KeyGoDatabaseTest.java => KeyGoDatabaseTest.kt} (100%) diff --git a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.java b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt similarity index 100% rename from app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.java rename to app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt From 6e91fd4695935a5754e32795511dc01d9bac27ba Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 11 Nov 2023 20:49:49 +0100 Subject: [PATCH 04/46] [Tag] Implemented Tag Entity This commit introduces the Tag entity, enhancing the database's capability to store and manage relationships between tags and secure elements. Additionally, this update includes the conversion of certain classes to Kotlin. Leveraging Kotlin's coroutine support provides a more streamlined and efficient approach, aligning with modern best practices for asynchronous programming. --- .../3.json | 141 ++++++++++++-- .../database/DatabaseMigrationTest.kt | 27 ++- .../database/KeyGoDatabaseTest.kt | 164 +++++++++-------- .../passwordmanager/backup/csv/CsvBackup.kt | 27 +-- .../backup/keygo/KeyGoBackup.kt | 27 ++- .../dashboard/DashboardAdapter.java | 4 +- .../davis/passwordmanager/dashboard/Item.java | 6 - .../davis/passwordmanager/dashboard/Item.kt | 5 + .../dashboard/SecureElementDiffCallback.java | 45 ----- .../dashboard/SecureElementDiffCallback.kt | 29 +++ .../viewholders/BasicViewHolder.java | 2 +- .../viewholders/SecureElementViewHolder.java | 18 +- .../passwordmanager/database/ElementType.kt | 53 ++++++ .../passwordmanager/database/KeyGoDatabase.kt | 16 +- .../database/SecureElementManager.kt | 129 +++++++++++++ .../database/converter/Converters.java | 2 +- .../converter/ElementTypeConverter.kt | 13 ++ .../database/daos/SecureElementDao.java | 71 -------- .../database/daos/SecureElementWithTagDao.kt | 106 +++++++++++ .../database/dto/SecureElement.kt | 106 +++++++++++ .../database/entities/SecureElementEntity.kt | 19 ++ .../passwordmanager/database/entities/Tag.kt | 12 ++ .../database/entities/Timestamps.kt | 14 ++ .../details/creditcard/CreditCardDetails.java | 9 +- .../details/password/PasswordDetails.java | 9 +- .../junction/SecureElementTagCrossRef.kt | 31 ++++ .../entities/wrappers/CombinedElement.kt | 18 ++ .../passwordmanager/dialog/DeleteDialog.java | 34 +--- .../davis/passwordmanager/filter/Filter.java | 13 +- .../gson/ElementDetailTypeAdapter.java | 8 +- .../OnInformationChangedListener.java | 8 +- .../manager/ActivityResultManager.java | 28 +-- .../security/element/SecureElement.java | 172 ------------------ .../security/element/SecureElementDetail.java | 72 -------- .../element/SecureElementManager.java | 86 --------- .../service/AutoFillService.java | 13 +- .../passwordmanager/service/Response.java | 19 +- .../ui/dashboard/DashboardFragment.java | 65 ++++--- .../elements/CreateSecureElementActivity.java | 12 +- .../passwordmanager/ui/elements/SEBaseUi.java | 4 +- .../ui/elements/SEViewActivity.java | 2 +- .../ui/elements/SEViewFragment.java | 2 +- .../elements/ViewSecureElementFragment.java | 14 +- .../creditcard/CreateCreditCardActivity.java | 13 +- .../creditcard/ViewCreditCardFragment.java | 6 +- .../password/CreatePasswordActivity.java | 11 +- .../password/GeneratePasswordActivity.java | 2 +- .../password/ViewPasswordFragment.java | 9 +- .../ui/highlights/HighlightsFragment.java | 26 +-- .../ui/viewmodels/DashboardViewModel.java | 17 +- .../ui/viewmodels/HighlightsViewModel.java | 37 ++-- .../repositories/DashboardRepo.java | 19 +- .../repositories/HighlightsRepo.java | 38 ---- .../ui/views/AddBottomSheet.java | 10 +- .../ui/views/OptionBottomSheet.java | 49 ++--- .../ui/views/PasswordStrengthBar.java | 2 +- .../main/res/navigation/element_nav_graph.xml | 7 +- 57 files changed, 1024 insertions(+), 877 deletions(-) delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/Item.java create mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.java create mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/ElementType.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/converter/ElementTypeConverter.kt delete mode 100644 app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java create mode 100644 app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/SecureElementEntity.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/junction/SecureElementTagCrossRef.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/wrappers/CombinedElement.kt delete mode 100644 app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/security/element/SecureElementDetail.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/HighlightsRepo.java diff --git a/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json b/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json index 54678e40..df5935e0 100644 --- a/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json +++ b/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json @@ -2,36 +2,29 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "59baef5dbb558a99b1e55c1f957119f0", + "identityHash": "ceb1caca6f62fb72bb85d7bfaaac98fd", "entities": [ { "tableName": "SecureElement", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`data` BLOB, `title` TEXT, `type` INTEGER NOT NULL, `favorite` INTEGER NOT NULL DEFAULT false, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` INTEGER, `modified_at` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `data` BLOB NOT NULL, `favorite` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `created_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `modified_at` INTEGER)", "fields": [ - { - "fieldPath": "detail", - "columnName": "data", - "affinity": "BLOB", - "notNull": false - }, { "fieldPath": "title", "columnName": "title", "affinity": "TEXT", - "notNull": false + "notNull": true }, { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", + "fieldPath": "detail", + "columnName": "data", + "affinity": "BLOB", "notNull": true }, { "fieldPath": "favorite", "columnName": "favorite", "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" + "notNull": true }, { "fieldPath": "id", @@ -40,13 +33,20 @@ "notNull": true }, { - "fieldPath": "createdAt", + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamps.createdAt", "columnName": "created_at", "affinity": "INTEGER", - "notNull": false + "notNull": true, + "defaultValue": "CURRENT_TIMESTAMP" }, { - "fieldPath": "modifiedAt", + "fieldPath": "timestamps.modifiedAt", "columnName": "modified_at", "affinity": "INTEGER", "notNull": false @@ -60,12 +60,117 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "Tag", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `tagId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "tagId" + ] + }, + "indices": [ + { + "name": "index_Tag_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Tag_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "SecureElementTagCrossRef", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tagId` INTEGER NOT NULL, PRIMARY KEY(`id`, `tagId`), FOREIGN KEY(`id`) REFERENCES `SecureElement`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`tagId`) REFERENCES `Tag`(`tagId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tagId" + ] + }, + "indices": [ + { + "name": "index_SecureElementTagCrossRef_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SecureElementTagCrossRef_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_SecureElementTagCrossRef_tagId", + "unique": false, + "columnNames": [ + "tagId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SecureElementTagCrossRef_tagId` ON `${TABLE_NAME}` (`tagId`)" + } + ], + "foreignKeys": [ + { + "table": "SecureElement", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Tag", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tagId" + ], + "referencedColumns": [ + "tagId" + ] + } + ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '59baef5dbb558a99b1e55c1f957119f0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ceb1caca6f62fb72bb85d7bfaaac98fd')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/de/davis/passwordmanager/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/de/davis/passwordmanager/database/DatabaseMigrationTest.kt index 410371c5..3394e7b9 100644 --- a/app/src/androidTest/java/de/davis/passwordmanager/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/de/davis/passwordmanager/database/DatabaseMigrationTest.kt @@ -7,10 +7,11 @@ import androidx.room.testing.MigrationTestHelper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import de.davis.passwordmanager.database.converter.Converters +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails +import de.davis.passwordmanager.database.entities.wrappers.CombinedElement import de.davis.passwordmanager.database.migration.MigrationSpec1To2 import de.davis.passwordmanager.database.migration.MigrationSpec2To3 -import de.davis.passwordmanager.security.element.SecureElement -import de.davis.passwordmanager.security.element.password.PasswordDetails +import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Rule import org.junit.Test @@ -39,14 +40,20 @@ class DatabaseMigrationTest { @Test @Throws(IOException::class) - fun testAllMigrations() { + fun testAllMigrations() = runTest { helper.createDatabase(TEST_DB, 1).apply { val contentValues = ContentValues().apply { put("title", TITLE) - put("type", SecureElement.TYPE_PASSWORD) + put("type", ElementType.PASSWORD.typeId) put( "data", - Converters.convertDetails(PasswordDetails(PASSWORD, ORIGIN, USERNAME)) + Converters.convertDetails( + PasswordDetails( + PASSWORD, + ORIGIN, + USERNAME + ) + ) ) } insert("SecureElement", SQLiteDatabase.CONFLICT_REPLACE, contentValues) @@ -58,12 +65,12 @@ class DatabaseMigrationTest { TEST_DB ).build().apply { openHelper.writableDatabase - val element: SecureElement = secureElementDao().getById(1) - element.run { + val element: CombinedElement = combinedDao().getCombinedElement().first() + element.secureElementEntity.run { assertEquals(TITLE, title) - assertFalse(isFavorite) - assertNull(modifiedAt) - assertNotNull(createdAt) + assertFalse(favorite) + assertNull(timestamps.modifiedAt) + assertNotNull(timestamps.createdAt) (detail as PasswordDetails).run { assertEquals(PASSWORD, password) assertEquals(ORIGIN, origin) diff --git a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt index cb0e73ec..f6f238e4 100644 --- a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt +++ b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt @@ -1,94 +1,106 @@ -package de.davis.passwordmanager.database; - -import static org.junit.Assert.assertEquals; - -import android.content.Context; - -import androidx.room.Room; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import de.davis.passwordmanager.database.daos.SecureElementDao; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.creditcard.CreditCardDetails; -import de.davis.passwordmanager.security.element.creditcard.Name; -import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.utils.GeneratorUtil; - -@RunWith(AndroidJUnit4.class) -public class KeyGoDatabaseTest { - - private KeyGoDatabase db; - - private SecureElementDao secureElementDao; +package de.davis.passwordmanager.database + +import android.content.Context +import androidx.room.Room.inMemoryDatabaseBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import de.davis.passwordmanager.database.daos.SecureElementWithTagDao +import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails +import de.davis.passwordmanager.database.entities.details.creditcard.Name +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails +import de.davis.passwordmanager.database.entities.wrappers.CombinedElement +import de.davis.passwordmanager.utils.GeneratorUtil +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class KeyGoDatabaseTest { + private lateinit var db: KeyGoDatabase + private lateinit var secureElementWithTagDao: SecureElementWithTagDao @Before - public void createDB() { - Context context = ApplicationProvider.getApplicationContext(); - db = Room.inMemoryDatabaseBuilder(context, KeyGoDatabase.class).build(); - secureElementDao = db.secureElementDao(); + fun createDB() { + val context = ApplicationProvider.getApplicationContext() + db = inMemoryDatabaseBuilder(context, KeyGoDatabase::class.java).build() + secureElementWithTagDao = db.combinedDao() } @Test - public void testInsertAndRetrievePasswordElement() { - String title = "Password Element"; - String password = generateTestPassword(); - String origin = "origin"; - String username = "username"; - - - SecureElement passwordElement = new SecureElement( - new PasswordDetails(password, origin, username), title); - - SecureElement element = writeAndRead(passwordElement); - PasswordDetails details = (PasswordDetails) element.getDetail(); + fun testInsertAndRetrievePasswordElement() = runTest { + val title = "Password Element" + val password = generateTestPassword() + val origin = "origin" + val username = "username" + val passwordElement = SecureElement(title, PasswordDetails(password, origin, username)) + val element = writeAndRead(passwordElement) + element.run { + secureElementEntity.run { + (secureElementEntity.detail as PasswordDetails).run { + assertEquals(title, secureElementEntity.title) + assertEquals(password, this.password) + assertEquals(origin, this.origin) + assertEquals(username, this.username) + } + + assertEquals(title, secureElementEntity.title) + assertEquals(ElementType.PASSWORD, secureElementEntity.type) + } + + assertEquals(ElementType.PASSWORD.tag.name, tags.first().name) + } - assertEquals(title, element.getTitle()); - assertEquals(password, details.getPassword()); - assertEquals(origin, details.getOrigin()); - assertEquals(username, details.getUsername()); } @Test - public void testInsertAndRetrieveCreditCardElement() { - String title = "Credit Card Element"; - String expirationDate = "05/12"; - String cardNumber = "0000000000000000"; - String cvv = "222"; - Name name = Name.fromFullName("cardholder"); - - SecureElement passwordElement = new SecureElement(new CreditCardDetails(name, expirationDate, cardNumber, cvv), title); - - SecureElement element = writeAndRead(passwordElement); - CreditCardDetails details = (CreditCardDetails) element.getDetail(); - - assertEquals(title, element.getTitle()); - assertEquals(name, details.getCardholder()); - assertEquals(expirationDate, details.getExpirationDate()); - assertEquals(cardNumber, details.getCardNumber()); - assertEquals(cvv, details.getCvv()); + fun testInsertAndRetrieveCreditCardElement() = runTest { + val title = "Credit Card Element" + val expirationDate = "05/12" + val cardNumber = "0000000000000000" + val cvv = "222" + val name = Name.fromFullName("cardholder") + val passwordElement = + SecureElement(title, CreditCardDetails(name, expirationDate, cardNumber, cvv)) + val element = writeAndRead(passwordElement) + + element.run { + secureElementEntity.run { + (secureElementEntity.detail as CreditCardDetails).run { + assertEquals(name, this.cardholder) + assertEquals(expirationDate, this.expirationDate) + assertEquals(cardNumber, this.cardNumber) + assertEquals(cvv, this.cvv) + } + + assertEquals(title, secureElementEntity.title) + assertEquals(ElementType.CREDIT_CARD, secureElementEntity.type) + } + + assertEquals(ElementType.CREDIT_CARD.tag.name, tags.first().name) + } } - private SecureElement writeAndRead(SecureElement element) { - long id = secureElementDao.insert(element); + private suspend fun writeAndRead(element: SecureElement): CombinedElement { + val id: Long = secureElementWithTagDao.insert(element.toEntity()) - return secureElementDao.getById(id); + return secureElementWithTagDao.getCombinedElementById(id); } - private String generateTestPassword(){ - return GeneratorUtil.generatePassword(15_000, GeneratorUtil.USE_DIGITS | - GeneratorUtil.USE_LOWERCASE | - GeneratorUtil.USE_PUNCTUATION | - GeneratorUtil.USE_UPPERCASE); + private fun generateTestPassword(): String { + return GeneratorUtil.generatePassword( + 15000, GeneratorUtil.USE_DIGITS or + GeneratorUtil.USE_LOWERCASE or + GeneratorUtil.USE_PUNCTUATION or + GeneratorUtil.USE_UPPERCASE + ) } @After - public void cleanUp(){ - db.close(); + fun cleanUp() { + db.close() } -} +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt index 30b32886..0e2c9206 100644 --- a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt +++ b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt @@ -9,10 +9,10 @@ import de.davis.passwordmanager.backup.DataBackup import de.davis.passwordmanager.backup.Result import de.davis.passwordmanager.backup.TYPE_EXPORT import de.davis.passwordmanager.backup.TYPE_IMPORT -import de.davis.passwordmanager.database.KeyGoDatabase.Companion.instance -import de.davis.passwordmanager.security.element.SecureElement -import de.davis.passwordmanager.security.element.SecureElementManager -import de.davis.passwordmanager.security.element.password.PasswordDetails +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStream @@ -38,9 +38,8 @@ class CsvBackup(context: Context) : DataBackup(context) { }.build() var line: Array - val elements: List = instance.secureElementDao() - .getAllByType(SecureElement.TYPE_PASSWORD) - .blockingGet() + val elements: List = + SecureElementManager.getSecureElements(ElementType.PASSWORD.typeId) var existed = 0 csvReader.use { @@ -57,8 +56,13 @@ class CsvBackup(context: Context) : DataBackup(context) { existed++ continue } - val details = PasswordDetails(pwd, origin, username) - SecureElementManager.getInstance().createElement(SecureElement(details, title)) + val details = + PasswordDetails( + pwd, + origin, + username + ) + SecureElementManager.insertElement(SecureElement(title, details)) } } return if (existed != 0) Result.Duplicate(existed) else Result.Success(TYPE_IMPORT) @@ -68,9 +72,8 @@ class CsvBackup(context: Context) : DataBackup(context) { override suspend fun runExport(outputStream: OutputStream): Result { val csvWriter = CSVWriterBuilder(OutputStreamWriter(outputStream)).build() - val elements: List = instance.secureElementDao() - .getAllByType(SecureElement.TYPE_PASSWORD) - .blockingGet() + val elements: List = + SecureElementManager.getSecureElements(ElementType.PASSWORD.typeId) csvWriter.use { it.writeNext(arrayOf("name", "url", "username", "password", "note")) diff --git a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt index f79e3fb2..81c00595 100644 --- a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt +++ b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt @@ -15,14 +15,13 @@ import de.davis.passwordmanager.backup.Result import de.davis.passwordmanager.backup.SecureDataBackup import de.davis.passwordmanager.backup.TYPE_EXPORT import de.davis.passwordmanager.backup.TYPE_IMPORT -import de.davis.passwordmanager.database.KeyGoDatabase.Companion.instance +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.entities.details.ElementDetail +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy import de.davis.passwordmanager.security.Cryptography -import de.davis.passwordmanager.security.element.ElementDetail -import de.davis.passwordmanager.security.element.SecureElement -import de.davis.passwordmanager.security.element.SecureElementDetail -import de.davis.passwordmanager.security.element.SecureElementManager -import de.davis.passwordmanager.security.element.password.PasswordDetails import org.apache.commons.io.IOUtils import java.io.InputStream import java.io.OutputStream @@ -40,7 +39,7 @@ class KeyGoBackup(context: Context) : SecureDataBackup(context) { ): ElementDetail { json.asJsonObject.run { val type = this["type"].asInt - if (type == SecureElement.TYPE_PASSWORD) { + if (type == ElementType.PASSWORD.typeId) { val passwordArray = JsonArray().apply { for (b in Cryptography.encryptAES(this@run["password"].asString.toByteArray())) { add(b) @@ -52,7 +51,7 @@ class KeyGoBackup(context: Context) : SecureDataBackup(context) { return context.deserialize( json, - SecureElementDetail.getFor(type).elementDetailClass + ElementType.getTypeByTypeId(type).elementDetailClass ) } } @@ -68,7 +67,7 @@ class KeyGoBackup(context: Context) : SecureDataBackup(context) { "password", src.password ) - addProperty("type", src.type) + addProperty("type", src.elementType.typeId) } return jsonObject @@ -96,9 +95,7 @@ class KeyGoBackup(context: Context) : SecureDataBackup(context) { return Result.Error(context.getString(R.string.invalid_file)) } - val elements = instance.secureElementDao() - .allOnce - .blockingGet() + val elements: List = SecureElementManager.getSecureElements() var existed = 0 val length = list.size @@ -110,7 +107,7 @@ class KeyGoBackup(context: Context) : SecureDataBackup(context) { notifyUpdate(i + 1, length) continue } - SecureElementManager.getInstance().createElement(element) + SecureElementManager.insertElement(element) notifyUpdate(i + 1, length) } return if (existed != 0) Result.Duplicate(existed) else Result.Success(TYPE_IMPORT) @@ -118,9 +115,7 @@ class KeyGoBackup(context: Context) : SecureDataBackup(context) { @Throws(Exception::class) override suspend fun runExport(outputStream: OutputStream): Result { - val elements = instance.secureElementDao() - .allOnce - .blockingGet() + val elements: List = SecureElementManager.getSecureElements() val json = gson.toJson(elements) diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java b/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java index 41bba79d..e319604b 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java @@ -28,7 +28,7 @@ import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; import de.davis.passwordmanager.dashboard.viewholders.HeaderViewHolder; import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.dto.SecureElement; public class DashboardAdapter extends RecyclerView.Adapter> { @@ -284,7 +284,7 @@ public void showOnly(List elements){ update(elements); } - public void update(List overrideElements){ + public void update(List overrideElements){ List oldEntries = getEntries(); items.clear(); diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/Item.java b/app/src/main/java/de/davis/passwordmanager/dashboard/Item.java deleted file mode 100644 index 4743d046..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/Item.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.davis.passwordmanager.dashboard; - -public interface Item { - - long getId(); -} diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt b/app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt new file mode 100644 index 00000000..e1d08a7d --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt @@ -0,0 +1,5 @@ +package de.davis.passwordmanager.dashboard + +interface Item { + val id: Long +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.java b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.java deleted file mode 100644 index eb391e32..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.java +++ /dev/null @@ -1,45 +0,0 @@ -package de.davis.passwordmanager.dashboard; - -import androidx.recyclerview.widget.DiffUtil; - -import java.util.List; - -import de.davis.passwordmanager.security.element.SecureElement; - -public class SecureElementDiffCallback extends DiffUtil.Callback { - - private final List oldItems; - private final List newItems; - - public SecureElementDiffCallback(List oldItems, List newItems) { - this.oldItems = oldItems; - this.newItems = newItems; - } - - @Override - public int getOldListSize() { - return oldItems.size(); - } - - @Override - public int getNewListSize() { - return newItems.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return oldItems.get(oldItemPosition).getId() == newItems.get(newItemPosition).getId(); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - Item oldItem = oldItems.get(oldItemPosition); - Item newItem = newItems.get(newItemPosition); - - if(oldItem instanceof SecureElement && newItem instanceof SecureElement){ - return ((SecureElement) oldItem).getUniqueString().equals(((SecureElement) newItem).getUniqueString()); - } - - return false; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt new file mode 100644 index 00000000..f7536aaa --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt @@ -0,0 +1,29 @@ +package de.davis.passwordmanager.dashboard + +import androidx.recyclerview.widget.DiffUtil +import de.davis.passwordmanager.database.dto.SecureElement + +class SecureElementDiffCallback( + private val oldItems: List, + private val newItems: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldItems.size + } + + override fun getNewListSize(): Int { + return newItems.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldItems[oldItemPosition].id == newItems[newItemPosition].id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return if (oldItem is SecureElement && newItem is SecureElement) { + oldItem == newItem + } else false + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java index 0e815793..4a78c086 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.RecyclerView; import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.dto.SecureElement; public abstract class BasicViewHolder extends RecyclerView.ViewHolder implements SecureElementDetailsLookup.ItemDetailsLookup { diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java index 3371a4e6..348df2cf 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java @@ -17,11 +17,13 @@ import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.color.MaterialColors; +import java.util.List; + import de.davis.passwordmanager.R; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementDetail; -import de.davis.passwordmanager.security.element.creditcard.CreditCardDetails; -import de.davis.passwordmanager.security.element.password.PasswordDetails; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.ui.views.OptionBottomSheet; public class SecureElementViewHolder extends BasicViewHolder { @@ -58,12 +60,12 @@ public void bind(@NonNull SecureElement item, String filter, OnItemClickedListen } title.setText(spannable); - type.setText(item.getTypeName()); + type.setText(item.getElementType().getTitle()); - typeIcon.setImageResource(SecureElementDetail.getFor(item).getIcon()); + typeIcon.setImageResource(item.getElementType().getIcon()); image.setImageDrawable(item.getIcon(context)); - if(item.getType() == SecureElement.TYPE_PASSWORD){ + if(item.getElementType() == ElementType.PASSWORD){ info.setText(((PasswordDetails)item.getDetail()).getStrength().getString()); info.setTextColor(((PasswordDetails)item.getDetail()).getStrength().getColor(context)); }else{ @@ -77,7 +79,7 @@ public void bind(@NonNull SecureElement item, String filter, OnItemClickedListen onItemClickedListener.onClicked(item); }); - more.setOnClickListener(v -> new OptionBottomSheet(itemView.getContext(), item).show()); + more.setOnClickListener(v -> new OptionBottomSheet(itemView.getContext(), List.of(item)).show()); } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt new file mode 100644 index 00000000..d7be683e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt @@ -0,0 +1,53 @@ +package de.davis.passwordmanager.database + +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import de.davis.passwordmanager.R +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.details.ElementDetail +import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails +import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity +import de.davis.passwordmanager.ui.elements.creditcard.CreateCreditCardActivity +import de.davis.passwordmanager.ui.elements.creditcard.ViewCreditCardFragment +import de.davis.passwordmanager.ui.elements.password.CreatePasswordActivity +import de.davis.passwordmanager.ui.elements.password.ViewPasswordFragment + +private const val TAG_PREFIX: String = "elementType:" + +enum class ElementType( + val typeId: Int, + val elementDetailClass: Class, + val createActivityClass: Class, + @IdRes val viewFragmentId: Int, + @StringRes var title: Int, + @DrawableRes val icon: Int, + var tag: Tag +) { + PASSWORD( + 0x1, + PasswordDetails::class.java, + CreatePasswordActivity::class.java, + ViewPasswordFragment.ID, + R.string.password, + R.drawable.ic_baseline_password_24, + Tag("${TAG_PREFIX}password") + ), + CREDIT_CARD( + 0x11, + CreditCardDetails::class.java, + CreateCreditCardActivity::class.java, + ViewCreditCardFragment.ID, + R.string.credit_card, + R.drawable.ic_baseline_credit_card_24, + Tag("${TAG_PREFIX}credit_card") + ); + + companion object { + @JvmStatic + fun getTypeByTypeId(id: Int): ElementType { + return values().first { it.typeId == id } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt b/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt index c26c26bb..3521f9e3 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt @@ -7,15 +7,18 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import de.davis.passwordmanager.PasswordManagerApplication import de.davis.passwordmanager.database.converter.Converters -import de.davis.passwordmanager.database.daos.SecureElementDao +import de.davis.passwordmanager.database.converter.ElementTypeConverter +import de.davis.passwordmanager.database.daos.SecureElementWithTagDao +import de.davis.passwordmanager.database.entities.SecureElementEntity +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.junction.SecureElementTagCrossRef import de.davis.passwordmanager.database.migration.MigrationSpec1To2 import de.davis.passwordmanager.database.migration.MigrationSpec2To3 -import de.davis.passwordmanager.security.element.SecureElement -@TypeConverters(Converters::class) +@TypeConverters(Converters::class, ElementTypeConverter::class) @Database( version = 3, - entities = [SecureElement::class], + entities = [SecureElementEntity::class, Tag::class, SecureElementTagCrossRef::class], autoMigrations = [ AutoMigration(from = 1, to = 2, spec = MigrationSpec1To2::class), AutoMigration(from = 2, to = 3, spec = MigrationSpec2To3::class) @@ -23,7 +26,7 @@ import de.davis.passwordmanager.security.element.SecureElement ) abstract class KeyGoDatabase : RoomDatabase() { - abstract fun secureElementDao(): SecureElementDao + abstract fun combinedDao(): SecureElementWithTagDao companion object { private var INSTANCE: KeyGoDatabase? = null @@ -34,7 +37,8 @@ abstract class KeyGoDatabase : RoomDatabase() { PasswordManagerApplication.getAppContext(), KeyGoDatabase::class.java, DB_NAME - ).fallbackToDestructiveMigration().build().also { INSTANCE = it } + ).fallbackToDestructiveMigration().allowMainThreadQueries().build() + .also { INSTANCE = it } } } diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt new file mode 100644 index 00000000..dbc9574d --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -0,0 +1,129 @@ +package de.davis.passwordmanager.database + +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import de.davis.passwordmanager.database.daos.SecureElementWithTagDao +import de.davis.passwordmanager.database.dto.SecureElement +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +object SecureElementManager { + + private val dao: SecureElementWithTagDao = KeyGoDatabase.instance.combinedDao() + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + + @JvmStatic + suspend fun getSecureElements(typeId: Int? = null): List { + return dao.getCombinedElement(typeId).map { SecureElement.fromEntity(it) } + } + + @JvmStatic + @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) + fun getSecureElementsSync(typeId: Int? = null): List { + return runBlocking { getSecureElements(typeId) } + } + + @JvmStatic + fun getSecureElementsFlow(typeId: Int? = null): Flow> { + return dao.getCombinedElementFlow(typeId) + .map { it.map { e -> SecureElement.fromEntity(e) } } + } + + @JvmStatic + fun getSecureElementsLiveData(typeId: Int? = null): LiveData> { + return getSecureElementsFlow(typeId).asLiveData() + } + + @JvmStatic + suspend fun getByTitle(query: String): List { + return dao.getByTitle("%${query}%").map { SecureElement.fromEntity(it) } + } + + @JvmStatic + @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) + fun getByTitleSync(query: String): List { + return runBlocking { getByTitle(query) } + } + + @JvmStatic + suspend fun switchFavState(secureElement: SecureElement) { + secureElement.favorite = !secureElement.favorite + updateElement(secureElement) + } + + @JvmStatic + @JvmName("switchFavState") + fun switchFavStateCoroutine(secureElement: SecureElement) { + secureElement.favorite = !secureElement.favorite + scope.launch { + updateElement(secureElement) + } + } + + @JvmStatic + suspend fun updateElement(secureElement: SecureElement) { + dao.update(secureElement.toEntity()) + } + + @JvmStatic + @JvmName("updateElement") + fun updateElementCoroutine(secureElement: SecureElement) { + scope.launch { + updateElement(secureElement) + } + } + + @JvmStatic + suspend fun insertElement(secureElement: SecureElement) { + dao.insert(secureElement.toEntity()) + } + + @JvmStatic + @JvmName("insertElement") + fun insertElementCoroutine(secureElement: SecureElement) { + scope.launch { + insertElement(secureElement) + } + } + + @JvmStatic + suspend fun deleteElement(secureElement: SecureElement) { + dao.delete(secureElement.toEntity().secureElementEntity) + } + + @JvmStatic + @JvmName("deleteElement") + fun deleteElementCoroutine(secureElement: SecureElement) { + scope.launch { + deleteElement(secureElement) + } + } + + @JvmStatic + @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) + fun getLastCreatedSync(limit: Int = 5): List { + return runBlocking { + dao.getLastCreated(limit).map { SecureElement.fromEntity(it) } + } + } + + @JvmStatic + @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) + fun getLastModifiedSync(limit: Int = 5): List { + return runBlocking { + dao.getLastModified(limit).map { SecureElement.fromEntity(it) } + } + } + + @JvmStatic + @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) + fun getFavoritesSync(limit: Int = 5): List { + return runBlocking { + dao.getFavorites(limit).map { SecureElement.fromEntity(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java b/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java index 5ab56787..70a9972d 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java +++ b/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java @@ -7,9 +7,9 @@ import java.util.Date; +import de.davis.passwordmanager.database.entities.details.ElementDetail; import de.davis.passwordmanager.gson.ElementDetailTypeAdapter; import de.davis.passwordmanager.security.Cryptography; -import de.davis.passwordmanager.security.element.ElementDetail; public class Converters { diff --git a/app/src/main/java/de/davis/passwordmanager/database/converter/ElementTypeConverter.kt b/app/src/main/java/de/davis/passwordmanager/database/converter/ElementTypeConverter.kt new file mode 100644 index 00000000..329723c1 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/converter/ElementTypeConverter.kt @@ -0,0 +1,13 @@ +package de.davis.passwordmanager.database.converter + +import androidx.room.TypeConverter +import de.davis.passwordmanager.database.ElementType + +object ElementTypeConverter { + + @TypeConverter + fun elementTypeToInt(elementType: ElementType): Int = elementType.typeId + + @TypeConverter + fun intToElementType(elementId: Int): ElementType = ElementType.getTypeByTypeId(elementId) +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java deleted file mode 100644 index d9611bfa..00000000 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementDao.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.davis.passwordmanager.database.daos; - -import androidx.lifecycle.LiveData; -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Update; - -import java.time.Instant; -import java.util.Date; -import java.util.List; - -import de.davis.passwordmanager.security.element.SecureElement; -import io.reactivex.rxjava3.core.Single; - -@Dao -public abstract class SecureElementDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - protected abstract long insertNew(SecureElement element); - - public long insert(SecureElement element){ - Date date = Date.from(Instant.now()); - element.setCreatedAt(date); - return insertNew(element); - } - - @Update - protected abstract void updateElement(SecureElement element); - - public void update(SecureElement element){ - Date date = Date.from(Instant.now()); - element.setModifiedAt(date); - updateElement(element); - } - - @Delete - public abstract void delete(SecureElement element); - - @Delete - public abstract void delete(SecureElement... element); - - @Query("SELECT * FROM SecureElement ORDER BY ROWID ASC") - public abstract LiveData> getAll(); - - @Query("SELECT * FROM SecureElement ORDER BY ROWID ASC") - public abstract Single> getAllOnce(); - - @Query("SELECT * FROM SecureElement WHERE type = :type ORDER BY ROWID ASC") - public abstract Single> getAllByType(@SecureElement.ElementType int type); - - @Query("SELECT * FROM SecureElement WHERE title LIKE :title ORDER BY ROWID ASC") - public abstract LiveData> getByTitle(String title); - - @Query("SELECT * FROM SecureElement WHERE id IS :id") - public abstract SecureElement getById(long id); - - @Query("SELECT count(*) FROM SecureElement") - public abstract Single count(); - - @Query("SELECT * FROM SecureElement WHERE favorite ORDER BY ROWID ASC LIMIT :limit") - public abstract LiveData> getFavorites(int limit); - - @Query("SELECT * FROM SecureElement ORDER BY created_at DESC LIMIT :limit") - public abstract LiveData> getLastCreated(int limit); - - @Query("SELECT * FROM SecureElement WHERE modified_at is not NULL ORDER BY modified_at DESC LIMIT :limit") - public abstract LiveData> getLastModified(int limit); -} diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt new file mode 100644 index 00000000..7ef6dcc9 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -0,0 +1,106 @@ +package de.davis.passwordmanager.database.daos + +import androidx.annotation.VisibleForTesting +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import de.davis.passwordmanager.database.entities.SecureElementEntity +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.junction.SecureElementTagCrossRef +import de.davis.passwordmanager.database.entities.wrappers.CombinedElement +import kotlinx.coroutines.flow.Flow +import java.util.Date + +@Dao +abstract class SecureElementWithTagDao { + + @Transaction + @Query("SELECT * FROM SecureElement WHERE (:typeId IS NULL OR type = :typeId)") + abstract suspend fun getCombinedElement(typeId: Int? = null): List + + @Transaction + @Query("SELECT * FROM SecureElement WHERE (:typeId IS NULL OR type = :typeId)") + abstract fun getCombinedElementFlow(typeId: Int? = null): Flow> + + @Transaction + @Query("SELECT * FROM SecureElement WHERE title LIKE :query") + abstract suspend fun getByTitle(query: String): List + + @Transaction + @VisibleForTesting + @Query("SELECT * FROM SecureElement WHERE id = :id") + abstract suspend fun getCombinedElementById(id: Long): CombinedElement + + @Insert + protected abstract suspend fun insert(secureElementEntity: SecureElementEntity): Long + + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun insert(tags: List): List + + @Insert + protected abstract suspend fun insert(crossRef: SecureElementTagCrossRef) + + @Delete + abstract suspend fun delete(secureElementEntity: SecureElementEntity) + + @Delete + abstract suspend fun delete(tag: Tag) + + @Delete + abstract suspend fun delete(crossRef: SecureElementTagCrossRef) + + @Update + abstract suspend fun update(secureElementEntity: SecureElementEntity) + + @Query("SELECT tagId FROM TAG WHERE name = :name") + protected abstract suspend fun getIdByTagName(name: String): Long + + @Query("DELETE FROM SecureElementTagCrossRef WHERE id = :elementId") + protected abstract suspend fun deleteTagRelationsTo(elementId: Long) + + suspend fun insert(elementWithTags: CombinedElement): Long { + val elementId = insert(elementWithTags.secureElementEntity) + insertTags(elementWithTags.tags, elementId) + return elementId + } + + private suspend fun insertTags(tags: List, elementId: Long) { + val tagIds = insert(tags) + + tagIds.forEachIndexed { index, l -> + insert( + SecureElementTagCrossRef( + elementId, + if (l > 0) l else getIdByTagName(tags[index].name) + ) + ) + } + } + + suspend fun update(elementWithTags: CombinedElement) { + elementWithTags.secureElementEntity.run { + timestamps.modifiedAt = Date() + update(this) + id.run { + deleteTagRelationsTo(this) + insertTags(elementWithTags.tags, this) + } + } + } + + @Transaction + @Query("SELECT * FROM SecureElement WHERE favorite ORDER BY ROWID ASC LIMIT :limit") + abstract suspend fun getFavorites(limit: Int = 5): List + + @Transaction + @Query("SELECT * FROM SecureElement WHERE created_at ORDER BY ROWID DESC LIMIT :limit") + abstract suspend fun getLastCreated(limit: Int = 5): List + + @Transaction + @Query("SELECT * FROM SecureElement WHERE modified_at IS NOT NULL ORDER BY modified_at DESC LIMIT :limit") + abstract suspend fun getLastModified(limit: Int = 5): List +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt b/app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt new file mode 100644 index 00000000..3338834b --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt @@ -0,0 +1,106 @@ +package de.davis.passwordmanager.database.dto + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Parcel +import android.os.Parcelable +import android.util.TypedValue +import com.amulyakhare.textdrawable.TextDrawable +import com.amulyakhare.textdrawable.util.ColorGenerator +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.entities.SecureElementEntity +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.Timestamps +import de.davis.passwordmanager.database.entities.details.ElementDetail +import de.davis.passwordmanager.database.entities.wrappers.CombinedElement +import de.davis.passwordmanager.gson.annotations.Exclude +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler +import net.greypanther.natsort.SimpleNaturalComparator +import java.util.Date + +private object TagParceler : Parceler { + override fun create(parcel: Parcel): Tag { + return Tag(parcel.readString()!!, parcel.readInt()) + } + + override fun Tag.write(parcel: Parcel, flags: Int) { + parcel.apply { + writeString(name) + writeInt(tagId) + } + } +} + +private object TimestampsParceler : Parceler { + override fun create(parcel: Parcel): Timestamps { + return Timestamps( + (parcel.readSerializable() as Date), + parcel.readSerializable()?.let { it as Date }) + } + + override fun Timestamps.write(parcel: Parcel, flags: Int) { + parcel.apply { + writeSerializable(createdAt) + writeSerializable(modifiedAt) + } + } +} + +@Parcelize +@TypeParceler +@TypeParceler +class SecureElement @JvmOverloads constructor( + var title: String, + var detail: ElementDetail, + var favorite: Boolean = false, + var tags: List = listOf(detail.elementType.tag), + private var timestamps: Timestamps = Timestamps.CURRENT, + @Exclude override val id: Long = 0 +) : Item, Comparable, Parcelable { + + val letter get() = title[0].uppercaseChar() + val elementType: ElementType get() = detail.elementType + + fun toEntity(): CombinedElement = run { + return CombinedElement(SecureElementEntity(title, detail, favorite, timestamps, id), tags) + } + + fun getIcon(context: Context): Drawable { + val px = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + context.resources.displayMetrics + ).toInt() + + return TextDrawable.builder() + .beginConfig().bold().endConfig() + .roundRect(px) + .build( + title.substring(0, if (title.length >= 2) 2 else 1), + ColorGenerator.MATERIAL.getColor(title) + ) + } + + override fun compareTo(other: SecureElement): Int { + return SimpleNaturalComparator.getInstance().compare( + title.lowercase(), + other.title.lowercase() + ) + } + + companion object { + @JvmStatic + fun fromEntity(combinedElement: CombinedElement): SecureElement = combinedElement.run { + val secureElement = secureElementEntity.run { + SecureElement(title, detail, favorite, listOf(), timestamps, id) + } + secureElement.apply { + tags = combinedElement.tags + } + return@run secureElement + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/SecureElementEntity.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/SecureElementEntity.kt new file mode 100644 index 00000000..005ed7c5 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/SecureElementEntity.kt @@ -0,0 +1,19 @@ +package de.davis.passwordmanager.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.entities.details.ElementDetail + +@Entity(tableName = "SecureElement") +data class SecureElementEntity @JvmOverloads constructor( + val title: String, + @ColumnInfo(name = "data") val detail: ElementDetail, + val favorite: Boolean = false, + @Embedded val timestamps: Timestamps = Timestamps.CURRENT, + @PrimaryKey(autoGenerate = true) var id: Long = 0, +) { + var type: ElementType = detail.elementType +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt new file mode 100644 index 00000000..cdf7191d --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt @@ -0,0 +1,12 @@ +package de.davis.passwordmanager.database.entities + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import de.davis.passwordmanager.gson.annotations.Exclude + +@Entity(indices = [Index("name", unique = true)]) +data class Tag @JvmOverloads constructor( + val name: String, + @Exclude @PrimaryKey(autoGenerate = true) val tagId: Int = 0 +) diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt new file mode 100644 index 00000000..54add62a --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt @@ -0,0 +1,14 @@ +package de.davis.passwordmanager.database.entities + +import androidx.room.ColumnInfo +import java.util.Date + +data class Timestamps( + @ColumnInfo(name = "created_at", defaultValue = "CURRENT_TIMESTAMP") var createdAt: Date, + @ColumnInfo(name = "modified_at") var modifiedAt: Date? = null +) { + + companion object { + val CURRENT get() = Timestamps(Date()) + } +} diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java index 5485733f..7aaa3a49 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/creditcard/CreditCardDetails.java @@ -3,8 +3,8 @@ import java.io.Serial; import java.util.Objects; -import de.davis.passwordmanager.security.element.ElementDetail; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.entities.details.ElementDetail; import de.davis.passwordmanager.utils.CreditCardUtil; public class CreditCardDetails implements ElementDetail { @@ -66,10 +66,9 @@ public void setExpirationDate(String expirationDate) { this.expirationDate = expirationDate; } - @SecureElement.ElementType @Override - public int getType() { - return SecureElement.TYPE_CREDIT_CARD; + public ElementType getElementType() { + return ElementType.CREDIT_CARD; } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java index 51952fbb..6e8e54e8 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java @@ -3,9 +3,9 @@ import java.io.Serial; import java.util.Objects; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.entities.details.ElementDetail; import de.davis.passwordmanager.security.Cryptography; -import de.davis.passwordmanager.security.element.ElementDetail; -import de.davis.passwordmanager.security.element.SecureElement; public class PasswordDetails implements ElementDetail { @@ -56,10 +56,9 @@ public String getPassword(){ return new String(Cryptography.decryptAES(getPasswordData())); } - @SecureElement.ElementType @Override - public int getType() { - return SecureElement.TYPE_PASSWORD; + public ElementType getElementType() { + return ElementType.PASSWORD; } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/junction/SecureElementTagCrossRef.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/junction/SecureElementTagCrossRef.kt new file mode 100644 index 00000000..955cf978 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/junction/SecureElementTagCrossRef.kt @@ -0,0 +1,31 @@ +package de.davis.passwordmanager.database.entities.junction + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import de.davis.passwordmanager.database.entities.SecureElementEntity +import de.davis.passwordmanager.database.entities.Tag + +@Entity( + primaryKeys = ["id", "tagId"], foreignKeys = [ + ForeignKey( + entity = SecureElementEntity::class, + parentColumns = ["id"], + childColumns = ["id"], + onDelete = CASCADE, + onUpdate = CASCADE + ), + ForeignKey( + entity = Tag::class, + parentColumns = ["tagId"], + childColumns = ["tagId"], + onDelete = CASCADE, + onUpdate = CASCADE + ) + ] +) +data class SecureElementTagCrossRef( + @ColumnInfo(index = true) val id: Long, + @ColumnInfo(index = true) val tagId: Long +) \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/wrappers/CombinedElement.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/wrappers/CombinedElement.kt new file mode 100644 index 00000000..23b26dfa --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/wrappers/CombinedElement.kt @@ -0,0 +1,18 @@ +package de.davis.passwordmanager.database.entities.wrappers + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation +import de.davis.passwordmanager.database.entities.SecureElementEntity +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.junction.SecureElementTagCrossRef + +data class CombinedElement( + @Embedded val secureElementEntity: SecureElementEntity, + @Relation( + parentColumn = "id", + entityColumn = "tagId", + associateBy = Junction(SecureElementTagCrossRef::class) + ) + val tags: List +) diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java index 8ed11c32..facaf790 100644 --- a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java +++ b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java @@ -5,9 +5,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.util.List; + import de.davis.passwordmanager.R; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementManager; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; public class DeleteDialog { @@ -19,28 +21,12 @@ public DeleteDialog(Context context) { .setMessage(R.string.sure_delete); } - public void show(OnClickedListener onClickedListener, SecureElement toDelete){ // if toDelete is null -> delete selected - builder.setNegativeButton(R.string.no, (dialog, which) -> { - if(onClickedListener != null) - onClickedListener.onClicked(false); - - dialog.dismiss(); - }).setPositiveButton(R.string.yes, (dialog, which) -> { - if(toDelete == null) - SecureElementManager.getInstance().deleteSelected(); - else - SecureElementManager.getInstance().delete(toDelete); - - Toast.makeText(builder.getContext(), R.string.successful_deleted, Toast.LENGTH_LONG).show(); - - if(onClickedListener != null) - onClickedListener.onClicked(true); - - dialog.dismiss(); - }).show(); - } + public void show(List toDelete){ // if toDelete is null -> delete selected + builder.setNegativeButton(R.string.no, (dialog, which) -> {}) + .setPositiveButton(R.string.yes, (dialog, which) -> { + toDelete.forEach(SecureElementManager::deleteElement); - public interface OnClickedListener{ - void onClicked(boolean deleted); + Toast.makeText(builder.getContext(), R.string.successful_deleted, Toast.LENGTH_LONG).show(); + }).show(); } } diff --git a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java index 577cecd2..a934910f 100644 --- a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java +++ b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java @@ -6,9 +6,10 @@ import java.util.List; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.security.element.password.Strength; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; +import de.davis.passwordmanager.database.entities.details.password.Strength; public class Filter { @@ -72,16 +73,16 @@ public List filter(List elements) { List typeIds = type.getCheckedChipIds(); List strengthIds = strength.getCheckedChipIds(); if(!typeIds.contains(ID_CREDIT_CARD)) - toFilter.removeIf(element -> element.getType() == SecureElement.TYPE_CREDIT_CARD); + toFilter.removeIf(element -> element.getElementType() == ElementType.CREDIT_CARD); if(!typeIds.contains(ID_PASSWORD)) { - toFilter.removeIf(element -> element.getType() == SecureElement.TYPE_PASSWORD); + toFilter.removeIf(element -> element.getElementType() == ElementType.PASSWORD); return toFilter; } toFilter.removeIf(element -> { - if(element.getType() != SecureElement.TYPE_PASSWORD) + if(element.getElementType() != ElementType.PASSWORD) return false; return !strengthIds.contains(ID_VERY_STRONG) && ((PasswordDetails)element.getDetail()).getStrength().type() == Strength.VERY_STRONG diff --git a/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java index 803bd003..d186cd1a 100644 --- a/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java +++ b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java @@ -9,21 +9,21 @@ import java.lang.reflect.Type; -import de.davis.passwordmanager.security.element.ElementDetail; -import de.davis.passwordmanager.security.element.SecureElementDetail; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.entities.details.ElementDetail; public class ElementDetailTypeAdapter implements JsonSerializer, JsonDeserializer { @Override public ElementDetail deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { int type = json.getAsJsonObject().get("type").getAsInt(); - return context.deserialize(json, SecureElementDetail.getFor(type).getElementDetailClass()); + return context.deserialize(json, ElementType.getTypeByTypeId(type).getElementDetailClass()); } @Override public JsonElement serialize(ElementDetail src, Type typeOfSrc, JsonSerializationContext context) { JsonElement jsonObject = context.serialize(src); - jsonObject.getAsJsonObject().addProperty("type", src.getType()); + jsonObject.getAsJsonObject().addProperty("type", src.getElementType().getTypeId()); return jsonObject; } } diff --git a/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java b/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java index 1dfff886..93dda1de 100644 --- a/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java +++ b/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java @@ -3,9 +3,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import de.davis.passwordmanager.security.element.ElementDetail; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementManager; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.ElementDetail; import de.davis.passwordmanager.ui.views.InformationView; public class OnInformationChangedListener implements InformationView.OnInformationChangedListener { @@ -24,7 +24,7 @@ public void onInformationChanged(String information) { if(detail != null) element.setDetail(detail); - SecureElementManager.getInstance().editElement(element); + SecureElementManager.updateElement(element); } public interface ApplyChangeToElementHelper { diff --git a/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java b/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java index 437d5287..012f989f 100644 --- a/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java +++ b/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java @@ -15,9 +15,9 @@ import java.util.Map; import de.davis.passwordmanager.Keys; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementDetail; -import de.davis.passwordmanager.security.element.SecureElementManager; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity; import de.davis.passwordmanager.ui.elements.password.GeneratePasswordActivity; public class ActivityResultManager { @@ -35,12 +35,14 @@ private ActivityResultManager(ActivityResultCaller resultCaller){ public void registerEdit(@Nullable OnResult onResult){ editElement = resultCaller.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Intent intent = checkAndGetData(result); - if(intent == null) + if(intent == null || intent.getExtras() == null) return; - SecureElement element = (SecureElement) intent.getExtras().getSerializable(Keys.KEY_NEW); + SecureElement element = intent.getExtras().getParcelable(Keys.KEY_NEW); + if(element == null) + return; - SecureElementManager.getInstance().editElement(element); + SecureElementManager.updateElement(element); if(onResult != null) onResult.onResult(element); @@ -50,12 +52,14 @@ public void registerEdit(@Nullable OnResult onResult){ public void registerCreate(){ createElement = resultCaller.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Intent intent = checkAndGetData(result); - if(intent == null) + if(intent == null || intent.getExtras() == null) return; - SecureElement element = (SecureElement) intent.getExtras().getSerializable(Keys.KEY_NEW); + SecureElement element = intent.getExtras().getParcelable(Keys.KEY_NEW); + if(element == null) + return; - SecureElementManager.getInstance().createElement(element); + SecureElementManager.insertElement(element); }); } @@ -75,14 +79,14 @@ public void launchEdit(SecureElement element, Context context){ if(editElement == null) throw new NullPointerException("registerEdit() must be called before launchEdit()"); - editElement.launch(new Intent(context, SecureElementDetail.getFor(element).getCreateActivityClass()).putExtra(Keys.KEY_OLD, element)); + editElement.launch(new Intent(context, element.getElementType().getCreateActivityClass()).putExtra(Keys.KEY_OLD, element)); } - public void launchCreate(SecureElementDetail detail, Context context){ + public void launchCreate(Class activityClass, Context context){ if(createElement == null) throw new NullPointerException("registerCreate() must be called before launchCreate()"); - createElement.launch(new Intent(context, detail.getCreateActivityClass())); + createElement.launch(new Intent(context, activityClass)); } public void launchGeneratePassword(Context context){ diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java deleted file mode 100644 index f796b92b..00000000 --- a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElement.java +++ /dev/null @@ -1,172 +0,0 @@ -package de.davis.passwordmanager.security.element; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.TypedValue; - -import androidx.annotation.IntDef; -import androidx.annotation.StringRes; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.PrimaryKey; - -import com.amulyakhare.textdrawable.TextDrawable; -import com.amulyakhare.textdrawable.util.ColorGenerator; - -import net.greypanther.natsort.SimpleNaturalComparator; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Date; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.Item; -import de.davis.passwordmanager.gson.annotations.Exclude; - -@Entity -public class SecureElement implements Serializable, Comparable, Item { - - @Serial - private static final long serialVersionUID = 4927550289788049498L; - - public static final int TYPE_PASSWORD = 0x1; - public static final int TYPE_CREDIT_CARD = 0x11; - - @IntDef({TYPE_PASSWORD, TYPE_CREDIT_CARD}) - public @interface ElementType{} - - - @ColumnInfo(name = "data") - private ElementDetail detail; - private String title; - - @ElementType - private int type; - - @ColumnInfo(defaultValue = "false") - private boolean favorite; - - @PrimaryKey(autoGenerate = true) - @Exclude - private long id; - - @ColumnInfo(name = "created_at") - private Date createdAt; - - @ColumnInfo(name = "modified_at") - private Date modifiedAt; - - public Drawable getIcon(Context context) { - int px = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 8, - context.getResources().getDisplayMetrics()); - - return TextDrawable.builder() - .beginConfig().bold().endConfig() - .roundRect(px) - .build(getTitle().substring(0, getTitle().length() >= 2 ? 2 : 1), ColorGenerator.MATERIAL.getColor(getTitle())); - } - - - public SecureElement(ElementDetail detail, String title, long id, @ElementType int type, Date createdAt, Date modifiedAt, boolean favorite) { - this.detail = detail; - this.title = title; - this.id = id; - this.type = type; - this.createdAt = createdAt; - this.modifiedAt = modifiedAt; - this.favorite = favorite; - } - - @Ignore - public SecureElement(ElementDetail detail, String title) { - setDetail(detail); - this.title = title; - } - - private SecureElement(){} - - public char getLetter(){ - return Character.toUpperCase(getTitle().charAt(0)); - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public ElementDetail getDetail() { - return detail; - } - - public void setDetail(ElementDetail detail) { - this.detail = detail; - this.type = detail.getType(); - } - - public String getUniqueString(){ - return getId() + title + detail; - } - - @Override - public long getId() { - return id; - } - - @ElementType - public int getType() { - return type; - } - - @StringRes - public int getTypeName(){ - return getTypeName(getType()); - } - - public boolean isFavorite() { - return favorite; - } - - public void setFavorite(boolean favorite) { - this.favorite = favorite; - } - - public Date getCreatedAt() { - return createdAt; - } - - public Date getModifiedAt() { - return modifiedAt; - } - - public void setCreatedAt(Date createdAt) { - this.createdAt = createdAt; - } - - public void setModifiedAt(Date modifiedAt) { - this.modifiedAt = modifiedAt; - } - - @Override - public int compareTo(SecureElement o) { - return SimpleNaturalComparator.getInstance().compare(getTitle().toLowerCase(), o.getTitle().toLowerCase()); - } - - public static SecureElement createEmpty(){ - return new SecureElement(); - } - - @StringRes - public static int getTypeName(@ElementType int type){ - return switch (type) { - case TYPE_PASSWORD -> R.string.password; - case TYPE_CREDIT_CARD -> R.string.credit_card; - default -> throw new IllegalStateException("Unexpected value: " + type); - }; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementDetail.java b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementDetail.java deleted file mode 100644 index b38e589c..00000000 --- a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementDetail.java +++ /dev/null @@ -1,72 +0,0 @@ -package de.davis.passwordmanager.security.element; - -import androidx.annotation.DrawableRes; -import androidx.annotation.IdRes; -import androidx.annotation.StringRes; - -import java.util.LinkedHashMap; -import java.util.Map; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.security.element.creditcard.CreditCardDetails; -import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity; -import de.davis.passwordmanager.ui.elements.creditcard.CreateCreditCardActivity; -import de.davis.passwordmanager.ui.elements.creditcard.ViewCreditCardFragment; -import de.davis.passwordmanager.ui.elements.password.CreatePasswordActivity; -import de.davis.passwordmanager.ui.elements.password.ViewPasswordFragment; - -public class SecureElementDetail { - - private final Class createClass; - private final Class elementDetailClass; - @IdRes private final int viewFragmentId; - private final int title; - private final int icon; - - public SecureElementDetail(Class createClass, Class elementDetailClass, @IdRes int viewFragmentId, @DrawableRes int icon, @StringRes int title) { - this.createClass = createClass; - this.elementDetailClass = elementDetailClass; - this.viewFragmentId = viewFragmentId; - this.icon = icon; - this.title = title; - } - - public int getTitle() { - return title; - } - - public int getIcon() { - return icon; - } - - public Class getCreateActivityClass(){ - return createClass; - } - - public Class getElementDetailClass() { - return elementDetailClass; - } - - public int getViewFragmentId() { - return viewFragmentId; - } - - public static Map getRegisteredDetails(){ - LinkedHashMap map = new LinkedHashMap<>(); - - map.put(SecureElement.TYPE_PASSWORD, new SecureElementDetail(CreatePasswordActivity.class, PasswordDetails.class, ViewPasswordFragment.ID, R.drawable.ic_baseline_password_24, R.string.password)); - map.put(SecureElement.TYPE_CREDIT_CARD, new SecureElementDetail(CreateCreditCardActivity.class, CreditCardDetails.class, ViewCreditCardFragment.ID, R.drawable.ic_baseline_credit_card_24, R.string.credit_card)); - - return map; - } - - public static SecureElementDetail getFor(SecureElement element) { - return getFor(element.getType()); - } - - public static SecureElementDetail getFor(@SecureElement.ElementType int type) { - return getRegisteredDetails().get(type); - } -} - diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java b/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java deleted file mode 100644 index 0861fe31..00000000 --- a/app/src/main/java/de/davis/passwordmanager/security/element/SecureElementManager.java +++ /dev/null @@ -1,86 +0,0 @@ -package de.davis.passwordmanager.security.element; - -import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; - -import java.util.List; - -import de.davis.passwordmanager.dashboard.DashboardAdapter; -import de.davis.passwordmanager.database.KeyGoDatabase; - -public class SecureElementManager { - - private static SecureElementManager instance; - - private final DashboardAdapter adapter; - private TriggerDataChanged triggerDataChanged; - - private SecureElementManager() { - adapter = new DashboardAdapter(); - } - - public static SecureElementManager getInstance(){ - if(instance == null) - instance = new SecureElementManager(); - - return instance; - } - - public void setTriggerDataChanged(TriggerDataChanged triggerDataChanged) { - this.triggerDataChanged = triggerDataChanged; - } - - public DashboardAdapter getAdapter() { - return adapter; - } - - public void switchFavoriteState(E element){ - element.setFavorite(!element.isFavorite()); - editElement(element); - } - - public void editElement(E editedElement){ - doInBackground(() -> KeyGoDatabase.getInstance().secureElementDao().update(editedElement)); - } - - public void createElement(E element){ - doInBackground(() -> KeyGoDatabase.getInstance().secureElementDao().insert(element)); - - triggerDataChanged(); - } - - public void update(List overrideElements){ - adapter.update(overrideElements); - triggerDataChanged(); - } - - @SuppressWarnings("unchecked") - public void deleteSelected(){ - List selectedElements = (List) adapter.getSelectedElements(); - doInBackground(() -> selectedElements.forEach(element -> KeyGoDatabase.getInstance().secureElementDao().delete(element))); - - adapter.removeSelectedElements(); - } - - public void delete(E element){ - doInBackground(() -> KeyGoDatabase.getInstance().secureElementDao().delete(element)); - - triggerDataChanged(); - } - - public void filter(List query){ - adapter.showOnly(query); - } - - public boolean hasElements(){ - return adapter.getItemCount() > 0; - } - - private void triggerDataChanged(){ - if(triggerDataChanged != null) - triggerDataChanged.triggerDataChanged(this); - } - - public interface TriggerDataChanged{ - void triggerDataChanged(SecureElementManager manager); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java b/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java index 4511dd7c..169d9a75 100644 --- a/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java +++ b/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java @@ -1,7 +1,5 @@ package de.davis.passwordmanager.service; -import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; - import android.app.assist.AssistStructure; import android.os.Build; import android.os.CancellationSignal; @@ -18,9 +16,9 @@ import java.util.List; -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; @RequiresApi(api = Build.VERSION_CODES.O) public class AutoFillService extends AutofillService { @@ -104,10 +102,7 @@ public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback ca return; } - doInBackground(() -> KeyGoDatabase.getInstance() - .secureElementDao() - .insert(new SecureElement( - new PasswordDetails(finalPassword, finalWebDomain, finalUsername), finalWebDomainShort))); + SecureElementManager.insertElement(new SecureElement(finalWebDomainShort, new PasswordDetails(finalPassword, finalWebDomain, finalUsername))); callback.onSuccess(); } diff --git a/app/src/main/java/de/davis/passwordmanager/service/Response.java b/app/src/main/java/de/davis/passwordmanager/service/Response.java index eba4ddd6..d0444ff6 100644 --- a/app/src/main/java/de/davis/passwordmanager/service/Response.java +++ b/app/src/main/java/de/davis/passwordmanager/service/Response.java @@ -31,12 +31,12 @@ import java.util.regex.Pattern; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.ui.auth.AuthenticationActivityKt; import de.davis.passwordmanager.ui.auth.AuthenticationRequest; -import io.reactivex.rxjava3.schedulers.Schedulers; @RequiresApi(api = Build.VERSION_CODES.O) public class Response { @@ -192,18 +192,11 @@ private FillResponse.Builder attachSaveInfo(FillResponse.Builder builder){ } private int count(){ - return KeyGoDatabase.getInstance().secureElementDao().count() - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.newThread()) - .blockingGet(); + return fetchData().size(); } private List fetchData(){ - return KeyGoDatabase.getInstance().secureElementDao() - .getAllByType(SecureElement.TYPE_PASSWORD) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.newThread()) - .blockingGet(); + return SecureElementManager.getSecureElementsSync(ElementType.PASSWORD.getTypeId()); } private IntentSender getIntentSender(){ diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java index f0967f4d..184cc0a9 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java @@ -29,14 +29,14 @@ import com.google.android.material.appbar.AppBarLayout; +import java.util.List; + import de.davis.passwordmanager.R; import de.davis.passwordmanager.dashboard.DashboardAdapter; import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; +import de.davis.passwordmanager.database.dto.SecureElement; import de.davis.passwordmanager.databinding.FragmentDashboardBinding; import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementDetail; -import de.davis.passwordmanager.security.element.SecureElementManager; import de.davis.passwordmanager.ui.callbacks.SearchViewBackPressedHandler; import de.davis.passwordmanager.ui.callbacks.SlidingBackPaneManager; import de.davis.passwordmanager.ui.viewmodels.DashboardViewModel; @@ -52,6 +52,8 @@ public class DashboardFragment extends Fragment implements SearchView.OnQueryTex private DashboardViewModel viewModel; private ScrollingViewModel scrollingViewModel; + private final DashboardAdapter adapter = new DashboardAdapter(); + private boolean oldState = true; @Nullable @@ -81,37 +83,39 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat arm.registerCreate(); arm.registerEdit(null); - SecureElementManager manager = SecureElementManager.getInstance(); - manager.setTriggerDataChanged(sem -> { - boolean hasElements = sem.hasElements(); - binding.listPane.progress.setVisibility(View.GONE); - - binding.listPane.recyclerView.setVisibility(hasElements ? View.VISIBLE : View.GONE); - binding.listPane.viewToShow.setVisibility(hasElements ? View.GONE : View.VISIBLE); - }); + /* + TODO + SecureElementManager manager = SecureElementManager.getInstance(); + manager.setTriggerDataChanged(sem -> { + boolean hasElements = sem.hasElements(); + binding.listPane.progress.setVisibility(View.GONE); + ' + binding.listPane.recyclerView.setVisibility(hasElements ? View.VISIBLE : View.GONE); + binding.listPane.viewToShow.setVisibility(hasElements ? View.GONE : View.VISIBLE); + }); + */ - DashboardAdapter dashboardAdapter = manager.getAdapter(); - dashboardAdapter.applyWithTracker(binding.listPane.recyclerView); + adapter.applyWithTracker(binding.listPane.recyclerView); BasicViewHolder.OnItemClickedListener onItemClickedListener = element -> { scrollingViewModel.setVisibility(false); binding.listPane.searchView.hide(); Bundle bundle = new Bundle(); - bundle.putSerializable("element", element); + bundle.putParcelable("element", element); navController.popBackStack(); - navController.navigate(SecureElementDetail.getFor(element).getViewFragmentId(), bundle); + navController.navigate(element.getElementType().getViewFragmentId(), bundle); binding.getRoot().open(); }; - dashboardAdapter.setOnItemClickedListener(onItemClickedListener); + adapter.setOnItemClickedListener(onItemClickedListener); DashboardAdapter searchResultAdapter = new DashboardAdapter(); searchResultAdapter.setOnItemClickedListener(onItemClickedListener); binding.listPane.recyclerViewResults.setAdapter(searchResultAdapter); - addMenu(dashboardAdapter); + addMenu(); binding.listPane.searchView.getEditText().addTextChangedListener(new TextWatcher() { @Override @@ -128,7 +132,7 @@ public void afterTextChanged(Editable s) { viewModel = new ViewModelProvider(this, ViewModelProvider.Factory.from(DashboardViewModel.initializer)).get(DashboardViewModel.class); - viewModel.getElements().observe(getViewLifecycleOwner(), secureElements -> SecureElementManager.getInstance().update(secureElements)); + viewModel.getElements().observe(getViewLifecycleOwner(), this::update); viewModel.getSearchResults().observe(getViewLifecycleOwner(), secureElements -> { searchResultAdapter.update(secureElements); searchResultAdapter.setFilter(viewModel.getSearchQuery()); @@ -165,7 +169,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if(getArguments() == null) return; - SecureElement element = (SecureElement) getArguments().getSerializable("element"); + SecureElement element = getArguments().getParcelable("element"); if(element == null) return; @@ -173,6 +177,16 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { oldState = false; } + private void update(List secureElements){ + adapter.update(secureElements); + + boolean hasElements = adapter.getItemCount() > 0; + binding.listPane.progress.setVisibility(View.GONE); + + binding.listPane.recyclerView.setVisibility(hasElements ? View.VISIBLE : View.GONE); + binding.listPane.viewToShow.setVisibility(hasElements ? View.GONE : View.VISIBLE); + } + @Override public void onPause() { super.onPause(); @@ -186,12 +200,12 @@ public void onResume() { scrollingViewModel.setVisibility(oldState); } - private void addMenu(DashboardAdapter dashboardAdapter){ + private void addMenu(){ requireActivity().addMenuProvider(new MenuProvider() { @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { menuInflater.inflate(R.menu.view_menu, menu); - dashboardAdapter.setStateChangeHandler(selectedItems -> { + adapter.setStateChangeHandler(selectedItems -> { requireActivity().invalidateMenu(); binding.listPane.searchBar.setHint(selectedItems > 0 ? getString(R.string.selected_items, selectedItems) : getString(android.R.string.search_go)); }); @@ -200,14 +214,15 @@ public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater @Override public void onPrepareMenu(@NonNull Menu menu) { MenuProvider.super.onPrepareMenu(menu); - menu.findItem(R.id.more).setVisible(dashboardAdapter.getTracker().hasSelection()); + menu.findItem(R.id.more).setVisible(adapter.getTracker().hasSelection()); } @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { if(menuItem.getItemId() == R.id.more){ - OptionBottomSheet optionBottomSheet = new OptionBottomSheet(requireContext(), null); + OptionBottomSheet optionBottomSheet = new OptionBottomSheet(requireContext(), adapter.getSelectedElements()); optionBottomSheet.show(); + adapter.getTracker().clearSelection(); }else if(menuItem.getItemId() == R.id.filter){ new FilterBottomSheet().show(getParentFragmentManager(), "FilterDialog"); } @@ -219,7 +234,7 @@ public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - SecureElementManager.getInstance().getAdapter().getTracker().onSaveInstanceState(outState); + adapter.getTracker().onSaveInstanceState(outState); if(binding == null) return; outState.putCharSequence("searchbar_hint", binding.listPane.searchBar.getHint()); @@ -228,7 +243,7 @@ public void onSaveInstanceState(@NonNull Bundle outState) { @Override public void onViewStateRestored(@Nullable Bundle savedInstanceState) { super.onViewStateRestored(savedInstanceState); - SecureElementManager.getInstance().getAdapter().getTracker().onRestoreInstanceState(savedInstanceState); + adapter.getTracker().onRestoreInstanceState(savedInstanceState); if(savedInstanceState == null) return; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java index b7a48b15..abb5e8ef 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java @@ -10,7 +10,8 @@ import de.davis.passwordmanager.Keys; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.dto.SecureElement; public abstract class CreateSecureElementActivity extends SEViewActivity { @@ -22,7 +23,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setTitle(SecureElement.getTypeName(getSecureElementType())); + setTitle(getSecureElementType().getTitle()); } @Override @@ -49,7 +50,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - outState.putSerializable(ELEMENT, toElement()); + outState.putParcelable(ELEMENT, toElement()); } @Override @@ -65,11 +66,10 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { @Override public void fillInElement(@NonNull SecureElement secureElement) { - setTitle(secureElement.getTypeName()); + setTitle(secureElement.getElementType().getTitle()); } - @SecureElement.ElementType - public abstract int getSecureElementType(); + public abstract ElementType getSecureElementType(); public abstract Result check(); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java index f6553bf8..b72374fa 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.dto.SecureElement; public interface SEBaseUi { @@ -17,7 +17,7 @@ public static SecureElement initiate(SEBaseUi seBaseUi, Bundle bundle, String ke if(bundle == null || !bundle.containsKey(key)) return null; - Object object = bundle.getSerializable(key); + Object object = bundle.getParcelable(key); SecureElement element = null; try { element = (SecureElement) object; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java index 0d0385a6..4c0140d7 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java @@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity; import de.davis.passwordmanager.Keys; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.dto.SecureElement; public abstract class SEViewActivity extends AppCompatActivity implements SEBaseUi { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java index ec89b23a..0b13952d 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java @@ -9,7 +9,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.dto.SecureElement; public abstract class SEViewFragment extends Fragment implements SEBaseUi { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java index 810807a0..151dbbfc 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java @@ -11,11 +11,10 @@ import com.google.android.material.appbar.MaterialToolbar; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; import de.davis.passwordmanager.listeners.OnInformationChangedListener; import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementDetail; -import de.davis.passwordmanager.security.element.SecureElementManager; import de.davis.passwordmanager.ui.views.InformationView; public abstract class ViewSecureElementFragment extends SEViewFragment { @@ -46,16 +45,17 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void fillInElement(@NonNull SecureElement e){ + SecureElementManager.updateElement(e); // Update last modified value toolbar = requireView().findViewById(R.id.toolbar); - handleFavIcon(e.isFavorite()); + handleFavIcon(e.getFavorite()); toolbar.setTitle(e.getTitle()); - toolbar.setSubtitle(SecureElementDetail.getFor(e).getTitle()); + toolbar.setSubtitle(e.getElementType().getTitle()); toolbar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.menu_fav){ - SecureElementManager.getInstance().switchFavoriteState(e); - handleFavIcon(e.isFavorite()); + SecureElementManager.switchFavState(e); + handleFavIcon(e.getFavorite()); return false; } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java index c26c8f90..09b2467e 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java @@ -24,14 +24,15 @@ import java.util.TimerTask; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; +import de.davis.passwordmanager.database.entities.details.creditcard.Name; import de.davis.passwordmanager.databinding.ActivityCreateCreditcardBinding; import de.davis.passwordmanager.listeners.OnCreditCardEndIconClickListener; import de.davis.passwordmanager.listeners.text.CreditCardNumberTextWatcher; import de.davis.passwordmanager.listeners.text.ExpiryDateTextWatcher; import de.davis.passwordmanager.nfc.NfcManager; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.creditcard.CreditCardDetails; -import de.davis.passwordmanager.security.element.creditcard.Name; import de.davis.passwordmanager.text.method.CreditCardNumberTransformationMethod; import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity; import de.davis.passwordmanager.utils.CreditCardUtil; @@ -200,7 +201,7 @@ protected SecureElement toElement() { CreditCardDetails details = new CreditCardDetails(name, expiryDate, creditCardNumber, cvv); SecureElement card = getElement() == null ? - SecureElement.createEmpty() : + new SecureElement(title, details) : getElement(); card.setTitle(title); card.setDetail(details); @@ -209,8 +210,8 @@ protected SecureElement toElement() { } @Override - public int getSecureElementType() { - return SecureElement.TYPE_CREDIT_CARD; + public ElementType getSecureElementType() { + return ElementType.CREDIT_CARD; } private void insertCard(EmvCard card){ diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java index 07d3e3b5..2829d5b1 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java @@ -13,14 +13,14 @@ import com.google.android.material.textfield.TextInputLayout; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; +import de.davis.passwordmanager.database.entities.details.creditcard.Name; import de.davis.passwordmanager.databinding.FragmentViewCreditCardBinding; import de.davis.passwordmanager.listeners.OnCreditCardEndIconClickListener; import de.davis.passwordmanager.listeners.OnInformationChangedListener; import de.davis.passwordmanager.listeners.text.CreditCardNumberTextWatcher; import de.davis.passwordmanager.listeners.text.ExpiryDateTextWatcher; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.creditcard.CreditCardDetails; -import de.davis.passwordmanager.security.element.creditcard.Name; import de.davis.passwordmanager.text.method.CreditCardNumberTransformationMethod; import de.davis.passwordmanager.ui.elements.ViewSecureElementFragment; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java index d1aaa385..4ac20e09 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java @@ -11,10 +11,11 @@ import java.util.Objects; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.ElementType; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.databinding.ActivityCreatePasswordBinding; import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity; public class CreatePasswordActivity extends CreateSecureElementActivity { @@ -87,7 +88,7 @@ protected SecureElement toElement(){ PasswordDetails details = new PasswordDetails(password, origin, user); if(getElement() == null) - return new SecureElement(details, title); + return new SecureElement(title, details); getElement().setTitle(title); getElement().setDetail(details); @@ -95,7 +96,7 @@ protected SecureElement toElement(){ } @Override - public int getSecureElementType() { - return SecureElement.TYPE_PASSWORD; + public ElementType getSecureElementType() { + return ElementType.PASSWORD; } } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java index f1c31f2d..9b7d2cd8 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java @@ -25,8 +25,8 @@ import de.davis.passwordmanager.Keys; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.entities.details.password.Strength; import de.davis.passwordmanager.databinding.ActivityGeneratePasswordBinding; -import de.davis.passwordmanager.security.element.password.Strength; import de.davis.passwordmanager.utils.AssetsUtil; import de.davis.passwordmanager.utils.GeneratorUtil; import de.davis.passwordmanager.utils.TimeoutUtil; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java index 6e0553cd..1f0ec821 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java @@ -1,7 +1,5 @@ package de.davis.passwordmanager.ui.elements.password; -import static de.davis.passwordmanager.utils.BackgroundUtil.doInBackground; - import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -15,12 +13,11 @@ import com.google.android.material.textfield.TextInputLayout; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.KeyGoDatabase; +import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.databinding.FragmentViewPasswordBinding; import de.davis.passwordmanager.listeners.OnInformationChangedListener; import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; import de.davis.passwordmanager.ui.elements.ViewSecureElementFragment; import de.davis.passwordmanager.ui.views.PasswordStrengthBar; import de.davis.passwordmanager.utils.BrowserUtil; @@ -71,8 +68,6 @@ public void fillInElement(@NonNull SecureElement password) { setStrengthValues(details); manageOrigin(details); - - doInBackground(() -> KeyGoDatabase.getInstance().secureElementDao().update(password)); } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java index 02e58fcc..99083c0d 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java @@ -15,10 +15,12 @@ import androidx.navigation.NavController; import androidx.navigation.Navigation; +import java.util.List; + import de.davis.passwordmanager.R; import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder; +import de.davis.passwordmanager.database.dto.SecureElement; import de.davis.passwordmanager.databinding.FragmentHighlightsBinding; -import de.davis.passwordmanager.security.element.SecureElement; import de.davis.passwordmanager.ui.viewmodels.HighlightsViewModel; public class HighlightsFragment extends Fragment { @@ -56,25 +58,25 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.materialButtonToggleGroup.addOnButtonCheckedListener((group, checkedId, isChecked) -> viewModel.setState(group.getCheckedButtonId() == binding.lastAdded.getId())); - viewModel.getFavorites().observe(getViewLifecycleOwner(), secureElements -> { - binding.noFavorites.setVisibility(secureElements.size() > 0 ? View.GONE : View.VISIBLE); - binding.favoriteContainer.removeViews(1, binding.favoriteContainer.getChildCount()-1); - secureElements.forEach(secureElement -> { - View fav_view = getLayoutInflater().inflate(R.layout.fav_layout, null, false); - ((TextView)fav_view.findViewById(R.id.title)).setText(secureElement.getTitle()); - ((ImageView)fav_view.findViewById(R.id.image)).setImageDrawable(secureElement.getIcon(requireContext())); - fav_view.setOnClickListener(v -> launchElement(secureElement)); + List favorites = viewModel.getFavorites(); + binding.noFavorites.setVisibility(favorites.size() > 0 ? View.GONE : View.VISIBLE); + binding.favoriteContainer.removeViews(1, binding.favoriteContainer.getChildCount()-1); + favorites.forEach(secureElement -> { + View fav_view = getLayoutInflater().inflate(R.layout.fav_layout, null, false); + ((TextView)fav_view.findViewById(R.id.title)).setText(secureElement.getTitle()); + ((ImageView)fav_view.findViewById(R.id.image)).setImageDrawable(secureElement.getIcon(requireContext())); - ((ViewGroup)view.findViewById(R.id.favorite_container)).addView(fav_view); - }); + fav_view.setOnClickListener(v -> launchElement(secureElement)); + + ((ViewGroup)view.findViewById(R.id.favorite_container)).addView(fav_view); }); } private void launchElement(SecureElement element){ NavController navController = Navigation.findNavController(requireActivity(), R.id.nav_host_fragment); Bundle bundle = new Bundle(); - bundle.putSerializable("element", element); + bundle.putParcelable("element", element); navController.navigate(R.id.dashboardFragment, bundle); } } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java index ec7e74d6..607f9fca 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java @@ -3,9 +3,8 @@ import static androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle; import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY; -import android.text.TextUtils; - import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.SavedStateHandle; import androidx.lifecycle.Transformations; @@ -15,16 +14,15 @@ import java.util.List; import de.davis.passwordmanager.PasswordManagerApplication; -import de.davis.passwordmanager.database.KeyGoDatabase; +import de.davis.passwordmanager.database.dto.SecureElement; import de.davis.passwordmanager.filter.Filter; -import de.davis.passwordmanager.security.element.SecureElement; import de.davis.passwordmanager.ui.viewmodels.repositories.DashboardRepo; public class DashboardViewModel extends ViewModel { private static final String QUERY = "query"; - private final LiveData> searchResults; + private final MediatorLiveData> searchResults = new MediatorLiveData<>(); private final SavedStateHandle savedStateHandle; private final MutableLiveData> elements; @@ -33,11 +31,8 @@ public class DashboardViewModel extends ViewModel { public DashboardViewModel(DashboardRepo dashboardRepo, SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; - this.searchResults = Transformations.switchMap(savedStateHandle.getLiveData(QUERY, ""), input -> { - if(TextUtils.isEmpty(input)) - return dashboardRepo.getElements(); - - return dashboardRepo.search("%"+ input +"%"); + searchResults.addSource(savedStateHandle.getLiveData(QUERY, ""), query -> { + searchResults.postValue(dashboardRepo.search(query)); }); elements = (MutableLiveData>) Transformations.map(dashboardRepo.getElements(), Filter.DEFAULT::filter); @@ -66,6 +61,6 @@ public String getSearchQuery(){ if(app == null) throw new RuntimeException("app is null"); - return new DashboardViewModel(DashboardRepo.getInstance(KeyGoDatabase.getInstance()), createSavedStateHandle(creationExtras)); + return new DashboardViewModel(DashboardRepo.getInstance(), createSavedStateHandle(creationExtras)); }); } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java index 8fa8cd51..c1713e7a 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java @@ -1,20 +1,17 @@ package de.davis.passwordmanager.ui.viewmodels; import static androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle; -import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.SavedStateHandle; -import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.viewmodel.ViewModelInitializer; import java.util.List; -import de.davis.passwordmanager.PasswordManagerApplication; -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.ui.viewmodels.repositories.HighlightsRepo; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; public class HighlightsViewModel extends ViewModel { @@ -22,18 +19,16 @@ public class HighlightsViewModel extends ViewModel { private final SavedStateHandle savedStateHandle; - private final LiveData> elements; - private final HighlightsRepo highlightsRepo; + private final MediatorLiveData> elements = new MediatorLiveData<>(); - public HighlightsViewModel(HighlightsRepo highlightsRepo, SavedStateHandle savedStateHandle) { + public HighlightsViewModel(SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; - this.highlightsRepo = highlightsRepo; - this.elements = Transformations.switchMap(savedStateHandle.getLiveData(STATE, true), input -> { - if(input) - return highlightsRepo.getLastAdded(); - - return highlightsRepo.getLastModified(); + elements.addSource(savedStateHandle.getLiveData(STATE, true), last ->{ + if(last) + elements.postValue(SecureElementManager.getLastCreatedSync(5)); + else + elements.postValue(SecureElementManager.getLastModifiedSync(5)); }); } @@ -45,16 +40,10 @@ public LiveData> getElements() { return elements; } - public LiveData> getFavorites() { - return highlightsRepo.getFavorites(); + public List getFavorites() { + return SecureElementManager.getFavoritesSync(10); } public static final ViewModelInitializer initializer = new ViewModelInitializer<>(HighlightsViewModel.class, creationExtras -> - { - PasswordManagerApplication app = (PasswordManagerApplication) creationExtras.get(APPLICATION_KEY); - if(app == null) - throw new RuntimeException("app is null"); - - return new HighlightsViewModel(HighlightsRepo.getInstance(KeyGoDatabase.getInstance()), createSavedStateHandle(creationExtras)); - }); + new HighlightsViewModel(createSavedStateHandle(creationExtras))); } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java index 3e4bbf4f..f2c73ddc 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java @@ -5,34 +5,31 @@ import java.util.List; -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; public class DashboardRepo { private static volatile DashboardRepo instance; - private final KeyGoDatabase database; private final MediatorLiveData> elements; - private DashboardRepo(KeyGoDatabase database) { - this.database = database; - + private DashboardRepo() { this.elements = new MediatorLiveData<>(); - this.elements.addSource(database.secureElementDao().getAll(), this.elements::postValue); + this.elements.addSource(SecureElementManager.getSecureElementsLiveData(null), this.elements::postValue); } public LiveData> getElements() { return elements; } - public LiveData> search(String query){ - return database.secureElementDao().getByTitle(query); + public List search(String query){ + return SecureElementManager.getByTitleSync(query); } - public static synchronized DashboardRepo getInstance(KeyGoDatabase database){ + public static synchronized DashboardRepo getInstance(){ if(instance == null) - instance = new DashboardRepo(database); + instance = new DashboardRepo(); return instance; } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/HighlightsRepo.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/HighlightsRepo.java deleted file mode 100644 index 397b8174..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/HighlightsRepo.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.davis.passwordmanager.ui.viewmodels.repositories; - -import androidx.lifecycle.LiveData; - -import java.util.List; - -import de.davis.passwordmanager.database.KeyGoDatabase; -import de.davis.passwordmanager.security.element.SecureElement; - -public class HighlightsRepo { - - private static volatile HighlightsRepo instance; - - private final KeyGoDatabase database; - - public HighlightsRepo(KeyGoDatabase database) { - this.database = database; - } - - public LiveData> getLastAdded(){ - return database.secureElementDao().getLastCreated(5); - } - - public LiveData> getLastModified(){ - return database.secureElementDao().getLastModified(5); - } - - public LiveData> getFavorites(){ - return database.secureElementDao().getFavorites(5); - } - - public static synchronized HighlightsRepo getInstance(KeyGoDatabase database){ - if(instance == null) - instance = new HighlightsRepo(database); - - return instance; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/AddBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/AddBottomSheet.java index d502b2fe..7141311c 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/AddBottomSheet.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/AddBottomSheet.java @@ -10,9 +10,9 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.databinding.AddBottomSheetContentBinding; import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.security.element.SecureElementDetail; import de.davis.passwordmanager.ui.dashboard.DashboardFragment; public class AddBottomSheet extends BottomSheetDialogFragment { @@ -22,12 +22,12 @@ public class AddBottomSheet extends BottomSheetDialogFragment { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { AddBottomSheetContentBinding binding = AddBottomSheetContentBinding.inflate(inflater, container, false); - for (SecureElementDetail detail : SecureElementDetail.getRegisteredDetails().values()){ + for (ElementType type : ElementType.values()){ AddButton btn = new AddButton(requireContext()); - btn.setText(detail.getTitle()); - btn.setIcon(detail.getIcon()); + btn.setText(type.getTitle()); + btn.setIcon(type.getIcon()); btn.setOnClickListener(v -> { - ActivityResultManager.getOrCreateManager(DashboardFragment.class, null).launchCreate(detail, requireContext()); + ActivityResultManager.getOrCreateManager(DashboardFragment.class, null).launchCreate(type.getCreateActivityClass(), requireContext()); dismiss(); }); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java index 7f953bea..2f0162a3 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java @@ -6,21 +6,23 @@ import com.google.android.material.bottomsheet.BottomSheetDialog; +import java.util.List; + import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dto.SecureElement; import de.davis.passwordmanager.databinding.MoreBottomSheetContentBinding; import de.davis.passwordmanager.dialog.DeleteDialog; import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.security.element.SecureElement; -import de.davis.passwordmanager.security.element.SecureElementManager; import de.davis.passwordmanager.ui.dashboard.DashboardFragment; public class OptionBottomSheet extends BottomSheetDialog { - private final SecureElement element; + private final List elements; - public OptionBottomSheet(Context context, SecureElement element) { + public OptionBottomSheet(Context context, List elements) { super(context); - this.element = element; + this.elements = elements; } @Override @@ -29,40 +31,39 @@ protected void onCreate(Bundle savedInstanceState) { MoreBottomSheetContentBinding binding = MoreBottomSheetContentBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - binding.title.setText(element == null ? getContext().getString(R.string.options) : element.getTitle()); - - binding.edit.setOnClickListener(v -> { - ActivityResultManager.getOrCreateManager(DashboardFragment.class, null).launchEdit(element, getContext()); - dismiss(); - }); + if(elements.isEmpty()) + return; + SecureElement firstElement = elements.get(0); - binding.favorite.setOnClickListener(v -> { - if(element == null) - return; + binding.title.setText(elements.size() > 1 ? getContext().getString(R.string.options) : firstElement.getTitle()); - SecureElementManager.getInstance().switchFavoriteState(element); - dismiss(); - }); - - if(element == null) { + if(elements.size() > 1) { binding.edit.setVisibility(View.GONE); binding.favorite.setVisibility(View.GONE); }else { + binding.edit.setOnClickListener(v -> { + ActivityResultManager.getOrCreateManager(DashboardFragment.class, null).launchEdit(firstElement, getContext()); + dismiss(); + }); + + binding.favorite.setOnClickListener(v -> { + SecureElementManager.switchFavState(firstElement); + dismiss(); + }); + binding.favorite.setCompoundDrawablesRelativeWithIntrinsicBounds( - element.isFavorite() ? + firstElement.getFavorite() ? R.drawable.baseline_star_24 : R.drawable.baseline_star_outline_24, 0, 0, 0); - binding.favorite.setText(element.isFavorite() ? R.string.remove_from_favorite : R.string.mark_as_favorite); + binding.favorite.setText(firstElement.getFavorite() ? R.string.remove_from_favorite : R.string.mark_as_favorite); } - binding.delete.setOnClickListener(v -> { - new DeleteDialog(getContext()).show(null, element); + new DeleteDialog(getContext()).show(elements); dismiss(); }); - } } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java b/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java index 3cda290c..d83547e3 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java @@ -25,7 +25,7 @@ import java.util.concurrent.Executors; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.security.element.password.Strength; +import de.davis.passwordmanager.database.entities.details.password.Strength; import de.davis.passwordmanager.utils.TimeoutUtil; public class PasswordStrengthBar extends LinearLayout implements TextWatcher { diff --git a/app/src/main/res/navigation/element_nav_graph.xml b/app/src/main/res/navigation/element_nav_graph.xml index 10ab6b88..bb3cd336 100644 --- a/app/src/main/res/navigation/element_nav_graph.xml +++ b/app/src/main/res/navigation/element_nav_graph.xml @@ -11,7 +11,7 @@ + app:argType="de.davis.passwordmanager.database.dto.SecureElement" /> + app:argType="de.davis.passwordmanager.database.dto.SecureElement" /> - Date: Sat, 18 Nov 2023 17:12:08 +0100 Subject: [PATCH 05/46] [Renamed dto package] Renamed to dtos --- .../davis/passwordmanager/database/KeyGoDatabaseTest.kt | 2 +- .../de/davis/passwordmanager/backup/csv/CsvBackup.kt | 2 +- .../de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt | 2 +- .../passwordmanager/dashboard/DashboardAdapter.java | 2 +- .../dashboard/SecureElementDiffCallback.kt | 2 +- .../dashboard/viewholders/BasicViewHolder.java | 2 +- .../dashboard/viewholders/SecureElementViewHolder.java | 2 +- .../passwordmanager/database/SecureElementManager.kt | 2 +- .../database/{dto => dtos}/SecureElement.kt | 9 +++------ .../de/davis/passwordmanager/dialog/DeleteDialog.java | 2 +- .../java/de/davis/passwordmanager/filter/Filter.java | 2 +- .../listeners/OnInformationChangedListener.java | 2 +- .../passwordmanager/manager/ActivityResultManager.java | 2 +- .../davis/passwordmanager/service/AutoFillService.java | 2 +- .../java/de/davis/passwordmanager/service/Response.java | 2 +- .../passwordmanager/ui/dashboard/DashboardFragment.java | 2 +- .../ui/elements/CreateSecureElementActivity.java | 2 +- .../de/davis/passwordmanager/ui/elements/SEBaseUi.java | 2 +- .../passwordmanager/ui/elements/SEViewActivity.java | 2 +- .../passwordmanager/ui/elements/SEViewFragment.java | 2 +- .../ui/elements/ViewSecureElementFragment.java | 2 +- .../ui/elements/creditcard/CreateCreditCardActivity.java | 2 +- .../ui/elements/creditcard/ViewCreditCardFragment.java | 2 +- .../ui/elements/password/CreatePasswordActivity.java | 2 +- .../ui/elements/password/ViewPasswordFragment.java | 2 +- .../ui/highlights/HighlightsFragment.java | 2 +- .../ui/viewmodels/DashboardViewModel.java | 2 +- .../ui/viewmodels/HighlightsViewModel.java | 2 +- .../ui/viewmodels/repositories/DashboardRepo.java | 2 +- .../passwordmanager/ui/views/OptionBottomSheet.java | 2 +- app/src/main/res/navigation/element_nav_graph.xml | 4 ++-- 31 files changed, 34 insertions(+), 37 deletions(-) rename app/src/main/java/de/davis/passwordmanager/database/{dto => dtos}/SecureElement.kt (93%) diff --git a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt index f6f238e4..fa9ee146 100644 --- a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt +++ b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt @@ -5,7 +5,7 @@ import androidx.room.Room.inMemoryDatabaseBuilder import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import de.davis.passwordmanager.database.daos.SecureElementWithTagDao -import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails import de.davis.passwordmanager.database.entities.details.creditcard.Name import de.davis.passwordmanager.database.entities.details.password.PasswordDetails diff --git a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt index 0e2c9206..cb6b0fed 100644 --- a/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt +++ b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt @@ -11,7 +11,7 @@ import de.davis.passwordmanager.backup.TYPE_EXPORT import de.davis.passwordmanager.backup.TYPE_IMPORT import de.davis.passwordmanager.database.ElementType import de.davis.passwordmanager.database.SecureElementManager -import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.entities.details.password.PasswordDetails import java.io.InputStream import java.io.InputStreamReader diff --git a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt index 81c00595..e0f81b34 100644 --- a/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt +++ b/app/src/main/java/de/davis/passwordmanager/backup/keygo/KeyGoBackup.kt @@ -17,7 +17,7 @@ import de.davis.passwordmanager.backup.TYPE_EXPORT import de.davis.passwordmanager.backup.TYPE_IMPORT import de.davis.passwordmanager.database.ElementType import de.davis.passwordmanager.database.SecureElementManager -import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.entities.details.ElementDetail import de.davis.passwordmanager.database.entities.details.password.PasswordDetails import de.davis.passwordmanager.gson.strategies.ExcludeAnnotationStrategy diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java b/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java index e319604b..08bc684b 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java @@ -28,7 +28,7 @@ import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; import de.davis.passwordmanager.dashboard.viewholders.HeaderViewHolder; import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public class DashboardAdapter extends RecyclerView.Adapter> { diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt index f7536aaa..31de0b75 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt @@ -1,7 +1,7 @@ package de.davis.passwordmanager.dashboard import androidx.recyclerview.widget.DiffUtil -import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.dtos.SecureElement class SecureElementDiffCallback( private val oldItems: List, diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java index 4a78c086..9139be1e 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.RecyclerView; import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public abstract class BasicViewHolder extends RecyclerView.ViewHolder implements SecureElementDetailsLookup.ItemDetailsLookup { diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java index 348df2cf..692340b3 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java @@ -21,7 +21,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.ui.views.OptionBottomSheet; diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt index dbc9574d..d4d1fdd2 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -3,7 +3,7 @@ package de.davis.passwordmanager.database import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import de.davis.passwordmanager.database.daos.SecureElementWithTagDao -import de.davis.passwordmanager.database.dto.SecureElement +import de.davis.passwordmanager.database.dtos.SecureElement import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt similarity index 93% rename from app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt rename to app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt index 3338834b..0069ebb7 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/dto/SecureElement.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.database.dto +package de.davis.passwordmanager.database.dtos import android.content.Context import android.graphics.drawable.Drawable @@ -55,8 +55,8 @@ private object TimestampsParceler : Parceler { class SecureElement @JvmOverloads constructor( var title: String, var detail: ElementDetail, - var favorite: Boolean = false, var tags: List = listOf(detail.elementType.tag), + var favorite: Boolean = false, private var timestamps: Timestamps = Timestamps.CURRENT, @Exclude override val id: Long = 0 ) : Item, Comparable, Parcelable { @@ -95,10 +95,7 @@ class SecureElement @JvmOverloads constructor( @JvmStatic fun fromEntity(combinedElement: CombinedElement): SecureElement = combinedElement.run { val secureElement = secureElementEntity.run { - SecureElement(title, detail, favorite, listOf(), timestamps, id) - } - secureElement.apply { - tags = combinedElement.tags + SecureElement(title, detail, combinedElement.tags, favorite, timestamps, id) } return@run secureElement } diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java index facaf790..8a1f2324 100644 --- a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java +++ b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java @@ -9,7 +9,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public class DeleteDialog { diff --git a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java index a934910f..0801216b 100644 --- a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java +++ b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java @@ -7,7 +7,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.database.entities.details.password.Strength; diff --git a/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java b/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java index 93dda1de..dbd5a346 100644 --- a/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java +++ b/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java @@ -4,7 +4,7 @@ import androidx.annotation.Nullable; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.ElementDetail; import de.davis.passwordmanager.ui.views.InformationView; diff --git a/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java b/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java index 012f989f..9be87e08 100644 --- a/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java +++ b/app/src/main/java/de/davis/passwordmanager/manager/ActivityResultManager.java @@ -16,7 +16,7 @@ import de.davis.passwordmanager.Keys; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.ui.elements.CreateSecureElementActivity; import de.davis.passwordmanager.ui.elements.password.GeneratePasswordActivity; diff --git a/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java b/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java index 169d9a75..4fe5eb8d 100644 --- a/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java +++ b/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java @@ -17,7 +17,7 @@ import java.util.List; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; @RequiresApi(api = Build.VERSION_CODES.O) diff --git a/app/src/main/java/de/davis/passwordmanager/service/Response.java b/app/src/main/java/de/davis/passwordmanager/service/Response.java index d0444ff6..adba0b8e 100644 --- a/app/src/main/java/de/davis/passwordmanager/service/Response.java +++ b/app/src/main/java/de/davis/passwordmanager/service/Response.java @@ -33,7 +33,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.ui.auth.AuthenticationActivityKt; import de.davis.passwordmanager.ui.auth.AuthenticationRequest; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java index 184cc0a9..622a1ada 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java @@ -34,7 +34,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.dashboard.DashboardAdapter; import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.databinding.FragmentDashboardBinding; import de.davis.passwordmanager.manager.ActivityResultManager; import de.davis.passwordmanager.ui.callbacks.SearchViewBackPressedHandler; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java index abb5e8ef..323289d1 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java @@ -11,7 +11,7 @@ import de.davis.passwordmanager.Keys; import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public abstract class CreateSecureElementActivity extends SEViewActivity { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java index b72374fa..0d0d7018 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEBaseUi.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public interface SEBaseUi { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java index 4c0140d7..d84f7991 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewActivity.java @@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity; import de.davis.passwordmanager.Keys; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public abstract class SEViewActivity extends AppCompatActivity implements SEBaseUi { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java index 0b13952d..204cacd5 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/SEViewFragment.java @@ -9,7 +9,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public abstract class SEViewFragment extends Fragment implements SEBaseUi { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java index 151dbbfc..bb8b1c04 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java @@ -12,7 +12,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.listeners.OnInformationChangedListener; import de.davis.passwordmanager.manager.ActivityResultManager; import de.davis.passwordmanager.ui.views.InformationView; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java index 09b2467e..2d66133e 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java @@ -25,7 +25,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; import de.davis.passwordmanager.database.entities.details.creditcard.Name; import de.davis.passwordmanager.databinding.ActivityCreateCreditcardBinding; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java index 2829d5b1..0b14b87c 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/ViewCreditCardFragment.java @@ -13,7 +13,7 @@ import com.google.android.material.textfield.TextInputLayout; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails; import de.davis.passwordmanager.database.entities.details.creditcard.Name; import de.davis.passwordmanager.databinding.FragmentViewCreditCardBinding; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java index 4ac20e09..f7a898a4 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java @@ -12,7 +12,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.databinding.ActivityCreatePasswordBinding; import de.davis.passwordmanager.manager.ActivityResultManager; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java index 1f0ec821..08192f44 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java @@ -13,7 +13,7 @@ import com.google.android.material.textfield.TextInputLayout; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.databinding.FragmentViewPasswordBinding; import de.davis.passwordmanager.listeners.OnInformationChangedListener; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java index 99083c0d..aaaca295 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java @@ -19,7 +19,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.databinding.FragmentHighlightsBinding; import de.davis.passwordmanager.ui.viewmodels.HighlightsViewModel; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java index 607f9fca..6ab43065 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java @@ -14,7 +14,7 @@ import java.util.List; import de.davis.passwordmanager.PasswordManagerApplication; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.filter.Filter; import de.davis.passwordmanager.ui.viewmodels.repositories.DashboardRepo; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java index c1713e7a..0f620df9 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/HighlightsViewModel.java @@ -11,7 +11,7 @@ import java.util.List; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public class HighlightsViewModel extends ViewModel { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java index f2c73ddc..e20fed92 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/repositories/DashboardRepo.java @@ -6,7 +6,7 @@ import java.util.List; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; public class DashboardRepo { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java index 2f0162a3..fe023cd4 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java @@ -10,7 +10,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dto.SecureElement; +import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.databinding.MoreBottomSheetContentBinding; import de.davis.passwordmanager.dialog.DeleteDialog; import de.davis.passwordmanager.manager.ActivityResultManager; diff --git a/app/src/main/res/navigation/element_nav_graph.xml b/app/src/main/res/navigation/element_nav_graph.xml index bb3cd336..d76ef4cb 100644 --- a/app/src/main/res/navigation/element_nav_graph.xml +++ b/app/src/main/res/navigation/element_nav_graph.xml @@ -11,7 +11,7 @@ + app:argType="de.davis.passwordmanager.database.dtos.SecureElement" /> + app:argType="de.davis.passwordmanager.database.dtos.SecureElement" /> Date: Sat, 18 Nov 2023 17:24:28 +0100 Subject: [PATCH 06/46] [TagView] Introduce UI Component for Tag Entry This commit implements a user interface element, empowering users to create and assign custom tags to their elements seamlessly. --- app/build.gradle.kts | 2 +- .../passwordmanager/database/ElementType.kt | 2 +- .../database/SecureElementManager.kt | 16 ++ .../database/daos/SecureElementWithTagDao.kt | 14 + .../passwordmanager/database/entities/Tag.kt | 9 +- .../elements/CreateSecureElementActivity.java | 2 + .../elements/ViewSecureElementFragment.java | 10 +- .../creditcard/CreateCreditCardActivity.java | 2 + .../password/CreatePasswordActivity.java | 6 +- .../password/ViewPasswordFragment.java | 11 +- .../davis/passwordmanager/ui/views/TagView.kt | 244 ++++++++++++++++++ .../res/layout/activity_create_creditcard.xml | 6 + .../res/layout/activity_create_password.xml | 6 + .../res/layout/fragment_view_credit_card.xml | 19 +- .../res/layout/fragment_view_password.xml | 21 +- app/src/main/res/layout/layout_tag_view.xml | 34 +++ app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/attrs_tag_view.xml | 10 + app/src/main/res/values/strings.xml | 5 + 19 files changed, 404 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt create mode 100644 app/src/main/res/layout/layout_tag_view.xml create mode 100644 app/src/main/res/values/attrs_tag_view.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 865b7dca..b8fb84c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -127,7 +127,7 @@ dependencies { androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") //Used to convert flow to livedata -> TODO delete when migrated Dashboard to kotlin - + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt index d7be683e..8cbe993e 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt @@ -14,7 +14,7 @@ import de.davis.passwordmanager.ui.elements.creditcard.ViewCreditCardFragment import de.davis.passwordmanager.ui.elements.password.CreatePasswordActivity import de.davis.passwordmanager.ui.elements.password.ViewPasswordFragment -private const val TAG_PREFIX: String = "elementType:" +const val TAG_PREFIX: String = "elementType:" enum class ElementType( val typeId: Int, diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt index d4d1fdd2..d171adc0 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import de.davis.passwordmanager.database.daos.SecureElementWithTagDao import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.entities.Tag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -43,6 +44,8 @@ object SecureElementManager { return dao.getByTitle("%${query}%").map { SecureElement.fromEntity(it) } } + suspend fun getTags(): List = dao.getTags() + @JvmStatic @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) fun getByTitleSync(query: String): List { @@ -77,6 +80,19 @@ object SecureElementManager { } } + @JvmStatic + suspend fun updateModifiedAt(secureElement: SecureElement) { + dao.updateModifiedAt(secureElement.toEntity()) + } + + @JvmStatic + @JvmName("updateModifiedAt") + fun updateModifiedAtCoroutine(secureElement: SecureElement) { + scope.launch { + updateModifiedAt(secureElement) + } + } + @JvmStatic suspend fun insertElement(secureElement: SecureElement) { dao.insert(secureElement.toEntity()) diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index 7ef6dcc9..5f8aba35 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -35,6 +35,9 @@ abstract class SecureElementWithTagDao { @Query("SELECT * FROM SecureElement WHERE id = :id") abstract suspend fun getCombinedElementById(id: Long): CombinedElement + @Query("SELECT * FROM Tag") + abstract suspend fun getTags(): List + @Insert protected abstract suspend fun insert(secureElementEntity: SecureElementEntity): Long @@ -62,6 +65,9 @@ abstract class SecureElementWithTagDao { @Query("DELETE FROM SecureElementTagCrossRef WHERE id = :elementId") protected abstract suspend fun deleteTagRelationsTo(elementId: Long) + @Query("DELETE FROM Tag WHERE tagId NOT IN (SELECT tagId FROM SecureElementTagCrossRef)") + protected abstract suspend fun deleteUnusedTags() + suspend fun insert(elementWithTags: CombinedElement): Long { val elementId = insert(elementWithTags.secureElementEntity) insertTags(elementWithTags.tags, elementId) @@ -88,10 +94,18 @@ abstract class SecureElementWithTagDao { id.run { deleteTagRelationsTo(this) insertTags(elementWithTags.tags, this) + deleteUnusedTags() } } } + suspend fun updateModifiedAt(elementWithTags: CombinedElement) { + elementWithTags.secureElementEntity.run { + timestamps.modifiedAt = Date() + update(this) + } + } + @Transaction @Query("SELECT * FROM SecureElement WHERE favorite ORDER BY ROWID ASC LIMIT :limit") abstract suspend fun getFavorites(limit: Int = 5): List diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt index cdf7191d..6b2f43d7 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt @@ -3,10 +3,17 @@ package de.davis.passwordmanager.database.entities import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey +import de.davis.passwordmanager.database.TAG_PREFIX import de.davis.passwordmanager.gson.annotations.Exclude @Entity(indices = [Index("name", unique = true)]) -data class Tag @JvmOverloads constructor( +class Tag @JvmOverloads constructor( val name: String, @Exclude @PrimaryKey(autoGenerate = true) val tagId: Int = 0 ) + +fun Collection.onlyCustoms(): Collection { + return filter { !it.name.startsWith(TAG_PREFIX) } +} + +val Tag.shouldBeProtected get() = this.name.startsWith(TAG_PREFIX) \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java index 323289d1..24f8a370 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/CreateSecureElementActivity.java @@ -12,6 +12,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.database.dtos.SecureElement; +import de.davis.passwordmanager.ui.views.TagView; public abstract class CreateSecureElementActivity extends SEViewActivity { @@ -67,6 +68,7 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { @Override public void fillInElement(@NonNull SecureElement secureElement) { setTitle(secureElement.getElementType().getTitle()); + ((TagView) findViewById(R.id.tagView)).setTags(secureElement.getTags(), true); } public abstract ElementType getSecureElementType(); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java index bb8b1c04..df292716 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/ViewSecureElementFragment.java @@ -16,6 +16,7 @@ import de.davis.passwordmanager.listeners.OnInformationChangedListener; import de.davis.passwordmanager.manager.ActivityResultManager; import de.davis.passwordmanager.ui.views.InformationView; +import de.davis.passwordmanager.ui.views.TagView; public abstract class ViewSecureElementFragment extends SEViewFragment { @@ -45,7 +46,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void fillInElement(@NonNull SecureElement e){ - SecureElementManager.updateElement(e); // Update last modified value + SecureElementManager.updateModifiedAt(e); toolbar = requireView().findViewById(R.id.toolbar); handleFavIcon(e.getFavorite()); @@ -64,6 +65,13 @@ public void fillInElement(@NonNull SecureElement e){ }); toolbar.setNavigationOnClickListener(v -> requireActivity().getOnBackPressedDispatcher().onBackPressed()); handleNavIcon(); + + TagView tagView = requireView().findViewById(R.id.tagView); + tagView.setTags(e.getTags()); + tagView.setOnLongClickListener(v -> { + ActivityResultManager.getOrCreateManager(getClass(), null).launchEdit(e, requireContext()); + return true; + }); } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java index 2d66133e..c7e0d39e 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/creditcard/CreateCreditCardActivity.java @@ -203,6 +203,8 @@ protected SecureElement toElement() { SecureElement card = getElement() == null ? new SecureElement(title, details) : getElement(); + + card.setTags(binding.tagView.getTags()); card.setTitle(title); card.setDetail(details); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java index f7a898a4..934f8fd6 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/CreatePasswordActivity.java @@ -8,11 +8,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.List; import java.util.Objects; import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.database.dtos.SecureElement; +import de.davis.passwordmanager.database.entities.Tag; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.databinding.ActivityCreatePasswordBinding; import de.davis.passwordmanager.manager.ActivityResultManager; @@ -86,10 +88,12 @@ protected SecureElement toElement(){ String origin = Objects.requireNonNull(binding.textInputLayoutOrigin.getEditText()).getText().toString(); + List tags = binding.tagView.getTags(); PasswordDetails details = new PasswordDetails(password, origin, user); if(getElement() == null) - return new SecureElement(title, details); + return new SecureElement(title, details, tags); + getElement().setTags(tags); getElement().setTitle(title); getElement().setDetail(details); return getElement(); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java index 08192f44..427c088c 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java @@ -30,11 +30,16 @@ public class ViewPasswordFragment extends ViewSecureElementFragment { private EditText editText; @Override - public void fillInElement(@NonNull SecureElement password) { - super.fillInElement(password); + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); ActivityResultManager activityResultManager = ActivityResultManager.getOrCreateManager(getClass(), this); activityResultManager.registerGeneratePassword(result -> editText.setText(result)); + } + + @Override + public void fillInElement(@NonNull SecureElement password) { + super.fillInElement(password); PasswordDetails details = (PasswordDetails) password.getDetail(); @@ -50,7 +55,7 @@ public void fillInElement(@NonNull SecureElement password) { passwordStrengthBar.update(editText.getText().toString(), false); editText.addTextChangedListener(passwordStrengthBar); - view.findViewById(R.id.generate).setOnClickListener(v -> activityResultManager.launchGeneratePassword(getContext())); + view.findViewById(R.id.generate).setOnClickListener(v -> ActivityResultManager.getOrCreateManager(getClass(), this).launchGeneratePassword(getContext())); }); binding.origin.setInformationText(details.getOrigin()); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt new file mode 100644 index 00000000..f2dc191b --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt @@ -0,0 +1,244 @@ +package de.davis.passwordmanager.ui.views + +import android.content.Context +import android.text.Editable +import android.text.InputFilter +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.AdapterView.OnItemClickListener +import android.widget.FrameLayout +import androidx.core.view.children +import androidx.core.widget.doAfterTextChanged +import androidx.core.widget.doBeforeTextChanged +import com.google.android.material.chip.Chip +import de.davis.passwordmanager.R +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.TAG_PREFIX +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.onlyCustoms +import de.davis.passwordmanager.database.entities.shouldBeProtected +import de.davis.passwordmanager.databinding.LayoutTagViewBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class TagView @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet?, + defStyleAttr: Int = 0 +) : FrameLayout(context, attributeSet, defStyleAttr) { + + private val binding: LayoutTagViewBinding + + private val whitespace = Regex("\\s+") + private var ignore: Boolean = false + private var editable: Boolean = true + + + private val _tags = linkedMapOf() + val tags get() = _tags.keys.map { Tag(it) } + + private val scope = CoroutineScope(Dispatchers.IO) + + init { + binding = LayoutTagViewBinding.inflate(LayoutInflater.from(context), this, true) + + context.obtainStyledAttributes(attributeSet, R.styleable.TagView, defStyleAttr, 0) + .apply { + val typeId = + getInteger(R.styleable.TagView_defaultTag, -1) + + if (typeId > -1) + addDefaultElementTag(ElementType.getTypeByTypeId(typeId)) + + setEditable(getBoolean(R.styleable.TagView_editable, editable)) + + + recycle() + } + + binding.tagInput.run { + doBeforeTextChanged { _, _, count, after -> + if (count < after) + handleLastChip(false) + } + + filters += InputFilter { source, _, _, dest, _, _ -> + if (source.isBlank() && dest.isProtectedTagName) + return@InputFilter "" + + return@InputFilter null + } + + doAfterTextChanged { editable -> + if (ignore) + return@doAfterTextChanged + + editable?.run { + if (isProtectedTagName) { + binding.textInputLayout.error = + context.getString(R.string.prefix_not_allowed) + binding.textInputLayout.isErrorEnabled = true + return@doAfterTextChanged + } + + binding.textInputLayout.isErrorEnabled = false + + ignore = true + handleInput(this) + ignore = false + } + } + + setOnEditorActionListener { v, actionId, _ -> + return@setOnEditorActionListener when (actionId) { + EditorInfo.IME_ACTION_GO -> { + handleInput(v.editableText, true) + true + } + + else -> false + } + } + + setOnKeyListener { _, keyCode, event -> + if (event.action != KeyEvent.ACTION_UP) return@setOnKeyListener false + return@setOnKeyListener when (keyCode) { + KeyEvent.KEYCODE_DEL -> { + if (text?.isNotEmpty() == true) return@setOnKeyListener false + handleLastChip(true) + true + } + + else -> false + } + } + + scope.launch { + val tags = SecureElementManager.getTags() + .onlyCustoms() + .map { it.name } + .toTypedArray() + + withContext(Dispatchers.Main) { + binding.tagInput.setSimpleItems(tags) + } + } + + binding.tagInput.onItemClickListener = OnItemClickListener { _, _, _, _ -> + handleInput(text, true) + } + + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + scope.cancel() + } + + private fun setEditable(editable: Boolean) { + this.editable = editable + + binding.textInputLayout.visibility = if (editable) View.VISIBLE else View.GONE + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + binding.root.setOnLongClickListener(l) + } + + private fun handleInput(editable: Editable, force: Boolean = false) = editable.run { + if (!contains(whitespace) && !force) + return@run + + if (isProtectedTagName) + return@run + + + val tags = split(whitespace).filter { it.isNotBlank() } + tags.forEach { addTag(Tag(it)) } + editable.delete(0, length) + } + + private fun createChip(tag: String, removable: Boolean): Chip { + return Chip( + context, + null, + com.google.android.material.R.style.Widget_Material3_Chip_Input + ).apply { + text = tag + isCloseIconVisible = removable + if (removable) + setOnCloseIconClickListener { + removeTag(tag) + } + } + } + + private fun addDefaultElementTag(elementType: ElementType) { + addTag(elementType.tag, context.getString(elementType.title), false) + } + + @JvmOverloads + fun setTags(tags: List, removable: Boolean = false) { + removeCustomTags() + tags.onlyCustoms().forEach { addTag(it, removable = removable) } + } + + private fun removeCustomTags() { + val iterator = _tags.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (entry.key.isProtectedTagName) + continue + + binding.chipGroup.removeView(entry.value) + iterator.remove() + } + } + + private fun addTag(tag: Tag, text: String = tag.name, removable: Boolean = true) { + val key = if (tag.shouldBeProtected) tag.name else text.capitalize() + if (_tags.containsKey(key)) + return + + val chip = createChip(text.capitalize(), removable) + _tags[key] = chip + binding.chipGroup.apply { addView(chip) } + } + + private fun removeTag(tag: String) { + binding.chipGroup.removeView(_tags.remove(tag.capitalize())) + } + + private fun handleLastChip(isDeleting: Boolean) { + val chip: Chip = binding.chipGroup.children.lastOrNull() as? Chip ?: return + + if (!chip.isCloseIconVisible) + return + + if (!isDeleting) { + chip.isSelected = false + return + } + + if (chip.isSelected) { + removeTag(chip.text.toString()) + } else { + chip.isSelected = true + } + } + + private fun String.capitalize(): String { + return replaceFirstChar { it.uppercase() } + } + + private val CharSequence.isProtectedTagName get() = startsWith(TAG_PREFIX) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create_creditcard.xml b/app/src/main/res/layout/activity_create_creditcard.xml index 23785f63..23f3dcba 100644 --- a/app/src/main/res/layout/activity_create_creditcard.xml +++ b/app/src/main/res/layout/activity_create_creditcard.xml @@ -118,5 +118,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create_password.xml b/app/src/main/res/layout/activity_create_password.xml index a69d7597..d7b8f413 100644 --- a/app/src/main/res/layout/activity_create_password.xml +++ b/app/src/main/res/layout/activity_create_password.xml @@ -105,5 +105,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_view_credit_card.xml b/app/src/main/res/layout/fragment_view_credit_card.xml index ae19b8e3..d2c30658 100644 --- a/app/src/main/res/layout/fragment_view_credit_card.xml +++ b/app/src/main/res/layout/fragment_view_credit_card.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + + app:applyEmpties="false" /> + app:copyable="true" /> + app:changeable="true" /> + android:maxLength="19" /> + + diff --git a/app/src/main/res/layout/fragment_view_password.xml b/app/src/main/res/layout/fragment_view_password.xml index 211d9786..813d31ec 100644 --- a/app/src/main/res/layout/fragment_view_password.xml +++ b/app/src/main/res/layout/fragment_view_password.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + + app:applyEmpties="false" /> + app:title="@string/strength" /> + app:changeable="true" /> + app:copyable="true" /> + app:dialogLayout="@layout/information_view_dialog_additional" /> + + diff --git a/app/src/main/res/layout/layout_tag_view.xml b/app/src/main/res/layout/layout_tag_view.xml new file mode 100644 index 00000000..bfe9d945 --- /dev/null +++ b/app/src/main/res/layout/layout_tag_view.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4b6ffee1..2b2abb8d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -151,4 +151,8 @@ Das könnte ein wenig Zeit in Anspruch nehmen. Bitte haben Sie Geduld Um ein Backup laden zu können, ist eine Authentifizierung erforderlich Authentifizieren um fortzufahren + + Tags + Tags hier hinzufügen + Dieser Prefix ist reserviert und kann nicht verwendet werden \ No newline at end of file diff --git a/app/src/main/res/values/attrs_tag_view.xml b/app/src/main/res/values/attrs_tag_view.xml new file mode 100644 index 00000000..70344ec6 --- /dev/null +++ b/app/src/main/res/values/attrs_tag_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2725554d..3343cd26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,4 +193,9 @@ This might take a little time. Please be patient To load a backup, authentication is required Authenticate to proceed + + Tags + Add tags here + This prefix is reserved and can\'t be used + \ No newline at end of file From 02cc962695042ca1c8a05624fa6e0dcca7d2a914 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 18 Nov 2023 21:47:51 +0100 Subject: [PATCH 07/46] [ProGuard] Adjusted rules --- app/proguard-rules.pro | 1 + 1 file changed, 1 insertion(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9730751c..6b07e08b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -34,6 +34,7 @@ # Application classes that will be serialized/deserialized over Gson -keep class de.davis.passwordmanager.security.element.** { ; } +-keep class de.davis.passwordmanager.database.dtos.** # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) From ba342b54dea904b2589b5a5fa75ab08e9bbf014a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 18 Nov 2023 21:53:58 +0100 Subject: [PATCH 08/46] Rename .java to .kt --- .../details/password/{PasswordDetails.java => PasswordDetails.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/de/davis/passwordmanager/database/entities/details/password/{PasswordDetails.java => PasswordDetails.kt} (100%) diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt similarity index 100% rename from app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.java rename to app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt From acd4fce4020a965916c21ed8089837e8697116df Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 18 Nov 2023 21:53:58 +0100 Subject: [PATCH 09/46] [Strength Estimation] Improved performance --- .../details/password/PasswordDetails.kt | 94 ++++++++----------- .../entities/details/password/Strength.java | 73 -------------- .../entities/details/password/Strength.kt | 43 +++++++++ .../davis/passwordmanager/filter/Filter.java | 10 +- .../gson/ElementDetailTypeAdapter.java | 7 ++ .../password/GeneratePasswordActivity.java | 3 +- .../ui/views/PasswordStrengthBar.java | 20 ++-- 7 files changed, 109 insertions(+), 141 deletions(-) delete mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.kt diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt index 6e8e54e8..3d034475 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt @@ -1,76 +1,58 @@ -package de.davis.passwordmanager.database.entities.details.password; +package de.davis.passwordmanager.database.entities.details.password -import java.io.Serial; -import java.util.Objects; +import com.google.gson.annotations.SerializedName +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.entities.details.ElementDetail +import de.davis.passwordmanager.security.Cryptography +import java.io.Serial -import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.entities.details.ElementDetail; -import de.davis.passwordmanager.security.Cryptography; +class PasswordDetails( + password: String, + var origin: String, + var username: String, +) : ElementDetail { -public class PasswordDetails implements ElementDetail { + var strength: Strength = EstimationHandler.estimate(password) + private set - @Serial - private static final long serialVersionUID = 4938873580704485021L; + @SerializedName("password") + private var passwordEncrypted: ByteArray = Cryptography.encryptAES(password.toByteArray()) - private byte[] password; - private String origin; - private String username; - private Strength strength; - - public PasswordDetails(String password, String origin, String username) { - setPassword(password); - this.origin = origin; - this.username = username; + fun setPassword(password: String) { + strength = EstimationHandler.estimate(password) + passwordEncrypted = Cryptography.encryptAES(password.toByteArray()) } - public String getOrigin() { - return origin; - } + val password get() = String(Cryptography.decryptAES(passwordEncrypted)) - public void setOrigin(String origin) { - this.origin = origin; - } - public String getUsername() { - return username; + override fun getElementType(): ElementType { + return ElementType.PASSWORD } - public void setUsername(String username) { - this.username = username; - } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false - public Strength getStrength() { - return strength; - } + other as PasswordDetails - public void setPassword(String password){ - this.password = Cryptography.encryptAES(password.getBytes()); - this.strength = Strength.estimateStrength(password); - } - - public byte[] getPasswordData() { - return password; - } + if (origin != other.origin) return false + if (username != other.username) return false + if (!passwordEncrypted.contentEquals(other.passwordEncrypted)) return false - public String getPassword(){ - return new String(Cryptography.decryptAES(getPasswordData())); + return true } - @Override - public ElementType getElementType() { - return ElementType.PASSWORD; + override fun hashCode(): Int { + var result = origin.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + passwordEncrypted.contentHashCode() + return result } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PasswordDetails that = (PasswordDetails) o; - return getPassword().equals(that.getPassword()) && Objects.equals(origin, that.origin) && Objects.equals(username, that.username); - } - @Override - public int hashCode() { - return Objects.hash(origin, username, getPassword()); + companion object { + @Serial + private val serialVersionUID = 4938873580704485021L } -} +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java deleted file mode 100644 index 9a612417..00000000 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.java +++ /dev/null @@ -1,73 +0,0 @@ -package de.davis.passwordmanager.database.entities.details.password; - -import android.content.Context; -import android.graphics.Color; - -import androidx.annotation.AttrRes; -import androidx.annotation.ColorInt; -import androidx.annotation.IntDef; -import androidx.annotation.StringRes; - -import com.google.android.material.color.MaterialColors; - -import java.io.Serializable; -import java.util.List; - -import de.davis.passwordmanager.R; -import me.gosimple.nbvcxz.Nbvcxz; -import me.gosimple.nbvcxz.resources.Feedback; -import me.gosimple.nbvcxz.scoring.Result; - -public record Strength(@StrengthType int type, String warning, List suggestions) implements Serializable { - - private static final long serialVersionUID = 3096723233113390206L; - private static final Nbvcxz nbvcxz = new Nbvcxz(); - - public static final int RIDICULOUS = 0; - public static final int WEAK = 1; - public static final int MODERATE = 2; - public static final int STRONG = 3; - public static final int VERY_STRONG = 4; - - @IntDef({ - RIDICULOUS, - WEAK, - MODERATE, - STRONG, - VERY_STRONG - }) - @interface StrengthType {} - - @StringRes - public int getString() { - return switch (type) { - case RIDICULOUS -> R.string.ridiculous; - case WEAK -> R.string.weak; - case MODERATE -> R.string.moderate; - case STRONG -> R.string.strong; - default -> R.string.very_strong; - }; - } - - @AttrRes - private int getColor() { - return switch (type) { - case RIDICULOUS -> R.attr.colorRidiculous; - case WEAK -> R.attr.colorWeak; - case MODERATE -> R.attr.colorModerate; - case STRONG -> R.attr.colorStrong; - default -> R.attr.colorVeryStrong; - }; - } - - @ColorInt - public int getColor(Context context) { - return MaterialColors.getColor(context, getColor(), Color.BLACK); - } - - public static synchronized Strength estimateStrength(String password) { - final Result result = nbvcxz.estimate(password); - final Feedback feedback = result.getFeedback(); - return new Strength(result.getBasicScore(), feedback.getWarning(), feedback.getSuggestion()); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.kt new file mode 100644 index 00000000..4710017e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/Strength.kt @@ -0,0 +1,43 @@ +package de.davis.passwordmanager.database.entities.details.password + +import android.content.Context +import android.graphics.Color +import androidx.annotation.AttrRes +import androidx.annotation.ColorRes +import com.google.android.material.color.MaterialColors +import de.davis.passwordmanager.R +import me.gosimple.nbvcxz.Nbvcxz +import java.io.Serializable + +enum class Strength(@ColorRes val string: Int, @AttrRes val color: Int) : Serializable { + + RIDICULOUS(R.string.ridiculous, R.attr.colorRidiculous), + WEAK(R.string.weak, R.attr.colorWeak), + MODERATE(R.string.moderate, R.attr.colorModerate), + STRONG(R.string.strong, R.attr.colorStrong), + VERY_STRONG(R.string.very_strong, R.attr.colorVeryStrong); + + + fun getColor(context: Context): Int { + return MaterialColors.getColor(context, color, Color.BLACK) + } +} + +object EstimationHandler { + + private val nbvcxz = Nbvcxz() + + @JvmStatic + fun estimate(password: String): Strength { + val result = nbvcxz.estimate(password) + return Strength.entries[result.basicScore] + } + + @JvmStatic + fun estimateWrapper(password: String): Wrapper { + val result = nbvcxz.estimate(password) + return Wrapper(Strength.entries[result.basicScore], result.feedback.warning) + } +} + +class Wrapper(val strength: Strength, val warning: String?) \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java index 0801216b..483be059 100644 --- a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java +++ b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java @@ -85,11 +85,11 @@ public List filter(List elements) { if(element.getElementType() != ElementType.PASSWORD) return false; - return !strengthIds.contains(ID_VERY_STRONG) && ((PasswordDetails)element.getDetail()).getStrength().type() == Strength.VERY_STRONG - || !strengthIds.contains(ID_STRONG) && ((PasswordDetails)element.getDetail()).getStrength().type() == Strength.STRONG - || !strengthIds.contains(ID_MODERATE) && ((PasswordDetails)element.getDetail()).getStrength().type() == Strength.MODERATE - || !strengthIds.contains(ID_WEAK) && ((PasswordDetails)element.getDetail()).getStrength().type() == Strength.WEAK - || !strengthIds.contains(ID_RIDICULOUS) && ((PasswordDetails)element.getDetail()).getStrength().type() == Strength.RIDICULOUS; + return !strengthIds.contains(ID_VERY_STRONG) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.VERY_STRONG + || !strengthIds.contains(ID_STRONG) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.STRONG + || !strengthIds.contains(ID_MODERATE) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.MODERATE + || !strengthIds.contains(ID_WEAK) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.WEAK + || !strengthIds.contains(ID_RIDICULOUS) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.RIDICULOUS; }); return toFilter; diff --git a/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java index d186cd1a..681f68ed 100644 --- a/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java +++ b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java @@ -11,12 +11,19 @@ import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.database.entities.details.ElementDetail; +import de.davis.passwordmanager.database.entities.details.password.Strength; public class ElementDetailTypeAdapter implements JsonSerializer, JsonDeserializer { @Override public ElementDetail deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { int type = json.getAsJsonObject().get("type").getAsInt(); + + JsonElement strengthElement = json.getAsJsonObject().get("strength"); + if(strengthElement != null && strengthElement.isJsonObject()){ + json.getAsJsonObject().addProperty("strength", Strength.values()[strengthElement.getAsJsonObject().get("type").getAsInt()].name()); + } + return context.deserialize(json, ElementType.getTypeByTypeId(type).getElementDetailClass()); } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java index 9b7d2cd8..a50e1374 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/GeneratePasswordActivity.java @@ -25,6 +25,7 @@ import de.davis.passwordmanager.Keys; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.entities.details.password.EstimationHandler; import de.davis.passwordmanager.database.entities.details.password.Strength; import de.davis.passwordmanager.databinding.ActivityGeneratePasswordBinding; import de.davis.passwordmanager.utils.AssetsUtil; @@ -133,7 +134,7 @@ private void estimateStrength(String password){ estimationExecutor = Executors.newSingleThreadExecutor(); estimationExecutor.execute(()->{ - Strength strength = Strength.estimateStrength(password); + Strength strength = EstimationHandler.estimate(password); handler.post(()->{ binding.strength.setInformationText(strength.getString()); binding.strength.setInformationTextColor(strength.getColor(this)); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java b/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java index d83547e3..56fa1d87 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/PasswordStrengthBar.java @@ -25,6 +25,7 @@ import java.util.concurrent.Executors; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.entities.details.password.EstimationHandler; import de.davis.passwordmanager.database.entities.details.password.Strength; import de.davis.passwordmanager.utils.TimeoutUtil; @@ -42,6 +43,7 @@ public class PasswordStrengthBar extends LinearLayout implements TextWatcher { private String lastEstimatedPassword; private Strength strength; + private String warning; public PasswordStrengthBar(Context context) { super(context); @@ -119,20 +121,22 @@ public void update(String password, boolean force){ executorService = Executors.newSingleThreadExecutor(); executorService.execute(() -> { - strength = Strength.estimateStrength(password); + var wrapper = EstimationHandler.estimateWrapper(password); + strength = wrapper.getStrength(); + warning = wrapper.getWarning(); handler.post(() -> { progressIndicator.setIndeterminate(false); progressIndicator.setIndicatorColor(strength.getColor(getContext())); - progressIndicator.setProgress(password.length() > 0 ? strength.type() + 1 : 0); + progressIndicator.setProgress(password.length() > 0 ? strength.ordinal() + 1 : 0); strengthText.setText(strength.getString()); strengthText.setTextColor(strength.getColor(getContext())); strengthText.setVisibility(password.length() > 0 ? View.VISIBLE : View.GONE); - strengthWarning.setText(strength.warning()); + strengthWarning.setText(warning); strengthWarning.setTextColor(strength.getColor(getContext())); - strengthWarning.setVisibility(password.length() == 0 || strength.warning() == null ? GONE : VISIBLE); + strengthWarning.setVisibility(password.length() == 0 || warning == null ? GONE : VISIBLE); }); }); executorService.shutdown(); @@ -143,6 +147,7 @@ public void update(String password, boolean force){ protected Parcelable onSaveInstanceState() { SavedState ss = new SavedState(super.onSaveInstanceState()); ss.strength = strength; + ss.warning = warning; return ss; } @@ -151,18 +156,20 @@ protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); strength = ss.strength; + warning = ss.warning; if(strength == null) return; progressIndicator.setIndicatorColor(strength.getColor(getContext())); strengthText.setVisibility(progressIndicator.getProgress() == 0 ? GONE : VISIBLE); - strengthWarning.setVisibility(progressIndicator.getProgress() == 0 || strength.warning() == null ? GONE : VISIBLE); + strengthWarning.setVisibility(progressIndicator.getProgress() == 0 || warning == null ? GONE : VISIBLE); } static class SavedState extends AbsSavedState { Strength strength; + String warning; SavedState(Parcelable superState) { super(superState); @@ -171,12 +178,14 @@ static class SavedState extends AbsSavedState { SavedState(@NonNull Parcel source, ClassLoader loader) { super(source, loader); strength = (Strength) source.readSerializable(); + warning = source.readString(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeSerializable(strength); + dest.writeString(warning); } public static final Creator CREATOR = @@ -187,7 +196,6 @@ public SavedState createFromParcel(@NonNull Parcel in, ClassLoader loader) { return new SavedState(in, loader); } - @Nullable @Override public SavedState createFromParcel(@NonNull Parcel in) { return new SavedState(in, null); From 04071fab94c60f49dbef0158793d7cd11fa02b6a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 18 Nov 2023 23:36:01 +0100 Subject: [PATCH 10/46] Rename .java to .kt --- .../ui/views/{FilterBottomSheet.java => FilterBottomSheet.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/de/davis/passwordmanager/ui/views/{FilterBottomSheet.java => FilterBottomSheet.kt} (100%) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt similarity index 100% rename from app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.java rename to app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt From cc734bc4202062fe6fc59ab1b20b837bd535dfcc Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 18 Nov 2023 23:36:09 +0100 Subject: [PATCH 11/46] [FilterBottomSheet] Converted to Kotlin --- .../ui/views/FilterBottomSheet.kt | 95 ++++++++----------- app/src/main/res/layout/dialog_filter.xml | 17 ++-- 2 files changed, 51 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt index cc278ed4..0cd6a5ed 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt @@ -1,64 +1,51 @@ -package de.davis.passwordmanager.ui.views; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.google.android.material.chip.ChipGroup; - -import java.util.List; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.filter.Filter; - -public class FilterBottomSheet extends BottomSheetDialogFragment { - - private static final int ID_PASSWORD = R.id.password; - - public FilterBottomSheet() {} - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.dialog_filter, container, false); +package de.davis.passwordmanager.ui.views + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.chip.ChipGroup +import de.davis.passwordmanager.R +import de.davis.passwordmanager.databinding.DialogFilterBinding +import de.davis.passwordmanager.filter.Filter + +class FilterBottomSheet : BottomSheetDialogFragment() { + + private lateinit var binding: DialogFilterBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogFilterBinding.inflate(inflater, container, false) + return binding.root } - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - InformationView type = view.findViewById(R.id.type); - InformationView strength = view.findViewById(R.id.strength); - - ChipGroup strengthGroup = (ChipGroup)((ViewGroup)strength.getContent()).getChildAt(0); - ChipGroup typeGroup = (ChipGroup)((ViewGroup)type.getContent()).getChildAt(0); - Filter.DEFAULT.setStrength(strengthGroup); - Filter.DEFAULT.setType(typeGroup); + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Filter.DEFAULT.setStrength(binding.strengthGroup) + Filter.DEFAULT.setType(binding.typeGroup) - updateSelection(strengthGroup, typeGroup); + updateSelection(binding.strengthGroup, binding.typeGroup) - - typeGroup.setOnCheckedStateChangeListener((group, checkedIds) -> { - strength.setEnabled(checkedIds.contains(ID_PASSWORD)); - Filter.DEFAULT.update(); - }); - - strengthGroup.setOnCheckedStateChangeListener((group, checkedIds) -> Filter.DEFAULT.update()); + binding.typeGroup.setOnCheckedStateChangeListener { _: ChipGroup, checkedIds: List -> + binding.strength.isEnabled = checkedIds.contains(R.id.password) + Filter.DEFAULT.update() + } + binding.strengthGroup.setOnCheckedStateChangeListener { _: ChipGroup, _: List -> Filter.DEFAULT.update() } } - private void updateSelection(ChipGroup... groups){ - List ids = Filter.DEFAULT.getSelectedIds(); - if(ids.isEmpty()) - return; + private fun updateSelection(vararg groups: ChipGroup) { + val ids = Filter.DEFAULT.selectedIds + if (ids.isEmpty()) + return - for (ChipGroup group : groups) { - group.clearCheck(); - ids.forEach(group::check); + groups.forEach { group -> + group.clearCheck() + ids.forEach { group.check(it) } } } -} +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml index 059b652a..91e9e9d1 100644 --- a/app/src/main/res/layout/dialog_filter.xml +++ b/app/src/main/res/layout/dialog_filter.xml @@ -29,6 +29,7 @@ app:contentEnabled="false"> @@ -39,7 +40,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/password" - android:checked="true"/> + android:checked="true" /> + android:checked="true" /> @@ -57,7 +58,9 @@ android:layout_height="wrap_content" app:title="@string/strength" app:contentEnabled="false"> + @@ -70,7 +73,7 @@ android:text="@string/very_strong" android:checked="true" android:textColor="?colorVeryStrong" - app:checkedIconTint="?colorVeryStrong"/> + app:checkedIconTint="?colorVeryStrong" /> + app:checkedIconTint="?colorStrong" /> + app:checkedIconTint="?colorModerate" /> + app:checkedIconTint="?colorWeak" /> + app:checkedIconTint="?colorRidiculous" /> From ad7bf4c725a8d737fd17b404cf75008ce91cf6eb Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Tue, 21 Nov 2023 16:10:07 +0100 Subject: [PATCH 12/46] [FilterBottomSheet] Added Tag filter option --- .../davis/passwordmanager/filter/Filter.java | 28 ++++++-- .../ui/views/FilterBottomSheet.kt | 70 +++++++++++++++++-- app/src/main/res/layout/dialog_filter.xml | 22 ++++++ app/src/main/res/layout/layout_chip.xml | 5 ++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/layout/layout_chip.xml diff --git a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java index 483be059..8c109e93 100644 --- a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java +++ b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java @@ -8,6 +8,7 @@ import de.davis.passwordmanager.R; import de.davis.passwordmanager.database.ElementType; import de.davis.passwordmanager.database.dtos.SecureElement; +import de.davis.passwordmanager.database.entities.Tag; import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; import de.davis.passwordmanager.database.entities.details.password.Strength; @@ -26,6 +27,7 @@ public class Filter { private ChipGroup type; private ChipGroup strength; + private List tags = new ArrayList<>(); private final List selectedIds = new ArrayList<>(); @@ -39,6 +41,14 @@ public void setStrength(ChipGroup strength) { this.strength = strength; } + public void setTags(List tags) { + this.tags = tags; + } + + public List getTags() { + return tags; + } + public Runnable updater; public void setUpdater(Runnable updater) { @@ -46,32 +56,36 @@ public void setUpdater(Runnable updater) { } public void update() { - if(updater != null) - updater.run(); - - if(!groupsSet()) + if(groupsUnset()) return; selectedIds.clear(); selectedIds.addAll(type.getCheckedChipIds()); selectedIds.addAll(strength.getCheckedChipIds()); + + if(updater != null) + updater.run(); } public List getSelectedIds() { return selectedIds; } - private boolean groupsSet(){ - return type != null && strength != null; + private boolean groupsUnset(){ + return type == null || strength == null; } public List filter(List elements) { - if(!groupsSet()) + if(groupsUnset()) return elements; List toFilter = new ArrayList<>(elements); List typeIds = type.getCheckedChipIds(); List strengthIds = strength.getCheckedChipIds(); + + if(!tags.isEmpty()) + toFilter.removeIf(element -> element.getTags().stream().map(Tag::getName).noneMatch(tags::contains)); + if(!typeIds.contains(ID_CREDIT_CARD)) toFilter.removeIf(element -> element.getElementType() == ElementType.CREDIT_CARD); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt index 0cd6a5ed..ca2dbd10 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt @@ -4,16 +4,24 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import de.davis.passwordmanager.R +import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.entities.onlyCustoms import de.davis.passwordmanager.databinding.DialogFilterBinding +import de.davis.passwordmanager.databinding.LayoutChipBinding import de.davis.passwordmanager.filter.Filter +import kotlinx.coroutines.launch class FilterBottomSheet : BottomSheetDialogFragment() { private lateinit var binding: DialogFilterBinding + private val tags = mutableSetOf() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -26,16 +34,70 @@ class FilterBottomSheet : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Filter.DEFAULT.setStrength(binding.strengthGroup) - Filter.DEFAULT.setType(binding.typeGroup) + Filter.DEFAULT.apply { + setStrength(binding.strengthGroup) + setType(binding.typeGroup) + } updateSelection(binding.strengthGroup, binding.typeGroup) - binding.typeGroup.setOnCheckedStateChangeListener { _: ChipGroup, checkedIds: List -> + binding.typeGroup.setOnCheckedStateChangeListener { _, checkedIds -> binding.strength.isEnabled = checkedIds.contains(R.id.password) Filter.DEFAULT.update() } - binding.strengthGroup.setOnCheckedStateChangeListener { _: ChipGroup, _: List -> Filter.DEFAULT.update() } + binding.strengthGroup.setOnCheckedStateChangeListener { _, _ -> Filter.DEFAULT.update() } + + //Tag chip logic + binding.tagGroup.setOnCheckedStateChangeListener { _, selectedIds -> + //This is used for auto select/deselect the default tag (All) when a other tag is selected/deselected + binding.allTagsChip.isChecked = selectedIds.isEmpty() + } + + binding.allTagsChip.setOnCheckedChangeListener { v, isChecked -> + // Clear all other tags selection if the all tag is selected + if (isChecked) { + binding.tagGroup.clearCheck() + return@setOnCheckedChangeListener + } + + // Prevent tag from being always selected -> Only select if no tags are selected + if (binding.tagGroup.checkedChipIds.size > 0) + return@setOnCheckedChangeListener + + v.isChecked = true + } + + lifecycleScope.launch { + val tags = SecureElementManager.getTags().onlyCustoms().map { it.name } + this@FilterBottomSheet.tags.addAll(tags) + val prevTags = Filter.DEFAULT.tags + Filter.DEFAULT.tags = tags + tags.forEach { + val checked = prevTags.contains(it) + binding.tagGroup.addView(createChip(it, checked)) + if (checked) + binding.allTagsChip.isChecked = false + } + } + } + + private fun createChip(tag: String, checked: Boolean): Chip { + return LayoutChipBinding.inflate(layoutInflater).root.apply { + isChecked = checked + text = tag + + setOnCheckedChangeListener { _, isChecked -> + if (isChecked) + tags.add(text.toString()) + else + tags.remove(text.toString()) + + Filter.DEFAULT.apply { + tags = this@FilterBottomSheet.tags.toList() + update() + } + } + } } private fun updateSelection(vararg groups: ChipGroup) { diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml index 91e9e9d1..4cff320c 100644 --- a/app/src/main/res/layout/dialog_filter.xml +++ b/app/src/main/res/layout/dialog_filter.xml @@ -116,5 +116,27 @@ app:checkedIconTint="?colorRidiculous" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_chip.xml b/app/src/main/res/layout/layout_chip.xml new file mode 100644 index 00000000..4de5838e --- /dev/null +++ b/app/src/main/res/layout/layout_chip.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2b2abb8d..b8c23f45 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -155,4 +155,5 @@ Tags Tags hier hinzufügen Dieser Prefix ist reserviert und kann nicht verwendet werden + Alle \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3343cd26..46bb75e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -197,5 +197,6 @@ Tags Add tags here This prefix is reserved and can\'t be used + All \ No newline at end of file From f3192b857182529aaab442abd2c7fbb10cb83cc0 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 24 Nov 2023 14:45:18 +0100 Subject: [PATCH 13/46] [SecureElement] Added data class This allows for better comparisons/equality checks between two SecureElement objects --- .../de/davis/passwordmanager/database/dtos/SecureElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt index 0069ebb7..20281fd0 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt @@ -52,7 +52,7 @@ private object TimestampsParceler : Parceler { @Parcelize @TypeParceler @TypeParceler -class SecureElement @JvmOverloads constructor( +data class SecureElement @JvmOverloads constructor( var title: String, var detail: ElementDetail, var tags: List = listOf(detail.elementType.tag), From 5f605e3533d8ddc7c08a2e48df179a3af3ba29d6 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 1 Dec 2023 21:50:26 +0100 Subject: [PATCH 14/46] [TagEntity] Changed tagID to Long --- .../de/davis/passwordmanager/database/dtos/SecureElement.kt | 4 ++-- .../java/de/davis/passwordmanager/database/entities/Tag.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt index 20281fd0..4b38406e 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt @@ -23,13 +23,13 @@ import java.util.Date private object TagParceler : Parceler { override fun create(parcel: Parcel): Tag { - return Tag(parcel.readString()!!, parcel.readInt()) + return Tag(parcel.readString()!!, parcel.readLong()) } override fun Tag.write(parcel: Parcel, flags: Int) { parcel.apply { writeString(name) - writeInt(tagId) + writeLong(tagId) } } } diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt index 6b2f43d7..ac1412ef 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt @@ -9,7 +9,7 @@ import de.davis.passwordmanager.gson.annotations.Exclude @Entity(indices = [Index("name", unique = true)]) class Tag @JvmOverloads constructor( val name: String, - @Exclude @PrimaryKey(autoGenerate = true) val tagId: Int = 0 + @Exclude @PrimaryKey(autoGenerate = true) val tagId: Long = 0 ) fun Collection.onlyCustoms(): Collection { From 7490b442786eb90bc59fbd2b8ed0b3416617b17a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 21 Dec 2023 23:38:00 +0100 Subject: [PATCH 15/46] [LifecycleOwner] Added doFlowInLifecycle Function Implemented the doFlowInLifecycle extension function. This function enhances UI state management by ensuring that Kotlin flows are handled in accordance with the lifecycle state of the component, thus providing more efficient and safer UI updates. --- .../passwordmanager/ktx/LifecycleOwner.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/src/main/java/de/davis/passwordmanager/ktx/LifecycleOwner.kt diff --git a/app/src/main/java/de/davis/passwordmanager/ktx/LifecycleOwner.kt b/app/src/main/java/de/davis/passwordmanager/ktx/LifecycleOwner.kt new file mode 100644 index 00000000..57c2628f --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ktx/LifecycleOwner.kt @@ -0,0 +1,20 @@ +package de.davis.passwordmanager.ktx + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +fun LifecycleOwner.doFlowInLifecycle( + flow: Flow, + state: Lifecycle.State = Lifecycle.State.STARTED, + operationBlock: suspend Flow.() -> R +) { + lifecycleScope.launch { + repeatOnLifecycle(state) { + operationBlock(flow) + } + } +} \ No newline at end of file From 6dbe1a45dd9d06177c18bdd578097cf25aaff954 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Thu, 21 Dec 2023 23:59:23 +0100 Subject: [PATCH 16/46] [OptionBottomSheet] Refactored to BottomSheetDialogFragment --- .../viewholders/SecureElementViewHolder.java | 8 +++--- .../ui/highlights/HighlightsFragment.java | 2 +- .../ui/views/OptionBottomSheet.java | 27 ++++++++++++------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java index 692340b3..bad29f27 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java @@ -33,7 +33,9 @@ public class SecureElementViewHolder extends BasicViewHolder { private final TextView title, info, type; private final MaterialCheckBox checkBox; - public SecureElementViewHolder(@NonNull View itemView) { + private final FragmentManager fragmentManager; + + public SecureElementViewHolder(@NonNull View itemView, FragmentManager fragmentManager) { super(itemView); more = itemView.findViewById(R.id.more); image = itemView.findViewById(R.id.image); @@ -41,7 +43,7 @@ public SecureElementViewHolder(@NonNull View itemView) { info = itemView.findViewById(R.id.info); type = itemView.findViewById(R.id.type); typeIcon = itemView.findViewById(R.id.typeIcon); - checkBox = itemView.findViewById(R.id.checkboxSelection); + this.fragmentManager = fragmentManager; } @Override @@ -79,7 +81,7 @@ public void bind(@NonNull SecureElement item, String filter, OnItemClickedListen onItemClickedListener.onClicked(item); }); - more.setOnClickListener(v -> new OptionBottomSheet(itemView.getContext(), List.of(item)).show()); + more.setOnClickListener(v -> new OptionBottomSheet(List.of(item)).show(fragmentManager, "OptionSheet")); } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java index aaaca295..94a82980 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java @@ -49,7 +49,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.viewToShow.setVisibility(elements.size() > 0 ? View.GONE : View.VISIBLE); binding.lastUsed.removeViews(2, binding.lastUsed.getChildCount()-2); elements.forEach(element -> { - SecureElementViewHolder viewHolder = new SecureElementViewHolder(getLayoutInflater().inflate(R.layout.item_element, null, false)); + SecureElementViewHolder viewHolder = new SecureElementViewHolder(getLayoutInflater().inflate(R.layout.item_element, null, false), getChildFragmentManager()); viewHolder.bind(element, null, this::launchElement); viewHolder.itemView.setPadding(px, viewHolder.itemView.getPaddingTop(), px, viewHolder.itemView.getPaddingBottom()); binding.lastUsed.addView(viewHolder.itemView); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java index fe023cd4..1c99278c 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java @@ -1,10 +1,14 @@ package de.davis.passwordmanager.ui.views; -import android.content.Context; import android.os.Bundle; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; -import com.google.android.material.bottomsheet.BottomSheetDialog; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import java.util.List; @@ -16,27 +20,30 @@ import de.davis.passwordmanager.manager.ActivityResultManager; import de.davis.passwordmanager.ui.dashboard.DashboardFragment; -public class OptionBottomSheet extends BottomSheetDialog { +public class OptionBottomSheet extends BottomSheetDialogFragment { + private MoreBottomSheetContentBinding binding; private final List elements; - public OptionBottomSheet(Context context, List elements) { - super(context); + public OptionBottomSheet(List elements) { this.elements = elements; } + @Nullable @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - MoreBottomSheetContentBinding binding = MoreBottomSheetContentBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return (binding = MoreBottomSheetContentBinding.inflate(getLayoutInflater())).getRoot(); + } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); if(elements.isEmpty()) return; SecureElement firstElement = elements.get(0); - binding.title.setText(elements.size() > 1 ? getContext().getString(R.string.options) : firstElement.getTitle()); + binding.title.setText(elements.size() > 1 ? requireContext().getString(R.string.options) : firstElement.getTitle()); if(elements.size() > 1) { binding.edit.setVisibility(View.GONE); From daf5282bc7d26a5f262e07a01a6d0ac21c0ab573 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 22 Dec 2023 00:06:10 +0100 Subject: [PATCH 17/46] [Tag] Converted to data class --- .../de/davis/passwordmanager/database/entities/Tag.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt index ac1412ef..d87891c7 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt @@ -1,13 +1,15 @@ package de.davis.passwordmanager.database.entities +import android.content.Context import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey +import de.davis.passwordmanager.database.ElementType import de.davis.passwordmanager.database.TAG_PREFIX import de.davis.passwordmanager.gson.annotations.Exclude @Entity(indices = [Index("name", unique = true)]) -class Tag @JvmOverloads constructor( +data class Tag @JvmOverloads constructor( val name: String, @Exclude @PrimaryKey(autoGenerate = true) val tagId: Long = 0 ) @@ -16,4 +18,7 @@ fun Collection.onlyCustoms(): Collection { return filter { !it.name.startsWith(TAG_PREFIX) } } -val Tag.shouldBeProtected get() = this.name.startsWith(TAG_PREFIX) \ No newline at end of file +val Tag.shouldBeProtected get() = this.name.startsWith(TAG_PREFIX) + +fun Tag.getLocalizedName(context: Context) = + if (shouldBeProtected) context.getString(ElementType.entries.first { e -> e.tag.name == name }.title) else name \ No newline at end of file From 2565560ca31a2e7aa50caf974075433befd6e907 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 22 Dec 2023 00:06:26 +0100 Subject: [PATCH 18/46] [Strings] Added Items: %d --- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b8c23f45..23437434 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -156,4 +156,5 @@ Tags hier hinzufügen Dieser Prefix ist reserviert und kann nicht verwendet werden Alle + Elemente: %d \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46bb75e4..c64b0715 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,5 +198,6 @@ Add tags here This prefix is reserved and can\'t be used All + Items: %d \ No newline at end of file From d1232b1170f4ee4a09367744aa4b40dcd4b3aa1e Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 22 Dec 2023 00:11:11 +0100 Subject: [PATCH 19/46] [Dependencies] Added recyclerview 1.3.2 --- app/build.gradle.kts | 1 + .../dashboard/viewholders/SecureElementViewHolder.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8fb84c7..f40c4c43 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { implementation("com.google.android.gms:play-services-oss-licenses:17.0.1") implementation("com.google.code.gson:gson:2.10.1") + implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.recyclerview:recyclerview-selection:1.1.0") implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") implementation("androidx.navigation:navigation-ui-ktx:2.7.4") diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java index bad29f27..57359fa8 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java @@ -96,7 +96,7 @@ public ItemDetailsLookup.ItemDetails getItemDetails() { return new ItemDetailsLookup.ItemDetails<>() { @Override public int getPosition() { - return getAdapterPosition(); + return getAbsoluteAdapterPosition(); } @NonNull From 02ae73bfdd0c065a21f1ba252609e3620cd9cbe6 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 00:42:26 +0100 Subject: [PATCH 20/46] [Kotlin Utilization] Relocated getPaddingBottom function to utility Kotlin file --- .../de/davis/passwordmanager/ui/LayoutManager.kt | 14 ++++++++++++++ .../passwordmanager/ui/LinearLayoutManager.java | 7 +------ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/LayoutManager.kt diff --git a/app/src/main/java/de/davis/passwordmanager/ui/LayoutManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/LayoutManager.kt new file mode 100644 index 00000000..2d0d737c --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/LayoutManager.kt @@ -0,0 +1,14 @@ +package de.davis.passwordmanager.ui + +import android.content.Context +import android.util.TypedValue + +internal fun getPaddingBottom(context: Context): Int { + val dip = (56 + 16 * 2).toFloat() + + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dip, + context.resources.displayMetrics + ).toInt() +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/LinearLayoutManager.java b/app/src/main/java/de/davis/passwordmanager/ui/LinearLayoutManager.java index 59b242c6..7db43444 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/LinearLayoutManager.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/LinearLayoutManager.java @@ -1,9 +1,7 @@ package de.davis.passwordmanager.ui; import android.content.Context; -import android.content.res.Resources; import android.util.AttributeSet; -import android.util.TypedValue; public class LinearLayoutManager extends androidx.recyclerview.widget.LinearLayoutManager { @@ -31,9 +29,6 @@ public boolean isAutoMeasureEnabled() { @Override public int getPaddingBottom() { - float dip = 56+16*2; - Resources r = context.getResources(); - float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, r.getDisplayMetrics()); - return (int) px; + return LayoutManagerKt.getPaddingBottom(context); } } From a8858dbc533a29dd511b7580f8eac4242e5ae259 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 00:42:30 +0100 Subject: [PATCH 21/46] [Kotlin Utilization] Enhanced code with bundleOf for streamlined argument passing --- .../passwordmanager/ui/auth/AuthenticationActivity.kt | 10 ++++++---- .../davis/passwordmanager/ui/backup/BackupFragment.kt | 7 ++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/auth/AuthenticationActivity.kt b/app/src/main/java/de/davis/passwordmanager/ui/auth/AuthenticationActivity.kt index 4dba8e55..e6d20025 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/auth/AuthenticationActivity.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/auth/AuthenticationActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.os.Bundle import androidx.annotation.IdRes import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit @@ -76,10 +77,11 @@ class AuthenticationActivity : AppCompatActivity() { authenticationRequest: AuthenticationRequest, mainPassword: MainPassword, tag: String? = null - ): FragmentTransaction = replace(containerViewId, F::class.java, Bundle().apply { - putParcelable(KEY_AUTHENTICATION_REQUEST, authenticationRequest) - putParcelable(KEY_MAIN_PASSWORD, mainPassword) - }, tag) + ): FragmentTransaction = replace( + containerViewId, F::class.java, bundleOf( + KEY_AUTHENTICATION_REQUEST to authenticationRequest, KEY_MAIN_PASSWORD to mainPassword + ), tag + ) } fun Context.requestAuthentication(authenticationRequest: AuthenticationRequest) { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt index 183843e7..bf7d0546 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/backup/BackupFragment.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat @@ -149,15 +150,11 @@ class BackupFragment : PreferenceFragmentCompat() { } private fun launchAuth(@Type type: Int, format: String) { - val bundle = Bundle().apply { - putInt("type", type) - putString("format_type", format) - } auth.launch( requireContext().createRequestAuthenticationIntent( AuthenticationRequest.Builder().apply { withMessage(R.string.authenticate_to_proceed) - withAdditionalExtras(bundle) + withAdditionalExtras(bundleOf("type" to type, "format_type" to format)) }.build() ) ) From 3ebe7b91396821d0f7ada444cdf11e7ac40fe5b0 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 00:48:54 +0100 Subject: [PATCH 22/46] [SecureElementDiffCallback] Improved logic --- .../dashboard/SecureElementDiffCallback.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt index 31de0b75..a67756b7 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt @@ -1,7 +1,6 @@ package de.davis.passwordmanager.dashboard import androidx.recyclerview.widget.DiffUtil -import de.davis.passwordmanager.database.dtos.SecureElement class SecureElementDiffCallback( private val oldItems: List, @@ -16,14 +15,14 @@ class SecureElementDiffCallback( } override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldItems[oldItemPosition].id == newItems[newItemPosition].id + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return oldItem::class.isInstance(newItem) && oldItems[oldItemPosition].id == newItems[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - return if (oldItem is SecureElement && newItem is SecureElement) { - oldItem == newItem - } else false + return oldItem == newItem } } \ No newline at end of file From 2fc5466d64056bd1713b34a1fd002a6a76e8e8a9 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 00:54:43 +0100 Subject: [PATCH 23/46] [SlidingBackPaneManager] Added Update State Callback --- .../ui/callbacks/SlidingBackPaneManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java b/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java index 87dfd8e9..61cf2c4a 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java @@ -6,12 +6,15 @@ import androidx.annotation.NonNull; import androidx.slidingpanelayout.widget.SlidingPaneLayout; +import java.util.function.Consumer; + import de.davis.passwordmanager.ui.viewmodels.ScrollingViewModel; public class SlidingBackPaneManager extends OnBackPressedCallback implements SlidingPaneLayout.PanelSlideListener { private final SlidingPaneLayout slidingPaneLayout; private final ScrollingViewModel scrollingViewModel; + private Consumer callback; public SlidingBackPaneManager(SlidingPaneLayout slidingPaneLayout, ScrollingViewModel scrollingViewModel) { super(slidingPaneLayout.isEnabled() && slidingPaneLayout.isOpen()); @@ -22,8 +25,15 @@ public SlidingBackPaneManager(SlidingPaneLayout slidingPaneLayout, ScrollingView slidingPaneLayout.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateState()); } + public void setUpdateStateCallback(Consumer callback) { + this.callback = callback; + } + private void updateState(){ setEnabled(slidingPaneLayout.isEnabled() && slidingPaneLayout.isOpen()); + + if(callback != null) + callback.accept(this); } @Override From 8932c6ac63e2f23a0d181c60d0b8611cbfc9a878 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 00:55:10 +0100 Subject: [PATCH 24/46] [SlidingBackPaneManager] Improved state update logic --- .../ui/callbacks/SlidingBackPaneManager.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java b/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java index 61cf2c4a..d250ae81 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/callbacks/SlidingBackPaneManager.java @@ -43,9 +43,7 @@ public void handleOnBackPressed() { } @Override - public void onPanelSlide(@NonNull View panel, float slideOffset) { - updateState(); - } + public void onPanelSlide(@NonNull View panel, float slideOffset) {} @Override public void onPanelOpened(@NonNull View panel) { @@ -54,6 +52,6 @@ public void onPanelOpened(@NonNull View panel) { @Override public void onPanelClosed(@NonNull View panel) { - + updateState(); } } From 46993a56b1a8cbcd42ca5f000ca4a58484c0c89e Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 00:59:13 +0100 Subject: [PATCH 25/46] [Filter] Converted and improved Filter --- .../davis/passwordmanager/filter/Filter.java | 111 ------------------ .../de/davis/passwordmanager/filter/Filter.kt | 68 +++++++++++ .../ui/views/FilterBottomSheet.kt | 104 ++++++++++------ 3 files changed, 137 insertions(+), 146 deletions(-) delete mode 100644 app/src/main/java/de/davis/passwordmanager/filter/Filter.java create mode 100644 app/src/main/java/de/davis/passwordmanager/filter/Filter.kt diff --git a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java b/app/src/main/java/de/davis/passwordmanager/filter/Filter.java deleted file mode 100644 index 8c109e93..00000000 --- a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java +++ /dev/null @@ -1,111 +0,0 @@ -package de.davis.passwordmanager.filter; - -import com.google.android.material.chip.ChipGroup; - -import java.util.ArrayList; -import java.util.List; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.ElementType; -import de.davis.passwordmanager.database.dtos.SecureElement; -import de.davis.passwordmanager.database.entities.Tag; -import de.davis.passwordmanager.database.entities.details.password.PasswordDetails; -import de.davis.passwordmanager.database.entities.details.password.Strength; - -public class Filter { - - public static final Filter DEFAULT = new Filter(); - - private static final int ID_PASSWORD = R.id.password; - private static final int ID_CREDIT_CARD = R.id.creditCard; - - private static final int ID_VERY_STRONG = R.id.veryStrong; - private static final int ID_STRONG = R.id.strong; - private static final int ID_MODERATE = R.id.moderate; - private static final int ID_WEAK = R.id.weak; - private static final int ID_RIDICULOUS = R.id.ridiculous; - - private ChipGroup type; - private ChipGroup strength; - private List tags = new ArrayList<>(); - - private final List selectedIds = new ArrayList<>(); - - private Filter(){} - - public void setType(ChipGroup type) { - this.type = type; - } - - public void setStrength(ChipGroup strength) { - this.strength = strength; - } - - public void setTags(List tags) { - this.tags = tags; - } - - public List getTags() { - return tags; - } - - public Runnable updater; - - public void setUpdater(Runnable updater) { - this.updater = updater; - } - - public void update() { - if(groupsUnset()) - return; - - selectedIds.clear(); - selectedIds.addAll(type.getCheckedChipIds()); - selectedIds.addAll(strength.getCheckedChipIds()); - - if(updater != null) - updater.run(); - } - - public List getSelectedIds() { - return selectedIds; - } - - private boolean groupsUnset(){ - return type == null || strength == null; - } - - public List filter(List elements) { - if(groupsUnset()) - return elements; - - List toFilter = new ArrayList<>(elements); - List typeIds = type.getCheckedChipIds(); - List strengthIds = strength.getCheckedChipIds(); - - if(!tags.isEmpty()) - toFilter.removeIf(element -> element.getTags().stream().map(Tag::getName).noneMatch(tags::contains)); - - if(!typeIds.contains(ID_CREDIT_CARD)) - toFilter.removeIf(element -> element.getElementType() == ElementType.CREDIT_CARD); - - - if(!typeIds.contains(ID_PASSWORD)) { - toFilter.removeIf(element -> element.getElementType() == ElementType.PASSWORD); - return toFilter; - } - - toFilter.removeIf(element -> { - if(element.getElementType() != ElementType.PASSWORD) - return false; - - return !strengthIds.contains(ID_VERY_STRONG) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.VERY_STRONG - || !strengthIds.contains(ID_STRONG) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.STRONG - || !strengthIds.contains(ID_MODERATE) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.MODERATE - || !strengthIds.contains(ID_WEAK) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.WEAK - || !strengthIds.contains(ID_RIDICULOUS) && ((PasswordDetails)element.getDetail()).getStrength() == Strength.RIDICULOUS; - }); - - return toFilter; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/filter/Filter.kt b/app/src/main/java/de/davis/passwordmanager/filter/Filter.kt new file mode 100644 index 00000000..de862221 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/filter/Filter.kt @@ -0,0 +1,68 @@ +package de.davis.passwordmanager.filter + +import de.davis.passwordmanager.database.ElementType +import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.entities.details.password.PasswordDetails +import de.davis.passwordmanager.database.entities.details.password.Strength +import de.davis.passwordmanager.database.entities.onlyCustoms +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +object Filter { + + private val _filterFlow = MutableStateFlow(FilterOptions()) + val filterFlow = _filterFlow.asStateFlow() + + private val _tagsFlow = MutableStateFlow(listOf()) + val tagsFlow = _tagsFlow.asStateFlow() + + fun updateFilter(block: FilterOptions.() -> Unit) { + val current = filterFlow.value + val filter = current + .copy(tags = mutableListOf().apply { addAll(current.tags) }) + .apply(block) + _filterFlow.value = filter + } + + fun updateTags(tags: List) { + _tagsFlow.value = tags + } + + data class FilterOptions( + var password: StrengthFilter? = StrengthFilter(), + var creditCard: Boolean = true, + var tags: MutableList = mutableListOf() //empty = all + ) + + data class StrengthFilter( + var veryStrong: Boolean = true, + var strong: Boolean = true, + var moderate: Boolean = true, + var weak: Boolean = true, + var ridiculous: Boolean = true + ) +} + +fun Collection.applyFilter(filter: Filter.FilterOptions): List { + return filter { if (it.elementType == ElementType.CREDIT_CARD) filter.creditCard else true } + .filter { if (it.elementType == ElementType.PASSWORD) filter.password != null else true } + .filter { + if (it.elementType != ElementType.PASSWORD) + return@filter true + + (it.detail as PasswordDetails).let { pwdDetail -> + filter.password?.let { pwd -> + pwd.veryStrong && pwdDetail.strength == Strength.VERY_STRONG + || pwd.strong && pwdDetail.strength == Strength.STRONG + || pwd.moderate && pwdDetail.strength == Strength.MODERATE + || pwd.weak && pwdDetail.strength == Strength.WEAK + || pwd.ridiculous && pwdDetail.strength == Strength.RIDICULOUS + } ?: true + } + } + .filter { + if (filter.tags.isEmpty()) true else filter.tags.any { tag -> + it.tags.onlyCustoms().map { it.name }.contains(tag) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt index ca2dbd10..60a538c3 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt @@ -4,17 +4,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import de.davis.passwordmanager.R -import de.davis.passwordmanager.database.SecureElementManager -import de.davis.passwordmanager.database.entities.onlyCustoms import de.davis.passwordmanager.databinding.DialogFilterBinding import de.davis.passwordmanager.databinding.LayoutChipBinding import de.davis.passwordmanager.filter.Filter -import kotlinx.coroutines.launch +import de.davis.passwordmanager.ktx.doFlowInLifecycle +import kotlinx.coroutines.flow.collectLatest class FilterBottomSheet : BottomSheetDialogFragment() { @@ -34,18 +31,24 @@ class FilterBottomSheet : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Filter.DEFAULT.apply { - setStrength(binding.strengthGroup) - setType(binding.typeGroup) - } - - updateSelection(binding.strengthGroup, binding.typeGroup) - binding.typeGroup.setOnCheckedStateChangeListener { _, checkedIds -> binding.strength.isEnabled = checkedIds.contains(R.id.password) - Filter.DEFAULT.update() + + Filter.updateFilter { + password = if (checkedIds.contains(R.id.password)) { + getStrengthFilter(binding.strengthGroup.checkedChipIds) + } else { + null + } + + creditCard = checkedIds.contains(R.id.creditCard) + } + } + binding.strengthGroup.setOnCheckedStateChangeListener { _, checkedIds -> + Filter.updateFilter { + password = getStrengthFilter(checkedIds) + } } - binding.strengthGroup.setOnCheckedStateChangeListener { _, _ -> Filter.DEFAULT.update() } //Tag chip logic binding.tagGroup.setOnCheckedStateChangeListener { _, selectedIds -> @@ -57,6 +60,9 @@ class FilterBottomSheet : BottomSheetDialogFragment() { // Clear all other tags selection if the all tag is selected if (isChecked) { binding.tagGroup.clearCheck() + Filter.updateFilter { + tags.clear() + } return@setOnCheckedChangeListener } @@ -67,47 +73,75 @@ class FilterBottomSheet : BottomSheetDialogFragment() { v.isChecked = true } - lifecycleScope.launch { + viewLifecycleOwner.doFlowInLifecycle(Filter.tagsFlow) { + collectLatest { tags -> + val prevTags = Filter.filterFlow.value.tags + val onlyOneTagAvailable = tags.size == 1 + tags.forEach { + val checked = prevTags.contains(it) || onlyOneTagAvailable + binding.tagGroup.addView( + createChip( + it, + checked, + staySelected = onlyOneTagAvailable + ) + ) + if (checked) + binding.allTagsChip.isChecked = false + } + + if (onlyOneTagAvailable) + binding.allTagsChip.visibility = View.GONE + + } + } + + /*lifecycleScope.launch { val tags = SecureElementManager.getTags().onlyCustoms().map { it.name } - this@FilterBottomSheet.tags.addAll(tags) - val prevTags = Filter.DEFAULT.tags - Filter.DEFAULT.tags = tags + //this@FilterBottomSheet.tags.addAll(tags) + val prevTags = FilterV2.filterFlow.value.tags + tags.forEach { val checked = prevTags.contains(it) binding.tagGroup.addView(createChip(it, checked)) if (checked) binding.allTagsChip.isChecked = false - } - } + } //TODO there is a bug with the filter -> when selecting all tags the "all" tag should be selected + }*/ } - private fun createChip(tag: String, checked: Boolean): Chip { + private fun getStrengthFilter(checkedIds: List): Filter.StrengthFilter { + return Filter.StrengthFilter( + veryStrong = checkedIds.contains(R.id.veryStrong), + strong = checkedIds.contains(R.id.strong), + moderate = checkedIds.contains(R.id.moderate), + weak = checkedIds.contains(R.id.weak), + ridiculous = checkedIds.contains(R.id.ridiculous) + ) + } + + private fun createChip(tag: String, checked: Boolean, staySelected: Boolean): Chip { return LayoutChipBinding.inflate(layoutInflater).root.apply { isChecked = checked text = tag setOnCheckedChangeListener { _, isChecked -> + if (staySelected) { + this.isChecked = true + return@setOnCheckedChangeListener + } if (isChecked) tags.add(text.toString()) else tags.remove(text.toString()) - Filter.DEFAULT.apply { - tags = this@FilterBottomSheet.tags.toList() - update() + Filter.updateFilter { + if (isChecked) { + tags += text.toString() + } else + tags -= text.toString() } } } } - - private fun updateSelection(vararg groups: ChipGroup) { - val ids = Filter.DEFAULT.selectedIds - if (ids.isEmpty()) - return - - groups.forEach { group -> - group.clearCheck() - ids.forEach { group.check(it) } - } - } } \ No newline at end of file From 02ca0e93437c4b4c899d24530ff8fc3a969970eb Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 13:23:21 +0100 Subject: [PATCH 26/46] [Database] Added Tag infrastructure --- .../database/SecureElementManager.kt | 5 +++++ .../database/daos/SecureElementWithTagDao.kt | 4 ++++ .../passwordmanager/database/dtos/TagWithCount.kt | 13 +++++++++++++ .../database/entities/TagWithCountEntity.kt | 5 +++++ 4 files changed, 27 insertions(+) create mode 100644 app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/database/entities/TagWithCountEntity.kt diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt index d171adc0..ca105ca6 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import de.davis.passwordmanager.database.daos.SecureElementWithTagDao import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.dtos.TagWithCount +import de.davis.passwordmanager.database.dtos.toDto import de.davis.passwordmanager.database.entities.Tag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -46,6 +48,9 @@ object SecureElementManager { suspend fun getTags(): List = dao.getTags() + fun getTagsWithCount(): Flow> = + dao.getTagsWithCount().map { it.map { list -> list.toDto() } } + @JvmStatic @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) fun getByTitleSync(query: String): List { diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index 5f8aba35..b01f112e 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -10,6 +10,7 @@ import androidx.room.Transaction import androidx.room.Update import de.davis.passwordmanager.database.entities.SecureElementEntity import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.TagWithCountEntity import de.davis.passwordmanager.database.entities.junction.SecureElementTagCrossRef import de.davis.passwordmanager.database.entities.wrappers.CombinedElement import kotlinx.coroutines.flow.Flow @@ -38,6 +39,9 @@ abstract class SecureElementWithTagDao { @Query("SELECT * FROM Tag") abstract suspend fun getTags(): List + @Query("SELECT Tag.*, COUNT(SecureElementTagCrossRef.tagId) AS count FROM Tag LEFT JOIN SecureElementTagCrossRef ON Tag.tagId = SecureElementTagCrossRef.tagId GROUP BY Tag.tagId") + abstract fun getTagsWithCount(): Flow> + @Insert protected abstract suspend fun insert(secureElementEntity: SecureElementEntity): Long diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt new file mode 100644 index 00000000..b9490d31 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt @@ -0,0 +1,13 @@ +package de.davis.passwordmanager.database.dtos + +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.TagWithCountEntity + +data class TagWithCount(val tag: Tag, val count: Int) : Item { + + override val id: Long + get() = tag.tagId +} + +fun TagWithCountEntity.toDto() = TagWithCount(tag, count) diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/TagWithCountEntity.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/TagWithCountEntity.kt new file mode 100644 index 00000000..4beba71e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/TagWithCountEntity.kt @@ -0,0 +1,5 @@ +package de.davis.passwordmanager.database.entities + +import androidx.room.Embedded + +data class TagWithCountEntity(@Embedded val tag: Tag, val count: Int) \ No newline at end of file From 3455cc85d4d2730d5fb1a44726bc4e1bebbf957a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 23 Dec 2023 13:28:16 +0100 Subject: [PATCH 27/46] [Tag View] Implemented tag view --- .../dashboard/DashboardAdapter.java | 421 ------------------ .../viewholders/BasicViewHolder.java | 17 +- .../viewholders/HeaderViewHolder.java | 55 --- .../viewholders/SecureElementViewHolder.java | 29 +- .../passwordmanager/ui/GridLayoutManager.kt | 21 + .../ui/dashboard/DashboardAdapter.kt | 124 ++++++ .../ui/dashboard/DashboardFragment.java | 301 ------------- .../ui/dashboard/DashboardFragment.kt | 255 +++++++++++ .../ui/dashboard/DashboardViewModel.kt | 93 ++++ .../ui/dashboard/managers/AbsItemManager.kt | 67 +++ .../dashboard/managers/ElementItemManager.kt | 227 ++++++++++ .../ui/dashboard/managers/TagItemManager.kt | 101 +++++ .../DefaultElementMenuProvider.kt | 52 +++ .../ui/highlights/HighlightsFragment.java | 5 +- .../ui/viewmodels/DashboardViewModel.java | 66 --- app/src/main/res/layout/item_element.xml | 97 ---- app/src/main/res/layout/item_header.xml | 33 -- app/src/main/res/layout/layout_element.xml | 101 +++++ .../layout/layout_element_letter_header.xml | 11 + app/src/main/res/layout/layout_tag_item.xml | 8 + app/src/main/res/layout/list_pane.xml | 20 +- 21 files changed, 1104 insertions(+), 1000 deletions(-) delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/HeaderViewHolder.java create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/GridLayoutManager.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt delete mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt delete mode 100644 app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java delete mode 100644 app/src/main/res/layout/item_element.xml delete mode 100644 app/src/main/res/layout/item_header.xml create mode 100644 app/src/main/res/layout/layout_element.xml create mode 100644 app/src/main/res/layout/layout_element_letter_header.xml create mode 100644 app/src/main/res/layout/layout_tag_item.xml diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java b/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java deleted file mode 100644 index 08bc684b..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/DashboardAdapter.java +++ /dev/null @@ -1,421 +0,0 @@ -package de.davis.passwordmanager.dashboard; - -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.selection.SelectionPredicates; -import androidx.recyclerview.selection.SelectionTracker; -import androidx.recyclerview.selection.StorageStrategy; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.checkbox.MaterialCheckBox; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.StreamSupport; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.selection.KeyProvider; -import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup; -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; -import de.davis.passwordmanager.dashboard.viewholders.HeaderViewHolder; -import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder; -import de.davis.passwordmanager.database.dtos.SecureElement; - -public class DashboardAdapter extends RecyclerView.Adapter> { - - private static final int HEADER_TYPE = 0; - private static final int ITEM_TYPE = 1; - - private String filter; - - private final SparseArray
headers; - private final ArrayList items; - private SelectionTracker tracker; - - private BasicViewHolder.OnItemClickedListener onItemClickedListener; - - private StateChangeHandler stateChangeHandler; - - public DashboardAdapter(){ - headers = new SparseArray<>(); - items = new ArrayList<>(); - setHasStableIds(true); - } - - public SelectionTracker getTracker() { - return tracker; - } - - public void setOnItemClickedListener(BasicViewHolder.OnItemClickedListener onItemClickedListener) { - this.onItemClickedListener = onItemClickedListener; - } - - public boolean isHeaderPosition(int realPosition){ - return headers.get(realPosition) != null; - } - - private Header createHeader(@NonNull SecureElement item){ - return new Header(item.getLetter()); - } - - private SecureElementViewHolder onCreateItemViewHolder(@NonNull ViewGroup parent){ - return new SecureElementViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_element, parent, false)); - } - - @NonNull - @Override - public BasicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return viewType == HEADER_TYPE - ? new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_header, parent, false)) - : onCreateItemViewHolder(parent); - } - - public void setFilter(String filter) { - this.filter = filter; - } - - @SuppressWarnings("unchecked") - @Override - public void onBindViewHolder(@NonNull BasicViewHolder holder, int position) { - if(isHeaderPosition(position)){ - ((BasicViewHolder)holder).bind(headers.get(position), filter, onItemClickedListener); - return; - } - - SecureElement data = getData(position); - if(data == null) - return; - - ((BasicViewHolder)holder).bind(data, filter, onItemClickedListener); - } - - @Override - public void onBindViewHolder(@NonNull BasicViewHolder holder, int position, @NonNull List payloads) { - if(payloads.isEmpty()){ - onBindViewHolder(holder, position); - return; - } - - //The payload represents whether an item can be selected or not. - Object payload = payloads.get(0); - if(Objects.equals(payload, "Selection-Changed")) - payload = tracker.hasSelection(); - - if(!(payload instanceof Boolean)) - return; - - handleSelectionUpdates(getItemId(position), holder); - } - - @Override - public long getItemId(int position) { - return isHeaderPosition(position) ? headers.get(position).getId() : items.get(getDataPosition(position)).getId(); - } - - @Override - public int getItemCount() { - return items.size() + headers.size(); - } - - @Override - public int getItemViewType(int position) { - return isHeaderPosition(position) ? HEADER_TYPE : ITEM_TYPE; - } - - public List getEntries(){ - List entries = new ArrayList<>(items.size()+headers.size()); - entries.addAll(items); - for (int i = 0; i < headers.size(); i++) { - entries.add(headers.keyAt(i), headers.valueAt(i)); - } - - return entries; - } - - public SecureElement getData(int realPosition){ - int dataPosition = getDataPosition(realPosition); - return dataPosition > RecyclerView.NO_POSITION ? items.get(dataPosition) : null; - } - - public int getDataPosition(int realPosition){ - if(isHeaderPosition(realPosition)) - return RecyclerView.NO_POSITION; - - int offset = 0; - for(int i = 0; i < headers.size(); i++){ - if(headers.keyAt(i) > realPosition) - break; - else - offset--; - } - - return realPosition + offset; - } - - public int getRealPosition(int itemIndex){ - if(itemIndex < 0 || itemIndex >= items.size()) - return RecyclerView.NO_POSITION; - - for(int i = headers.size() - 1; i >= 0; i--){ - int key = headers.keyAt(i); - if(key - i <= itemIndex) - return itemIndex + i + 1; - } - - return RecyclerView.NO_POSITION; - } - - public int getRealPositionById(long id){ - for (int i = 0; i < headers.size(); i++) { - int pos = headers.keyAt(i); - if(getItemId(pos) == id) - return pos; - } - - - return items.stream() - .filter(item -> item.getId() == id) - .findFirst() - .map(item -> getRealPosition(items.indexOf(item))) - .orElse(RecyclerView.NO_POSITION); - } - - public List getIdsInHeader(Header header){ - int headerIndex = headers.indexOfValue(header); - if(headerIndex < 0) - return new ArrayList<>(); - - int from = headers.keyAt(headerIndex) +1; //inclusive - int to = headers.size() == headerIndex+1 ? getItemCount() : headers.keyAt(headerIndex+1); //exclusive - - return IntStream.range(from, to).mapToLong(this::getItemId).boxed().collect(Collectors.toList()); - } - - public Header getHeaderByRealPosition(int realPosition){ - return headers.get(realPosition); - } - - public Header findHeaderByRealItemPosition(int realItemPosition){ - if(isHeaderPosition(realItemPosition)) - return headers.get(realItemPosition); - - for (int i = headers.size()-1; i >= 0; i--) { - int position = headers.keyAt(i); - if(position < realItemPosition) - return headers.get(position); - } - - return null; - } - - public List getSelectedElements(){ - return StreamSupport.stream(tracker.getSelection().spliterator(), false).map(this::getElementById).filter(Objects::nonNull).collect(Collectors.toList()); - } - - public SecureElement getElementById(long id){ - int dataPosition = getDataPosition(getRealPositionById(id)); - if(dataPosition < 0) - return null; - - return items.get(dataPosition); - } - - public void applyWithTracker(RecyclerView recyclerView){ - recyclerView.setAdapter(this); - tracker = new SelectionTracker.Builder<>( - "tracker", - recyclerView, - new KeyProvider(recyclerView), - new SecureElementDetailsLookup(recyclerView), - StorageStrategy.createLongStorage() - ).withSelectionPredicate(SelectionPredicates.createSelectAnything()).build(); - - tracker.addObserver(new SelectionStateHandler(recyclerView)); - } - - private boolean shouldCreateHeader(int index){ - if (index == 0) - return true; - - SecureElement previous = items.get(index - 1); - SecureElement current = items.get(index); - - return previous == null || current == null || previous.getLetter() != current.getLetter(); - } - - private void prepareHeaderDataSet(){ - Collections.sort(items); - - SparseArray
headersList = new SparseArray<>(); - for (int i = 0; i < headers.size(); i++) { - Header header = headers.valueAt(i); - headersList.put((int) header.getId(), header); - } - - headers.clear(); - - for (int i = 0; i < items.size(); i++) { - if (!shouldCreateHeader(i)) - continue; - - SecureElement item = items.get(i); - Header header = headersList.get(item.getLetter()); - if(header == null) - header = createHeader(item); - - headers.put(headers.size() + i, header); - } - } - - public void removeSelectedElements(){ - getTracker().clearSelection(); - } - - public void showOnly(List elements){ - update(elements); - } - - public void update(List overrideElements){ - List oldEntries = getEntries(); - - items.clear(); - - items.addAll(overrideElements); - prepareHeaderDataSet(); - - SecureElementDiffCallback callback = new SecureElementDiffCallback(oldEntries, getEntries()); - DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); - result.dispatchUpdatesTo(this); - } - - public void setStateChangeHandler(StateChangeHandler stateChangeHandler){ - this.stateChangeHandler = stateChangeHandler; - } - - private void handleSelectionUpdates(long id, BasicViewHolder viewHolder){ - int realPos = getRealPositionById(id); - if(isHeaderPosition(realPos)){ - ((HeaderViewHolder)viewHolder).onChildSelected(getTracker().hasSelection(), getState(getHeaderByRealPosition(realPos))); - }else - viewHolder.onBindSelectablePayload(tracker.hasSelection(), tracker.isSelected(id)); - } - - private class SelectionStateHandler extends SelectionTracker.SelectionObserver implements RecyclerView.OnChildAttachStateChangeListener { - - private boolean hadSelections; - private boolean changing; - - private final RecyclerView recyclerView; - - public SelectionStateHandler(RecyclerView recyclerView) { - this.recyclerView = recyclerView; - recyclerView.addOnChildAttachStateChangeListener(this); - } - - @Override - public void onItemStateChanged(@NonNull Long key, boolean selected) { - if(changing) - return; - - changing = true; - long headerId = handleSelection(key, selected); - changing = false; - - if(stateChangeHandler != null) - stateChangeHandler.onStateChanged(getSelectedElements().size()); - - // Update all items only if the selected item is the first or the last item that gets - // selected - if(tracker.hasSelection() == hadSelections) - return; - - // To avoid re-notifying all observers to get their work done, this loop binds the - // payload to the views directly. Differences do not have to be calculated. - for (int i = 0; i < getItemCount(); i++) { - long itemId = getItemId(i); - if(itemId == key || itemId == headerId) - continue; - - BasicViewHolder viewHolder = (BasicViewHolder) recyclerView.findViewHolderForAdapterPosition(i); - if(viewHolder == null) - continue; - - viewHolder.onBindSelectablePayload(tracker.hasSelection(), false); - } - - hadSelections = tracker.hasSelection(); - } - - @Override - public void onChildViewAttachedToWindow(@NonNull View view) { - long id = getItemId(recyclerView.getChildAdapterPosition(view)); - BasicViewHolder viewHolder = (BasicViewHolder) recyclerView.findContainingViewHolder(view); - if(viewHolder == null) - return; - - handleSelectionUpdates(id, viewHolder); - } - - @Override - public void onChildViewDetachedFromWindow(@NonNull View view) {} - - /** - * Handles each selection so that selecting all items of the same category also selects - * the header. When the header is selected, all items with in this header are selected. - * This also works the other way around. - * @param id the id of the selected or deselected item. - * @param selected true if the item got selected, false otherwise. - */ - private long handleSelection(long id, boolean selected){ - int realItemPosition = getRealPositionById(id); - if(isHeaderPosition(realItemPosition)){ - tracker.setItemsSelected(getIdsInHeader(getHeaderByRealPosition(realItemPosition)), selected); - return id; - } - - Header header = findHeaderByRealItemPosition(realItemPosition); - if(header == null) - return RecyclerView.NO_POSITION; - - long headerKey = header.getId(); - - int state = getState(header); - - if(state == MaterialCheckBox.STATE_CHECKED) tracker.select(headerKey); - else tracker.deselect(headerKey); - - HeaderViewHolder headerViewHolder = ((HeaderViewHolder)recyclerView.findViewHolderForItemId(headerKey)); - if(headerViewHolder != null) - headerViewHolder.onChildSelected(getTracker().hasSelection(), state); - - return headerKey; - } - } - - private int getState(Header header){ - List idsInHeader = getIdsInHeader(header); - int count = (int) idsInHeader - .stream() - .filter(itemId -> tracker.getSelection().contains(itemId)).count(); - - int state; - if(count == 0) state = MaterialCheckBox.STATE_UNCHECKED; - else if(count == idsInHeader.size()) state = MaterialCheckBox.STATE_CHECKED; - else state = MaterialCheckBox.STATE_INDETERMINATE; - - return state; - } - - public interface StateChangeHandler{ - void onStateChanged(int selectedItems); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java index 9139be1e..c7c76149 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java @@ -5,20 +5,25 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import de.davis.passwordmanager.dashboard.Item; import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup; -import de.davis.passwordmanager.database.dtos.SecureElement; -public abstract class BasicViewHolder extends RecyclerView.ViewHolder implements SecureElementDetailsLookup.ItemDetailsLookup { +public abstract class BasicViewHolder extends RecyclerView.ViewHolder implements SecureElementDetailsLookup.ItemDetailsLookup { public BasicViewHolder(@NonNull View itemView) { super(itemView); } - public abstract void bind(@NonNull T item, String filter, OnItemClickedListener onItemClickedListener); + public void bind(@NonNull T item, String filter, OnItemClickedListener onItemClickedListener, boolean selected){ + bindGeneral(item, filter, onItemClickedListener); + handleSelectionState(selected); + } + + protected abstract void bindGeneral(@NonNull T item, String filter, OnItemClickedListener onItemClickedListener); - public abstract void onBindSelectablePayload(boolean selectable, boolean selected); + protected abstract void handleSelectionState(boolean selected); - public interface OnItemClickedListener { - void onClicked(SecureElement element); + public interface OnItemClickedListener { + void onClicked(T element); } } \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/HeaderViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/HeaderViewHolder.java deleted file mode 100644 index 59e68f1f..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/HeaderViewHolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package de.davis.passwordmanager.dashboard.viewholders; - -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.selection.ItemDetailsLookup; - -import com.google.android.material.checkbox.MaterialCheckBox; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.Header; - -public class HeaderViewHolder extends BasicViewHolder
{ - - private final TextView header; - private final MaterialCheckBox checkBox; - - public HeaderViewHolder(@NonNull View itemView) { - super(itemView); - this.header = itemView.findViewById(R.id.title); - this.checkBox = itemView.findViewById(R.id.checkBox); - } - - @Override - public void bind(@NonNull Header item, String filter, OnItemClickedListener onItemClickedListener) { - header.setText(String.valueOf(item.header())); - } - - @Override - public void onBindSelectablePayload(boolean selectable, boolean selected) { - checkBox.setVisibility(selectable ? View.VISIBLE : View.GONE); - checkBox.setChecked(selected); - } - - public void onChildSelected(boolean selectable, @MaterialCheckBox.CheckedState int selected){ - checkBox.setVisibility(selectable ? View.VISIBLE : View.GONE); - checkBox.setCheckedState(selected); - } - - @Override - public ItemDetailsLookup.ItemDetails getItemDetails() { - return new ItemDetailsLookup.ItemDetails<>() { - @Override - public int getPosition() { - return getAdapterPosition(); - } - - @Override - public Long getSelectionKey() { - return getItemId(); - } - }; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java index 57359fa8..93528cf1 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java @@ -1,20 +1,25 @@ package de.davis.passwordmanager.dashboard.viewholders; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.text.Spannable; import android.text.SpannableString; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; import androidx.recyclerview.selection.ItemDetailsLookup; -import com.google.android.material.checkbox.MaterialCheckBox; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.color.MaterialColors; import java.util.List; @@ -31,23 +36,24 @@ public class SecureElementViewHolder extends BasicViewHolder { private final ImageButton more; private final ImageView image, typeIcon; private final TextView title, info, type; - private final MaterialCheckBox checkBox; + public final TextView letterView; private final FragmentManager fragmentManager; - public SecureElementViewHolder(@NonNull View itemView, FragmentManager fragmentManager) { - super(itemView); + public SecureElementViewHolder(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @NonNull FragmentManager fragmentManager) { + super(inflater.inflate(R.layout.layout_element, parent, false)); more = itemView.findViewById(R.id.more); image = itemView.findViewById(R.id.image); title = itemView.findViewById(R.id.title); info = itemView.findViewById(R.id.info); type = itemView.findViewById(R.id.type); + letterView = itemView.findViewById(R.id.header); typeIcon = itemView.findViewById(R.id.typeIcon); this.fragmentManager = fragmentManager; } @Override - public void bind(@NonNull SecureElement item, String filter, OnItemClickedListener onItemClickedListener) { + public void bindGeneral(@NonNull SecureElement item, String filter, OnItemClickedListener onItemClickedListener) { Context context = itemView.getContext(); String text = item.getTitle(); @@ -84,11 +90,16 @@ public void bind(@NonNull SecureElement item, String filter, OnItemClickedListen more.setOnClickListener(v -> new OptionBottomSheet(List.of(item)).show(fragmentManager, "OptionSheet")); } + public void setLetterVisible(boolean visible, char letter){ + this.letterView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + this.letterView.setText(visible ? Character.toString(letter) : ""); + } + + @SuppressLint("PrivateResource") @Override - public void onBindSelectablePayload(boolean selectable, boolean selected) { - more.setVisibility(selectable ? View.GONE : View.VISIBLE); - checkBox.setVisibility(selectable ? View.VISIBLE : View.GONE); - checkBox.setChecked(selected); + protected void handleSelectionState(boolean selected) { + ((MaterialCardView) itemView).setChecked(selected); + ((MaterialCardView) itemView).setStrokeWidth(selected ? (int) itemView.getResources().getDimension(com.google.android.material.R.dimen.m3_comp_outlined_card_outline_width) : 0); } @Override diff --git a/app/src/main/java/de/davis/passwordmanager/ui/GridLayoutManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/GridLayoutManager.kt new file mode 100644 index 00000000..406a8150 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/GridLayoutManager.kt @@ -0,0 +1,21 @@ +package de.davis.passwordmanager.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.GridLayoutManager + +class GridLayoutManager( + val context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : GridLayoutManager(context, attributeSet, defStyleAttr, defStyleRes) { + + init { + spanCount = 2 + } + + override fun getPaddingBottom(): Int { + return getPaddingBottom(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt new file mode 100644 index 00000000..831a6949 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt @@ -0,0 +1,124 @@ +package de.davis.passwordmanager.ui.dashboard + +import android.os.Bundle +import android.view.ViewGroup +import androidx.recyclerview.selection.SelectionPredicates +import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.selection.StorageStrategy +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.dashboard.SecureElementDiffCallback +import de.davis.passwordmanager.dashboard.selection.KeyProvider +import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup +import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.ui.dashboard.managers.AbsItemManager + +class DashboardAdapter(private val onUpdate: (DashboardAdapter) -> Unit) : + RecyclerView.Adapter>() { + + private var itemManager: AbsItemManager = AbsItemManager.Empty() + + private lateinit var recyclerView: RecyclerView + + var filter: String = "" + + private lateinit var tracker: SelectionTracker + + init { + setHasStableIds(true) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicViewHolder { + return itemManager.createViewHolder(parent) + } + + override fun getItemCount(): Int = itemManager.items.size + + override fun onBindViewHolder(holder: BasicViewHolder, position: Int) { + itemManager.bind(holder, filter, position, tracker.isSelected(getItemId(position))) + } + + override fun getItemViewType(position: Int): Int { + return itemManager.viewType + } + + override fun getItemId(position: Int): Long { + return itemManager.getItemId(position) + } + + fun apply( + recyclerView: RecyclerView, + onSelectionChanged: (selectedElements: List) -> Unit = {} + ) = recyclerView.apply { + setHasFixedSize(true) + this@DashboardAdapter.recyclerView = recyclerView + adapter = this@DashboardAdapter + + tracker = SelectionTracker.Builder( + "tracker", + this, + KeyProvider(this), + SecureElementDetailsLookup(this), + StorageStrategy.createLongStorage() + ).withSelectionPredicate(SelectionPredicates.createSelectAnything()).build() + + tracker.addObserver(object : SelectionTracker.SelectionObserver() { + var oldSelection: List? = null + + override fun onSelectionChanged() { + if (tracker.selection.toList() == oldSelection) + return + + onSelectionChanged(getSelectedElements()) + oldSelection = tracker.selection.toList() + } + }) + } + + fun getSelectedElements(): List { + return tracker.selection + .map { itemManager.getElementById(it) } + .mapNotNull { it }.toList() + } + + fun clearSelection() = tracker.clearSelection() + + private fun configureRecyclerView() = recyclerView.apply { + layoutManager = itemManager.getLayoutManager(recyclerView.context) + for (i in 0 until recyclerView.itemDecorationCount) { + recyclerView.removeItemDecorationAt(i) + } + itemManager.getItemDecoration()?.let { + recyclerView.addItemDecoration(it) + } + } + + @Suppress("UNCHECKED_CAST") + fun update(itemManager: AbsItemManager) { + val old = this.itemManager.items + + this.itemManager = itemManager as AbsItemManager + itemManager.prepareDataset() + + configureRecyclerView() + + if (old.toTypedArray().contentEquals(itemManager.items.toTypedArray())) + return + + + onUpdate(this) + + val callback = SecureElementDiffCallback(old, itemManager.items) + val result = DiffUtil.calculateDiff(callback) + result.dispatchUpdatesTo(this) + } + + fun onSaveInstanceState(bundle: Bundle) { + tracker.onSaveInstanceState(bundle) + } + + fun onRestoreInstanceState(bundle: Bundle?) { + tracker.onRestoreInstanceState(bundle) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java deleted file mode 100644 index 622a1ada..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java +++ /dev/null @@ -1,301 +0,0 @@ -package de.davis.passwordmanager.ui.dashboard; - -import android.content.Context; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.AttributeSet; -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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SearchView; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.MenuProvider; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavController; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.RecyclerView; -import androidx.slidingpanelayout.widget.SlidingPaneLayout; - -import com.google.android.material.appbar.AppBarLayout; - -import java.util.List; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.DashboardAdapter; -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; -import de.davis.passwordmanager.database.dtos.SecureElement; -import de.davis.passwordmanager.databinding.FragmentDashboardBinding; -import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.ui.callbacks.SearchViewBackPressedHandler; -import de.davis.passwordmanager.ui.callbacks.SlidingBackPaneManager; -import de.davis.passwordmanager.ui.viewmodels.DashboardViewModel; -import de.davis.passwordmanager.ui.viewmodels.ScrollingViewModel; -import de.davis.passwordmanager.ui.views.AddBottomSheet; -import de.davis.passwordmanager.ui.views.FilterBottomSheet; -import de.davis.passwordmanager.ui.views.OptionBottomSheet; - -public class DashboardFragment extends Fragment implements SearchView.OnQueryTextListener { - - private FragmentDashboardBinding binding; - - private DashboardViewModel viewModel; - private ScrollingViewModel scrollingViewModel; - - private final DashboardAdapter adapter = new DashboardAdapter(); - - private boolean oldState = true; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - binding = FragmentDashboardBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - binding.listPane.recyclerView.setHasFixedSize(true); - - NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager().findFragmentById(R.id.elementContainer); - if(navHostFragment == null) - return; - - NavController navController = navHostFragment.getNavController(); - binding.getRoot().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED); - - ((AppCompatActivity)requireActivity()).setSupportActionBar(binding.listPane.searchBar); - - binding.listPane.viewAddFirst.setOnClickListener(v -> showBottomSheet()); - - ActivityResultManager arm = ActivityResultManager.getOrCreateManager(getClass(), this); - arm.registerCreate(); - arm.registerEdit(null); - - /* - TODO - SecureElementManager manager = SecureElementManager.getInstance(); - manager.setTriggerDataChanged(sem -> { - boolean hasElements = sem.hasElements(); - binding.listPane.progress.setVisibility(View.GONE); - ' - binding.listPane.recyclerView.setVisibility(hasElements ? View.VISIBLE : View.GONE); - binding.listPane.viewToShow.setVisibility(hasElements ? View.GONE : View.VISIBLE); - }); - */ - - adapter.applyWithTracker(binding.listPane.recyclerView); - - BasicViewHolder.OnItemClickedListener onItemClickedListener = element -> { - scrollingViewModel.setVisibility(false); - binding.listPane.searchView.hide(); - Bundle bundle = new Bundle(); - bundle.putParcelable("element", element); - navController.popBackStack(); - navController.navigate(element.getElementType().getViewFragmentId(), bundle); - - binding.getRoot().open(); - }; - - adapter.setOnItemClickedListener(onItemClickedListener); - - DashboardAdapter searchResultAdapter = new DashboardAdapter(); - searchResultAdapter.setOnItemClickedListener(onItemClickedListener); - binding.listPane.recyclerViewResults.setAdapter(searchResultAdapter); - - - addMenu(); - - binding.listPane.searchView.getEditText().addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - - @Override - public void afterTextChanged(Editable s) { - viewModel.search(s.toString()); - } - }); - - - viewModel = new ViewModelProvider(this, ViewModelProvider.Factory.from(DashboardViewModel.initializer)).get(DashboardViewModel.class); - viewModel.getElements().observe(getViewLifecycleOwner(), this::update); - viewModel.getSearchResults().observe(getViewLifecycleOwner(), secureElements -> { - searchResultAdapter.update(secureElements); - searchResultAdapter.setFilter(viewModel.getSearchQuery()); - if(!TextUtils.isEmpty(viewModel.getSearchQuery()) && secureElements.isEmpty()){ - binding.listPane.noResults.setVisibility(View.VISIBLE); - return; - } - - binding.listPane.noResults.setVisibility(View.GONE); - }); - - - scrollingViewModel = new ViewModelProvider(requireActivity()).get(ScrollingViewModel.class); - - SlidingBackPaneManager slidingBackPaneManager = new SlidingBackPaneManager(binding.slidingPaneLayout, scrollingViewModel); - requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), slidingBackPaneManager); - requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new SearchViewBackPressedHandler(binding.listPane.searchView)); - - - //Animation for fab and bottom nav bar - binding.listPane.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - } - - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - scrollingViewModel.setConsumedY(dy); - } - }); - - if(getArguments() == null) - return; - - SecureElement element = getArguments().getParcelable("element"); - if(element == null) - return; - - onItemClickedListener.onClicked(element); - oldState = false; - } - - private void update(List secureElements){ - adapter.update(secureElements); - - boolean hasElements = adapter.getItemCount() > 0; - binding.listPane.progress.setVisibility(View.GONE); - - binding.listPane.recyclerView.setVisibility(hasElements ? View.VISIBLE : View.GONE); - binding.listPane.viewToShow.setVisibility(hasElements ? View.GONE : View.VISIBLE); - } - - @Override - public void onPause() { - super.onPause(); - oldState = Boolean.TRUE.equals(scrollingViewModel.getVisibility().getValue()); - scrollingViewModel.setVisibility(false); - } - - @Override - public void onResume() { - super.onResume(); - scrollingViewModel.setVisibility(oldState); - } - - private void addMenu(){ - requireActivity().addMenuProvider(new MenuProvider() { - @Override - public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { - menuInflater.inflate(R.menu.view_menu, menu); - adapter.setStateChangeHandler(selectedItems -> { - requireActivity().invalidateMenu(); - binding.listPane.searchBar.setHint(selectedItems > 0 ? getString(R.string.selected_items, selectedItems) : getString(android.R.string.search_go)); - }); - } - - @Override - public void onPrepareMenu(@NonNull Menu menu) { - MenuProvider.super.onPrepareMenu(menu); - menu.findItem(R.id.more).setVisible(adapter.getTracker().hasSelection()); - } - - @Override - public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { - if(menuItem.getItemId() == R.id.more){ - OptionBottomSheet optionBottomSheet = new OptionBottomSheet(requireContext(), adapter.getSelectedElements()); - optionBottomSheet.show(); - adapter.getTracker().clearSelection(); - }else if(menuItem.getItemId() == R.id.filter){ - new FilterBottomSheet().show(getParentFragmentManager(), "FilterDialog"); - } - return true; - } - }, getViewLifecycleOwner(), Lifecycle.State.RESUMED); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - adapter.getTracker().onSaveInstanceState(outState); - if(binding == null) - return; - outState.putCharSequence("searchbar_hint", binding.listPane.searchBar.getHint()); - } - - @Override - public void onViewStateRestored(@Nullable Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - adapter.getTracker().onRestoreInstanceState(savedInstanceState); - if(savedInstanceState == null) - return; - - binding.listPane.searchBar.setHint(savedInstanceState.getCharSequence("searchbar_hint", getString(android.R.string.search_go))); - } - - private void showBottomSheet(){ - new AddBottomSheet().show(getParentFragmentManager(), "add-bottom-sheet"); - } - - @Override - public boolean onQueryTextSubmit(String query) { - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - viewModel.search(newText); - return true; - } - - public static class ScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior { - - private boolean initialized = false; - - public ScrollingViewBehavior() {} - - public ScrollingViewBehavior(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onDependentViewChanged( - @NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) { - boolean changed = super.onDependentViewChanged(parent, child, dependency); - if (!initialized && dependency instanceof AppBarLayout appBarLayout) { - initialized = true; - setAppBarLayoutColor(appBarLayout); - } - return changed; - } - - private void setAppBarLayoutColor(AppBarLayout appBarLayout) { - appBarLayout.setBackgroundColor(appBarLayout.getContext().getColor(android.R.color.transparent)); - - // Remove AppBarLayout elevation shadow - appBarLayout.setElevation(0); - } - - @Override - protected boolean shouldHeaderOverlapScrollingChild() { - return false; - } - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt new file mode 100644 index 00000000..e7499dd1 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt @@ -0,0 +1,255 @@ +package de.davis.passwordmanager.ui.dashboard + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.os.bundleOf +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnScrollListener +import androidx.slidingpanelayout.widget.SlidingPaneLayout +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.dtos.TagWithCount +import de.davis.passwordmanager.databinding.FragmentDashboardBinding +import de.davis.passwordmanager.filter.Filter +import de.davis.passwordmanager.ktx.doFlowInLifecycle +import de.davis.passwordmanager.ktx.getParcelableCompat +import de.davis.passwordmanager.manager.ActivityResultManager +import de.davis.passwordmanager.ui.callbacks.SearchViewBackPressedHandler +import de.davis.passwordmanager.ui.callbacks.SlidingBackPaneManager +import de.davis.passwordmanager.ui.dashboard.managers.AbsItemManager +import de.davis.passwordmanager.ui.dashboard.managers.ElementItemManager +import de.davis.passwordmanager.ui.dashboard.managers.TagItemManager +import de.davis.passwordmanager.ui.dashboard.menuprovider.DefaultElementMenuProvider +import de.davis.passwordmanager.ui.viewmodels.ScrollingViewModel +import kotlinx.coroutines.flow.collectLatest + +class DashboardFragment : Fragment() { + + private lateinit var binding: FragmentDashboardBinding + + private val adapter = DashboardAdapter { updateUI() } + + val scrollingViewModel: ScrollingViewModel by activityViewModels() + private val dashboardViewModel: DashboardViewModel by viewModels() + + private lateinit var navController: NavController + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentDashboardBinding.inflate(inflater, container, false) + return binding.root + } + + @SuppressLint("PrivateResource") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.root.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED + + val menuProvider = DefaultElementMenuProvider(parentFragmentManager, adapter) + requireActivity().addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.STARTED + ) + + adapter.apply(binding.listPane.recyclerView) { + binding.listPane.searchBar.hint = if (it.isNotEmpty()) + getString(R.string.selected_items, it.size) + else + getString(android.R.string.search_go) + + menuProvider.updateMenu(requireActivity()) { + selectedElements = it + } + } + + val navHostFragment = + childFragmentManager.findFragmentById(R.id.elementContainer) as NavHostFragment + navController = navHostFragment.navController + + (requireActivity() as AppCompatActivity).setSupportActionBar(binding.listPane.searchBar) + + + val backToTagsCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + dashboardViewModel.updateState(ListState.Tag) + } + } + + val slidingBackPaneManager = + SlidingBackPaneManager(binding.slidingPaneLayout, scrollingViewModel) + + slidingBackPaneManager.setUpdateStateCallback { + backToTagsCallback.isEnabled = !it.isEnabled + } + + requireActivity().onBackPressedDispatcher.apply { + addCallback( + viewLifecycleOwner, + slidingBackPaneManager + ) + + addCallback( + viewLifecycleOwner, + SearchViewBackPressedHandler(binding.listPane.searchView) + ) + + addCallback(viewLifecycleOwner, backToTagsCallback) + } + + // Animation for fab and bottom nav bar + binding.listPane.recyclerView.addOnScrollListener(object : OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + scrollingViewModel.setConsumedY(dy) + } + }) + // ------------------------------------ + + ActivityResultManager.getOrCreateManager(javaClass, this).apply { + registerCreate() + registerEdit {} + } + + viewLifecycleOwner.doFlowInLifecycle(dashboardViewModel.state) { + collectLatest { pair -> + @Suppress("UNCHECKED_CAST") + pair?.let { (listState, data) -> + // handle menu + menuProvider.updateMenu(requireActivity()) { + filterVisible = listState !is ListState.Tag + } + // ----------- + + val absItemManager = when (listState) { + is ListState.Tag -> TagItemManager( + data.data as List, + onClick = { + dashboardViewModel.updateState(ListState.Element(it.tag.name)) + } + ) + + // Element and AllElements + else -> { + val elements = data.data as List + if (data is ListData.Elements) { + Filter.updateTags(data.tags) + } + + ElementItemManager( + elements, + ::launchElement, + childFragmentManager + ) + } + } + + update(absItemManager) + + // Update icon + backToTagsCallback.isEnabled = listState is ListState.Element + + if (backToTagsCallback.isEnabled) { + binding.listPane.searchBar.navigationIcon = + AppCompatResources.getDrawable( + requireContext(), + R.drawable.ic_baseline_close_24 + ) + + binding.listPane.searchBar.setNavigationOnClickListener { + dashboardViewModel.updateState(ListState.Tag) + } + } else { + binding.listPane.searchBar.navigationIcon = + AppCompatResources.getDrawable( + requireContext(), + com.google.android.material.R.drawable.ic_search_black_24 + ) + + binding.listPane.searchBar.setNavigationOnClickListener(null) + } + } + } + } + + binding.listPane.searchView.editText.doAfterTextChanged { + dashboardViewModel.search(it.toString()) + } + val searchResultAdapter = DashboardAdapter {} + searchResultAdapter.apply(binding.listPane.recyclerViewResults) + + doFlowInLifecycle(dashboardViewModel.searchResults) { + collectLatest { + searchResultAdapter.update( + ElementItemManager( + it.second, + onClick = ::launchElement, + childFragmentManager + ) + ) + searchResultAdapter.filter = it.first + + binding.listPane.noResults.visibility = + if (it.first.isNotEmpty() && it.second.isEmpty()) View.VISIBLE + else View.GONE + } + } + + + arguments?.getParcelableCompat("element", SecureElement::class.java)?.let { + launchElement(it) + } + } + + private fun launchElement(element: SecureElement) { + scrollingViewModel.setVisibility(false) + binding.listPane.searchView.hide() + val bundle = bundleOf("element" to element) + navController.apply { + popBackStack() + navigate(element.elementType.viewFragmentId, bundle) + } + + binding.root.open() + } + + private fun update(update: AbsItemManager) { + adapter.update(update) + + updateUI() + } + + private fun updateUI() { + binding.listPane.progress.visibility = View.GONE + + binding.listPane.viewToShow.visibility = + if (adapter.itemCount > 0) View.GONE else View.VISIBLE + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + adapter.onSaveInstanceState(outState) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + adapter.onRestoreInstanceState(savedInstanceState) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 00000000..78467249 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,93 @@ +package de.davis.passwordmanager.ui.dashboard + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.dtos.TagWithCount +import de.davis.passwordmanager.database.entities.onlyCustoms +import de.davis.passwordmanager.filter.Filter +import de.davis.passwordmanager.filter.applyFilter +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.launch + + +val DEFAULT_LIST_SATE = ListState.Tag + +class DashboardViewModel : ViewModel() { + + private val _state = MutableStateFlow>?>(null) + val state = _state.asStateFlow() + + private val _listState = MutableStateFlow(DEFAULT_LIST_SATE) + + private val elementsFlow = SecureElementManager.getSecureElementsFlow() + private val tagsFlow = SecureElementManager.getTagsWithCount() + + + // For searching + private val _searchResults = + MutableStateFlow>>("" to emptyList()) + val searchResults: StateFlow>> = _searchResults.asStateFlow() + private var searchJob: Job? = null + + fun updateState(state: ListState) { + _listState.value = state + } + + fun search(query: String) { + searchJob?.cancel() + + searchJob = viewModelScope.launch { + _searchResults.value = query to SecureElementManager.getByTitle(query) + } + } + + init { + viewModelScope.launch { + combine( + _listState, + elementsFlow, + tagsFlow, + Filter.filterFlow + ) { listState: ListState, secureElements: List, tagsWithCount: List, filter -> + listState to when (listState) { + is ListState.Tag -> ListData.Tags(tagsWithCount) + + is ListState.Element -> { + val elements = secureElements + .filter { it.tags.any { tag -> listState.tagName == tag.name } } + + ListData.Elements( + elements.applyFilter(filter), + elements.flatMap { it.tags.onlyCustoms() }.map { it.name }.distinct() + ) + } + + is ListState.AllElements -> ListData.Elements(secureElements, emptyList()) + } + }.distinctUntilChangedBy { it.second.data }.collect { + _state.value = it + } + } + } +} + +sealed class ListState { + data object Tag : ListState() + data class Element(val tagName: String) : ListState() + data object AllElements : ListState() +} + +sealed class ListData(val data: List) { + + class Tags(data: List) : ListData(data) + class Elements(data: List, val tags: List) : + ListData(data) +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt new file mode 100644 index 00000000..0d366a90 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt @@ -0,0 +1,67 @@ +package de.davis.passwordmanager.ui.dashboard.managers + +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import androidx.recyclerview.widget.RecyclerView.LayoutManager +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder + +sealed class AbsItemManager( + initialItems: List, + private val onClick: ((E) -> Unit)? = null +) { + + val items = mutableListOf().apply { addAll(initialItems) } + + fun prepareDataset() = items.apply { + sortItems() + } + + fun update(collection: Collection) = items.apply { + clear() + addAll(collection) + } + + abstract fun createViewHolder(parent: ViewGroup): BasicViewHolder + abstract fun getLayoutManager(context: Context): LayoutManager + open fun getItemDecoration(): ItemDecoration? = null + abstract fun getItemId(position: Int): Long + abstract fun MutableList.sortItems() + + abstract val viewType: Int + + open fun bind( + viewHolder: BasicViewHolder, + filter: String, + position: Int, + selected: Boolean + ) { + viewHolder.bind(items[position], filter, onClick, selected) + } + + fun getElementById(id: Long): E? { + return items.mapIndexed { index, e -> getItemId(index) to e } + .find { (eId, _) -> eId == id }?.second + } + + class Empty : AbsItemManager(arrayListOf()) { + + override val viewType: Int + get() = 0 + + override fun createViewHolder(parent: ViewGroup): BasicViewHolder { + throw IllegalArgumentException("Can not call createViewHolder on a dummy Manager") + } + + override fun getLayoutManager(context: Context): LayoutManager { + throw IllegalArgumentException("Can not call getLayoutManager on a dummy Manager") + } + + override fun getItemId(position: Int): Long = 0 + + override fun MutableList.sortItems() {} + + } +} + diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt new file mode 100644 index 00000000..2b569b96 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt @@ -0,0 +1,227 @@ +package de.davis.passwordmanager.ui.dashboard.managers + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Point +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder +import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.ui.LinearLayoutManager + +class ElementItemManager( + initialItems: List, + onClick: ((SecureElement) -> Unit)?, + private val fragmentManager: FragmentManager +) : + AbsItemManager(initialItems, onClick) { + + override fun createViewHolder(parent: ViewGroup): BasicViewHolder { + return SecureElementViewHolder(LayoutInflater.from(parent.context), parent, fragmentManager) + } + + override fun bind( + viewHolder: BasicViewHolder, + filter: String, + position: Int, + selected: Boolean + ) { + super.bind(viewHolder, filter, position, selected) + (viewHolder as SecureElementViewHolder).setLetterVisible( + isHeader(position), + items[position].letter + ) + } + + override val viewType: Int + get() = 1 + + + override fun getLayoutManager(context: Context): RecyclerView.LayoutManager { + return LinearLayoutManager(context) + } + + override fun getItemId(position: Int): Long { + return items[position].id + } + + override fun MutableList.sortItems() = sort() + + fun isHeader(position: Int): Boolean { + val grouped = items.groupBy { it.letter } + val item = items[position] + return grouped[item.letter]?.firstOrNull() == item + } + + override fun getItemDecoration(): RecyclerView.ItemDecoration { + return object : RecyclerView.ItemDecoration() { + private var header: TextView? = null + + private var defaultTranslationX: Float = -1f + private var headerHeight: Int = 0 + + private fun prepareHeaderView(itemPosition: Int, parent: ViewGroup): View { + if (header == null) { + header = LayoutInflater.from(parent.context) + .inflate(R.layout.layout_element_letter_header, parent, false) as TextView + + measureLayout(header!!, parent) + } + + return header!!.apply { + text = items[itemPosition].letter.toString() + } + } + + private fun measureLayout(view: View, parent: ViewGroup) { + val widthSpec = + View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) + val heightSpec = View.MeasureSpec.makeMeasureSpec( + parent.height, + View.MeasureSpec.UNSPECIFIED + ) + + val childWidth = ViewGroup.getChildMeasureSpec( + widthSpec, + parent.paddingLeft + parent.paddingRight, + view.layoutParams.width + ) + val childHeight = ViewGroup.getChildMeasureSpec( + heightSpec, + parent.paddingTop + parent.paddingBottom, + view.layoutParams.height + ) + + view.measure(childWidth, childHeight) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + + headerHeight = view.measuredHeight + } + + private var lastInvisibleHeader: View? = null + + override fun onDrawOver( + c: Canvas, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.onDrawOver(c, parent, state) + + + val topChild = parent.getChildAt(0) ?: return + val topChildPosition = parent.getChildAdapterPosition(topChild) + if (topChildPosition == RecyclerView.NO_POSITION) return + + lastInvisibleHeader?.visibility = View.VISIBLE + + val topChildHeader = topChild.getHeader() + + prepareHeaderView(topChildPosition, parent).run { + val contactPoint = bottom + val childInContact = getChildInContact(parent, contactPoint) + + if (defaultTranslationX < 0) + defaultTranslationX = getPositionRelativeToOtherView( + topChildHeader, + parent + ).x.toFloat() + + if (childInContact != null) { + if (topChild != childInContact) { + moveHeader( + c, + this, + getPositionRelativeToOtherView( + childInContact.getHeader(), + parent + ).y.toFloat(), + defaultTranslationX + ) + } + return + } + + topChildHeader.visibility = View.INVISIBLE + lastInvisibleHeader = topChildHeader + drawHeader(c, this, defaultTranslationX) + } + } + + private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? { + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + if (!isHeader(parent.getChildAdapterPosition(child))) + continue + + val relPos = getRelativeTopAndBottom( + child.getHeader(), + parent + ) + + + if (relPos.second > contactPoint && relPos.first <= contactPoint) { + return child + } + } + return null + } + + + fun getRelativeTopAndBottom(child: View, parent: View): Pair { + val parentLocation = IntArray(2) + val childLocation = IntArray(2) + + // Choose either getLocationOnScreen or getLocationInWindow based on your needs + parent.getLocationOnScreen(parentLocation) + child.getLocationOnScreen(childLocation) + + val relativeTop = childLocation[1] - parentLocation[1] + val relativeBottom = relativeTop + child.height + + return Pair(relativeTop, relativeBottom) + } + + + fun getPositionRelativeToOtherView(targetView: View, relativeToView: View): Point { + val targetLocation = IntArray(2) + val relativeLocation = IntArray(2) + + targetView.getLocationOnScreen(targetLocation) + relativeToView.getLocationOnScreen(relativeLocation) + + val relativeX = targetLocation[0] - relativeLocation[0] + val relativeY = targetLocation[1] - relativeLocation[1] + + return Point(relativeX, relativeY) + } + + + private fun moveHeader( + c: Canvas, + header: View, + nextChildX: Float, + x: Float + ) { + c.save() + c.translate(x, nextChildX - header.height) + header.draw(c) + c.restore() + } + + private fun drawHeader(c: Canvas, header: View, x: Float) { + c.save() + c.translate(x, 0f) + header.draw(c) + c.restore() + } + + private fun View.getHeader(): View = findViewById(R.id.header) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt new file mode 100644 index 00000000..aa4d8ade --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt @@ -0,0 +1,101 @@ +package de.davis.passwordmanager.ui.dashboard.managers + +import android.content.Context +import android.graphics.Rect +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.selection.ItemDetailsLookup +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.card.MaterialCardView +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.database.dtos.TagWithCount +import de.davis.passwordmanager.database.entities.getLocalizedName +import de.davis.passwordmanager.ui.GridLayoutManager +import de.davis.passwordmanager.ui.views.InformationView + +class TagItemManager( + initialItems: List, + onClick: ((TagWithCount) -> Unit)? +) : + AbsItemManager(initialItems, onClick) { + + override fun createViewHolder(parent: ViewGroup): BasicViewHolder { + return object : BasicViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.layout_tag_item, parent, false) + ) { + override fun getItemDetails(): ItemDetailsLookup.ItemDetails { + return object : ItemDetailsLookup.ItemDetails() { + override fun getPosition(): Int = absoluteAdapterPosition + override fun getSelectionKey(): Long = itemId + } + } + + override fun handleSelectionState(selected: Boolean) { + (itemView as MaterialCardView).isChecked = selected + } + + override fun bindGeneral( + item: TagWithCount, + filter: String?, + onItemClickedListener: OnItemClickedListener? + ) { + (itemView as InformationView).apply { + setTitle(item.tag.getLocalizedName(parent.context)) + setInformationText(context.getString(R.string.items_n, item.count)) + } + + if (item.count > 0) + itemView.setOnClickListener { + onItemClickedListener?.onClicked(item) + } + } + } + } + + override fun getLayoutManager(context: Context): RecyclerView.LayoutManager { + return GridLayoutManager(context) + } + + override fun getItemDecoration(): RecyclerView.ItemDecoration { + return object : RecyclerView.ItemDecoration() { + + private val spanCount = 2 + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildAdapterPosition(view) + val column = position % spanCount + val spacing = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + parent.context.resources.displayMetrics + ).toInt() + + outRect.left = spacing - column * spacing / spanCount + outRect.right = (column + 1) * spacing / spanCount + + if (position < spanCount) { + outRect.top = spacing + } + outRect.bottom = spacing + } + } + } + + override fun getItemId(position: Int): Long { + return -items[position].tag.tagId - 5 + } + + override val viewType: Int + get() = 2 + + override fun MutableList.sortItems() {} +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt new file mode 100644 index 00000000..4bedff6e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt @@ -0,0 +1,52 @@ +package de.davis.passwordmanager.ui.dashboard.menuprovider + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.fragment.app.FragmentManager +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.ui.dashboard.DashboardAdapter +import de.davis.passwordmanager.ui.views.FilterBottomSheet +import de.davis.passwordmanager.ui.views.OptionBottomSheet + +class DefaultElementMenuProvider( + val manager: FragmentManager, + private val adapter: DashboardAdapter +) : MenuProvider { + + var filterVisible: Boolean = false + var selectedElements: List = emptyList() + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.view_menu, menu) + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + menu.apply { + findItem(R.id.more).setVisible(selectedElements.isNotEmpty()) + findItem(R.id.filter).setVisible(filterVisible) + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId == R.id.more) { + val optionBottomSheet = OptionBottomSheet(selectedElements.map { it as SecureElement }) + optionBottomSheet.show(manager, "MoreDialog") + adapter.clearSelection() + } else if (menuItem.itemId == R.id.filter) { + FilterBottomSheet().show(manager, "FilterDialog") + } + return true + } + + fun updateMenu(menuHost: MenuHost, block: DefaultElementMenuProvider.() -> Unit) { + apply(block) + menuHost.invalidateMenu() + } +} + diff --git a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java index 94a82980..492286cc 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java @@ -49,8 +49,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.viewToShow.setVisibility(elements.size() > 0 ? View.GONE : View.VISIBLE); binding.lastUsed.removeViews(2, binding.lastUsed.getChildCount()-2); elements.forEach(element -> { - SecureElementViewHolder viewHolder = new SecureElementViewHolder(getLayoutInflater().inflate(R.layout.item_element, null, false), getChildFragmentManager()); - viewHolder.bind(element, null, this::launchElement); + SecureElementViewHolder viewHolder = new SecureElementViewHolder(getLayoutInflater(), binding.lastUsed, getChildFragmentManager()); + viewHolder.bind(element, null, this::launchElement, false); + viewHolder.letterView.setVisibility(View.GONE); viewHolder.itemView.setPadding(px, viewHolder.itemView.getPaddingTop(), px, viewHolder.itemView.getPaddingBottom()); binding.lastUsed.addView(viewHolder.itemView); }); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java b/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java deleted file mode 100644 index 6ab43065..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java +++ /dev/null @@ -1,66 +0,0 @@ -package de.davis.passwordmanager.ui.viewmodels; - -import static androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle; -import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MediatorLiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.SavedStateHandle; -import androidx.lifecycle.Transformations; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.viewmodel.ViewModelInitializer; - -import java.util.List; - -import de.davis.passwordmanager.PasswordManagerApplication; -import de.davis.passwordmanager.database.dtos.SecureElement; -import de.davis.passwordmanager.filter.Filter; -import de.davis.passwordmanager.ui.viewmodels.repositories.DashboardRepo; - -public class DashboardViewModel extends ViewModel { - - private static final String QUERY = "query"; - - private final MediatorLiveData> searchResults = new MediatorLiveData<>(); - private final SavedStateHandle savedStateHandle; - - private final MutableLiveData> elements; - - - public DashboardViewModel(DashboardRepo dashboardRepo, SavedStateHandle savedStateHandle) { - this.savedStateHandle = savedStateHandle; - - searchResults.addSource(savedStateHandle.getLiveData(QUERY, ""), query -> { - searchResults.postValue(dashboardRepo.search(query)); - }); - - elements = (MutableLiveData>) Transformations.map(dashboardRepo.getElements(), Filter.DEFAULT::filter); - Filter.DEFAULT.setUpdater(() -> elements.setValue(Filter.DEFAULT.filter(dashboardRepo.getElements().getValue()))); - } - - public LiveData> getElements() { - return elements; - } - - public void search(String query){ - savedStateHandle.set(QUERY, query); - } - - public LiveData> getSearchResults(){ - return searchResults; - } - - public String getSearchQuery(){ - return savedStateHandle.get(QUERY); - } - - public static final ViewModelInitializer initializer = new ViewModelInitializer<>(DashboardViewModel.class, creationExtras -> - { - PasswordManagerApplication app = (PasswordManagerApplication) creationExtras.get(APPLICATION_KEY); - if(app == null) - throw new RuntimeException("app is null"); - - return new DashboardViewModel(DashboardRepo.getInstance(), createSavedStateHandle(creationExtras)); - }); -} diff --git a/app/src/main/res/layout/item_element.xml b/app/src/main/res/layout/item_element.xml deleted file mode 100644 index 43ed050c..00000000 --- a/app/src/main/res/layout/item_element.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_header.xml b/app/src/main/res/layout/item_header.xml deleted file mode 100644 index b479dcf4..00000000 --- a/app/src/main/res/layout/item_header.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_element.xml b/app/src/main/res/layout/layout_element.xml new file mode 100644 index 00000000..1688c2ea --- /dev/null +++ b/app/src/main/res/layout/layout_element.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_element_letter_header.xml b/app/src/main/res/layout/layout_element_letter_header.xml new file mode 100644 index 00000000..736dfedf --- /dev/null +++ b/app/src/main/res/layout/layout_element_letter_header.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_tag_item.xml b/app/src/main/res/layout/layout_tag_item.xml new file mode 100644 index 00000000..ae22562a --- /dev/null +++ b/app/src/main/res/layout/layout_tag_item.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_pane.xml b/app/src/main/res/layout/list_pane.xml index e672397d..58b30d9c 100644 --- a/app/src/main/res/layout/list_pane.xml +++ b/app/src/main/res/layout/list_pane.xml @@ -10,21 +10,21 @@ android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" - app:layoutManager="de.davis.passwordmanager.ui.LinearLayoutManager" - app:layout_behavior="de.davis.passwordmanager.ui.dashboard.DashboardFragment$ScrollingViewBehavior"/> + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> + android:fitsSystemWindows="true" + app:liftOnScroll="false" + app:liftOnScrollColor="?colorSurface"> + + android:hint="@android:string/search_go" /> + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + android:layout_gravity="center" /> @@ -79,6 +79,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:indeterminate="true"/> + android:indeterminate="true" /> \ No newline at end of file From 9bbcdea0dd037f44e61068f55b8688b3547a42ae Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 24 Dec 2023 14:13:42 +0100 Subject: [PATCH 28/46] [EditDialogBuilder] Enhance setPositiveButton with Text Input --- .../dialog/EditDialogBuilder.java | 23 ++++++++++++++++++- .../ui/views/InformationView.java | 13 ++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java b/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java index 80224c39..9a08d27b 100644 --- a/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java +++ b/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java @@ -4,6 +4,7 @@ import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE; import android.content.Context; +import android.content.DialogInterface; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.InputFilter; @@ -12,6 +13,7 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import de.davis.passwordmanager.databinding.DialogEditViewBinding; import de.davis.passwordmanager.ui.views.InformationView; @@ -25,6 +27,8 @@ public class EditDialogBuilder extends BaseDialogBuilder { @LayoutRes private int additionalCustomLayout; + private DialogEditViewBinding binding; + public EditDialogBuilder(@NonNull Context context) { super(context); } @@ -35,7 +39,7 @@ public EditDialogBuilder(@NonNull Context context, int overrideThemeResId) { @Override public View onCreateView(LayoutInflater inflater) { - DialogEditViewBinding binding = DialogEditViewBinding.inflate(inflater); + binding = DialogEditViewBinding.inflate(inflater); binding.textInputLayout.setEndIconMode(information.isSecret() ? END_ICON_PASSWORD_TOGGLE : END_ICON_NONE); binding.textInputEditText.setInputType(information.getInputType()); @@ -73,4 +77,21 @@ public EditDialogBuilder withAdditionalCustomLayout(@LayoutRes int additionalCus this.additionalCustomLayout = additionalCustomLayout; return this; } + + @NonNull + public EditDialogBuilder setPositiveButton(int textId, @Nullable OnClickListener listener) { + return super.setPositiveButton(textId, listener == null ? null : (dialog, which) -> + listener.onClick(dialog, which, binding.textInputEditText.getText().toString())); + } + + @NonNull + public EditDialogBuilder setPositiveButton(@Nullable CharSequence text, @Nullable OnClickListener listener) { + return super.setPositiveButton(text, listener == null ? null : (dialog, which) -> + listener.onClick(dialog, which, binding.textInputEditText.getText().toString())); + } + + public interface OnClickListener { + + void onClick(DialogInterface dialog, int which, String newText); + } } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/InformationView.java b/app/src/main/java/de/davis/passwordmanager/ui/views/InformationView.java index 53b3b050..73991f0e 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/InformationView.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/InformationView.java @@ -13,7 +13,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; @@ -21,7 +20,6 @@ import androidx.annotation.LayoutRes; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.card.MaterialCardView; @@ -378,15 +376,14 @@ private void showDialog(){ new EditDialogBuilder(getContext()) .setTitle(getContext().getString(R.string.change_param, titleView.getText().toString())) - .setPositiveButton(R.string.text_continue, (dialogInterface, i) -> { - dialogInterface.dismiss(); - String information = ((EditText)((AlertDialog)dialogInterface).findViewById(R.id.textInputEditText)).getText().toString(); - if(!applyEmpties && information.trim().isEmpty()) + .setPositiveButton(R.string.text_continue, (dialog, i, newText) -> { + dialog.dismiss(); + if(!applyEmpties && newText.trim().isEmpty()) return; - setInformationText(information); + setInformationText(newText); if(onInformationChangedListener != null) - onInformationChangedListener.onInformationChanged(information); + onInformationChangedListener.onInformationChanged(newText); }) .withInformation(information) .withStartIcon(iconView.getDrawable()) From 7c5db648cb26fd581fc9b2ca93d5f50ee8a6b03d Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 24 Dec 2023 15:19:43 +0100 Subject: [PATCH 29/46] [EditDialogBuilder] Added Custom Listener Support Without Auto-Dismiss Implemented enhancements in EditDialogBuilder to allow setting custom button listeners that do not automatically dismiss the dialog upon execution. This modification provides more control over the dialog's behavior during user interactions. --- .../backup/SecureDataBackup.kt | 45 ++++++++++--------- .../dialog/EditDialogBuilder.java | 30 +++++++++++-- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt index 39e0c348..66138c84 100644 --- a/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt +++ b/app/src/main/java/de/davis/passwordmanager/backup/SecureDataBackup.kt @@ -4,8 +4,7 @@ import android.content.Context import android.content.DialogInterface import android.net.Uri import android.text.InputType -import android.view.View -import android.widget.EditText +import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import com.google.android.material.textfield.TextInputLayout import de.davis.passwordmanager.R @@ -31,10 +30,31 @@ abstract class SecureDataBackup(context: Context) : DataBackup(context) { inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD isSecret = true } - val alertDialog = withContext(Dispatchers.Main) { + withContext(Dispatchers.Main) { EditDialogBuilder(context).apply { setTitle(R.string.password) setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> } + setButtonListener( + DialogInterface.BUTTON_POSITIVE, + R.string.yes + ) { dialog, _, password -> + /* + Needed for the error message that appears when the password (field) is empty. + otherwise the dialog would close itself + */ + + val alertDialog = dialog as AlertDialog + if (password.isEmpty()) { + alertDialog.findViewById(R.id.textInputLayout)?.error = + context.getString(R.string.is_not_filled_in) + return@setButtonListener + } + alertDialog.dismiss() + this@SecureDataBackup.password = password + CoroutineScope(Job() + Dispatchers.IO).launch { + super.execute(type, uri, onSyncedHandler) + } + } withInformation(information) withStartIcon( AppCompatResources.getDrawable( @@ -45,25 +65,6 @@ abstract class SecureDataBackup(context: Context) : DataBackup(context) { setCancelable(type == TYPE_IMPORT) }.show() } - - /* - Needed for the error message that appears when the password (field) is empty. - otherwise the dialogue would close itself - */ - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { _: View? -> - val password = - alertDialog.findViewById(R.id.textInputEditText)?.text.toString() - if (password.isEmpty()) { - alertDialog.findViewById(R.id.textInputLayout)?.error = - context.getString(R.string.is_not_filled_in) - return@setOnClickListener - } - alertDialog.dismiss() - this.password = password - CoroutineScope(Job() + Dispatchers.IO).launch { - super.execute(type, uri, onSyncedHandler) - } - } } override suspend fun execute(@Type type: Int, uri: Uri, onSyncedHandler: OnSyncedHandler?) { diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java b/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java index 9a08d27b..78988bdd 100644 --- a/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java +++ b/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java @@ -14,6 +14,7 @@ import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import de.davis.passwordmanager.databinding.DialogEditViewBinding; import de.davis.passwordmanager.ui.views.InformationView; @@ -28,6 +29,7 @@ public class EditDialogBuilder extends BaseDialogBuilder { private int additionalCustomLayout; private DialogEditViewBinding binding; + private final OnClickListener[] listeners = new OnClickListener[3]; public EditDialogBuilder(@NonNull Context context) { super(context); @@ -81,13 +83,33 @@ public EditDialogBuilder withAdditionalCustomLayout(@LayoutRes int additionalCus @NonNull public EditDialogBuilder setPositiveButton(int textId, @Nullable OnClickListener listener) { return super.setPositiveButton(textId, listener == null ? null : (dialog, which) -> - listener.onClick(dialog, which, binding.textInputEditText.getText().toString())); + listener.onClick(dialog, which, getText())); } @NonNull - public EditDialogBuilder setPositiveButton(@Nullable CharSequence text, @Nullable OnClickListener listener) { - return super.setPositiveButton(text, listener == null ? null : (dialog, which) -> - listener.onClick(dialog, which, binding.textInputEditText.getText().toString())); + public EditDialogBuilder setButtonListener(int whichButton, int textId, @Nullable OnClickListener listener) { + listeners[-whichButton - 1] = listener; + return super.setPositiveButton(textId, listener == null ? null : (dialog, which) -> {}); + } + + @Override + public AlertDialog show() { + AlertDialog alertDialog = super.show(); + + for(int i = 0; i < listeners.length; i++){ + OnClickListener listener = listeners[i]; + if(listener == null) + continue; + + int finalI = i; + alertDialog.getButton(-(i + 1)).setOnClickListener(v -> listener.onClick(alertDialog, finalI, getText())); + } + + return alertDialog; + } + + private String getText() { + return binding.textInputEditText.getText().toString(); } public interface OnClickListener { From 262ea395e3d0d992f98f02edeea079f686bbbab6 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 24 Dec 2023 15:23:02 +0100 Subject: [PATCH 30/46] [OptionBottomSheet] Added support for Tag-View --- .../viewholders/SecureElementViewHolder.java | 2 +- .../database/SecureElementManager.kt | 37 ++++- .../database/daos/SecureElementWithTagDao.kt | 7 +- .../passwordmanager/dialog/DeleteDialog.java | 6 +- .../DefaultElementMenuProvider.kt | 3 +- .../ui/views/OptionBottomSheet.java | 76 ---------- .../ui/views/OptionBottomSheet.kt | 139 ++++++++++++++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 185 insertions(+), 89 deletions(-) delete mode 100644 app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java create mode 100644 app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java index 93528cf1..ba37f761 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java @@ -87,7 +87,7 @@ public void bindGeneral(@NonNull SecureElement item, String filter, OnItemClicke onItemClickedListener.onClicked(item); }); - more.setOnClickListener(v -> new OptionBottomSheet(List.of(item)).show(fragmentManager, "OptionSheet")); + more.setOnClickListener(v -> new OptionBottomSheet<>(List.of(item), SecureElement.class).show(fragmentManager, "OptionSheet")); } public void setLetterVisible(boolean visible, char letter){ diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt index ca105ca6..b518ec21 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -2,6 +2,7 @@ package de.davis.passwordmanager.database import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData +import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.daos.SecureElementWithTagDao import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.dtos.TagWithCount @@ -85,6 +86,10 @@ object SecureElementManager { } } + suspend fun updateTag(tag: Tag): Int { + return dao.updateTag(tag) + } + @JvmStatic suspend fun updateModifiedAt(secureElement: SecureElement) { dao.updateModifiedAt(secureElement.toEntity()) @@ -112,15 +117,37 @@ object SecureElementManager { } @JvmStatic - suspend fun deleteElement(secureElement: SecureElement) { - dao.delete(secureElement.toEntity().secureElementEntity) + suspend fun deleteElements(secureElements: List) { + dao.deleteElements(secureElements.map { it.toEntity().secureElementEntity }) + } + + @JvmStatic + suspend fun deleteTags(tags: List) { + dao.deleteTags(tags) + } + + /** + * Deletes a list of items from the database. + * + * This function can handle different types of items that extend from the base 'Item' class. + * It filters and processes each item based on its actual type and performs the appropriate + * deletion operation. + * + * @param items A list of items to be deleted. These can be of any type that extends from 'Item'. + * @param The type parameter indicating the subtype of 'Item' being deleted. + */ + @JvmStatic + suspend fun delete(items: List) { + deleteElements(items.filterIsInstance()) + deleteTags(items.filterIsInstance().map { it.tag }) } @JvmStatic - @JvmName("deleteElement") - fun deleteElementCoroutine(secureElement: SecureElement) { + @JvmName("delete") + fun deleteCoroutine(items: List) { scope.launch { - deleteElement(secureElement) + deleteElements(items.filterIsInstance()) + deleteTags(items.filterIsInstance().map { it.tag }) } } diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index b01f112e..bfdeaafb 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -52,10 +52,10 @@ abstract class SecureElementWithTagDao { protected abstract suspend fun insert(crossRef: SecureElementTagCrossRef) @Delete - abstract suspend fun delete(secureElementEntity: SecureElementEntity) + abstract suspend fun deleteElements(secureElementEntities: List) @Delete - abstract suspend fun delete(tag: Tag) + abstract suspend fun deleteTags(tags: List) @Delete abstract suspend fun delete(crossRef: SecureElementTagCrossRef) @@ -91,6 +91,9 @@ abstract class SecureElementWithTagDao { } } + @Update(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun updateTag(tag: Tag): Int + suspend fun update(elementWithTags: CombinedElement) { elementWithTags.secureElementEntity.run { timestamps.modifiedAt = Date() diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java index 8a1f2324..9b6e81cb 100644 --- a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java +++ b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java @@ -8,8 +8,8 @@ import java.util.List; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.dashboard.Item; import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dtos.SecureElement; public class DeleteDialog { @@ -21,10 +21,10 @@ public DeleteDialog(Context context) { .setMessage(R.string.sure_delete); } - public void show(List toDelete){ // if toDelete is null -> delete selected + public void show(List toDelete){ // if toDelete is null -> delete selected builder.setNegativeButton(R.string.no, (dialog, which) -> {}) .setPositiveButton(R.string.yes, (dialog, which) -> { - toDelete.forEach(SecureElementManager::deleteElement); + SecureElementManager.delete(toDelete); Toast.makeText(builder.getContext(), R.string.successful_deleted, Toast.LENGTH_LONG).show(); }).show(); diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt index 4bedff6e..3ed6084a 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt @@ -8,7 +8,6 @@ import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentManager import de.davis.passwordmanager.R import de.davis.passwordmanager.dashboard.Item -import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.ui.dashboard.DashboardAdapter import de.davis.passwordmanager.ui.views.FilterBottomSheet import de.davis.passwordmanager.ui.views.OptionBottomSheet @@ -35,7 +34,7 @@ class DefaultElementMenuProvider( override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.more) { - val optionBottomSheet = OptionBottomSheet(selectedElements.map { it as SecureElement }) + val optionBottomSheet = OptionBottomSheet(selectedElements) optionBottomSheet.show(manager, "MoreDialog") adapter.clearSelection() } else if (menuItem.itemId == R.id.filter) { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java deleted file mode 100644 index 1c99278c..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.davis.passwordmanager.ui.views; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; - -import java.util.List; - -import de.davis.passwordmanager.R; -import de.davis.passwordmanager.database.SecureElementManager; -import de.davis.passwordmanager.database.dtos.SecureElement; -import de.davis.passwordmanager.databinding.MoreBottomSheetContentBinding; -import de.davis.passwordmanager.dialog.DeleteDialog; -import de.davis.passwordmanager.manager.ActivityResultManager; -import de.davis.passwordmanager.ui.dashboard.DashboardFragment; - -public class OptionBottomSheet extends BottomSheetDialogFragment { - - private MoreBottomSheetContentBinding binding; - private final List elements; - - public OptionBottomSheet(List elements) { - this.elements = elements; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return (binding = MoreBottomSheetContentBinding.inflate(getLayoutInflater())).getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if(elements.isEmpty()) - return; - - SecureElement firstElement = elements.get(0); - - binding.title.setText(elements.size() > 1 ? requireContext().getString(R.string.options) : firstElement.getTitle()); - - if(elements.size() > 1) { - binding.edit.setVisibility(View.GONE); - binding.favorite.setVisibility(View.GONE); - }else { - binding.edit.setOnClickListener(v -> { - ActivityResultManager.getOrCreateManager(DashboardFragment.class, null).launchEdit(firstElement, getContext()); - dismiss(); - }); - - binding.favorite.setOnClickListener(v -> { - SecureElementManager.switchFavState(firstElement); - dismiss(); - }); - - binding.favorite.setCompoundDrawablesRelativeWithIntrinsicBounds( - firstElement.getFavorite() ? - R.drawable.baseline_star_24 - : R.drawable.baseline_star_outline_24, - 0, 0, 0); - - binding.favorite.setText(firstElement.getFavorite() ? R.string.remove_from_favorite : R.string.mark_as_favorite); - } - - binding.delete.setOnClickListener(v -> { - new DeleteDialog(getContext()).show(elements); - dismiss(); - }); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt new file mode 100644 index 00000000..22d8e21d --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt @@ -0,0 +1,139 @@ +package de.davis.passwordmanager.ui.views + +import android.content.DialogInterface +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.textfield.TextInputLayout +import de.davis.passwordmanager.R +import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.SecureElementManager.switchFavStateCoroutine +import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.dtos.TagWithCount +import de.davis.passwordmanager.database.entities.getLocalizedName +import de.davis.passwordmanager.databinding.MoreBottomSheetContentBinding +import de.davis.passwordmanager.dialog.DeleteDialog +import de.davis.passwordmanager.dialog.EditDialogBuilder +import de.davis.passwordmanager.manager.ActivityResultManager +import de.davis.passwordmanager.ui.dashboard.DashboardFragment +import kotlinx.coroutines.launch + +@Suppress("FunctionName") +inline fun OptionBottomSheet(items: List): BottomSheetDialogFragment = + OptionBottomSheet(items, items[0]::class.java) + + +class OptionBottomSheet( + private val items: List, + private val iClass: Class +) : + BottomSheetDialogFragment() { + + private lateinit var binding: MoreBottomSheetContentBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return MoreBottomSheetContentBinding.inflate(layoutInflater).also { binding = it }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (items.isEmpty()) return + + tryViewCreatedForElement() + tryViewCreatedForTag() + + binding.delete.setOnClickListener { + DeleteDialog(context).show(items) + dismiss() + } + } + + @Suppress("UNCHECKED_CAST") + private inline fun tryCast(): List? { + return if (iClass == E::class.java) (items as List) else null + } + + private fun tryViewCreatedForElement() = tryCast()?.let { list -> + val firstElement: SecureElement = list[0] + setTitle(list) { it.title } + + if (items.size > 1) { + binding.edit.visibility = View.GONE + binding.favorite.visibility = View.GONE + } else { + binding.edit.setOnClickListener { + ActivityResultManager.getOrCreateManager(DashboardFragment::class.java, null) + .launchEdit(firstElement, context) + dismiss() + } + binding.favorite.setOnClickListener { + switchFavStateCoroutine(firstElement) + dismiss() + } + binding.favorite.setCompoundDrawablesRelativeWithIntrinsicBounds( + if (firstElement.favorite) R.drawable.baseline_star_24 else R.drawable.baseline_star_outline_24, + 0, 0, 0 + ) + binding.favorite.setText(if (firstElement.favorite) R.string.remove_from_favorite else R.string.mark_as_favorite) + } + } + + private fun tryViewCreatedForTag() = tryCast()?.let { list -> + val title = setTitle(list) { it.tag.getLocalizedName(requireContext()) } + binding.favorite.visibility = View.GONE + + if (items.size > 1) { + binding.edit.visibility = View.GONE + } else { + val firstTag = list.first() + binding.edit.setOnClickListener { + dismiss() + EditDialogBuilder(requireContext()).apply { + setTitle(R.string.edit_tag) + setButtonListener( + DialogInterface.BUTTON_POSITIVE, + R.string.ok + ) { dialog, _, newText -> + + lifecycleScope.launch { + val updatedRows = + SecureElementManager.updateTag(firstTag.tag.copy(name = newText)) + if (updatedRows != 0) { + dialog.dismiss() + return@launch + } + + (dialog as AlertDialog).findViewById(R.id.textInputLayout)?.error = + context.getString(R.string.tag_already_existed) + } + } + + withInformation(InformationView.Information().apply { + text = title + hint = getString(R.string.title) + inputType = InputType.TYPE_CLASS_TEXT + isSecret = false + }) + }.show() + } + } + } + + private fun setTitle(list: List, titleProvider: (I) -> String): String { + val title = if (list.size > 1) requireContext().getString(R.string.options) + else titleProvider(list[0]) + binding.title.text = title + + return title + } +} \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 23437434..be3eb4a9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -153,7 +153,9 @@ Authentifizieren um fortzufahren Tags + Tag bearbeiten Tags hier hinzufügen + Dieser Tag existiert bereits Dieser Prefix ist reserviert und kann nicht verwendet werden Alle Elemente: %d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c64b0715..bfd913ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,6 +196,8 @@ Tags Add tags here + Edit tag + This tag already exists This prefix is reserved and can\'t be used All Items: %d From 01fc97e013572839e886deac1d4501f4372db931 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 24 Dec 2023 15:36:58 +0100 Subject: [PATCH 31/46] [DashboardAdapter] Enhanced SelectionPredicate for Restricted Tag Handling Refined withSelectionPredicate in SelectionTracker to disallow operations on default tags like 'Password'. This change ensures critical tags remain protected from actions like deletion or renaming, enhancing data integrity and user experience. --- .../ui/dashboard/DashboardAdapter.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt index 831a6949..648d63c7 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt @@ -2,8 +2,8 @@ package de.davis.passwordmanager.ui.dashboard import android.os.Bundle import android.view.ViewGroup -import androidx.recyclerview.selection.SelectionPredicates import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -12,6 +12,8 @@ import de.davis.passwordmanager.dashboard.SecureElementDiffCallback import de.davis.passwordmanager.dashboard.selection.KeyProvider import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.database.dtos.TagWithCount +import de.davis.passwordmanager.database.entities.shouldBeProtected import de.davis.passwordmanager.ui.dashboard.managers.AbsItemManager class DashboardAdapter(private val onUpdate: (DashboardAdapter) -> Unit) : @@ -61,7 +63,24 @@ class DashboardAdapter(private val onUpdate: (DashboardAdapter) -> Unit) : KeyProvider(this), SecureElementDetailsLookup(this), StorageStrategy.createLongStorage() - ).withSelectionPredicate(SelectionPredicates.createSelectAnything()).build() + ).withSelectionPredicate(object : SelectionPredicate() { + override fun canSetStateForKey(key: Long, nextState: Boolean): Boolean { + val item = itemManager.getElementById(key) + return !(item is TagWithCount && item.tag.shouldBeProtected) + } + + override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean { + if (position !in itemManager.items.indices) return false + + val item = itemManager.items[position] + return !(item is TagWithCount && item.tag.shouldBeProtected) + } + + override fun canSelectMultiple(): Boolean { + return true + } + + }).build() tracker.addObserver(object : SelectionTracker.SelectionObserver() { var oldSelection: List? = null @@ -106,7 +125,6 @@ class DashboardAdapter(private val onUpdate: (DashboardAdapter) -> Unit) : if (old.toTypedArray().contentEquals(itemManager.items.toTypedArray())) return - onUpdate(this) val callback = SecureElementDiffCallback(old, itemManager.items) From 756f6061848f1e53f47e9d736cb15de77929fbb9 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 24 Dec 2023 16:06:07 +0100 Subject: [PATCH 32/46] [OptionBottomSheet] Enhanced Tag Validation Refactored the setButtonListener logic in EditDialogBuilder to include additional validations for tag editing. Now, the updated code checks for blank inputs and unchanged tag names. This enhancement streamlines user input handling and prevents unnecessary database operations, contributing to a more robust and user-friendly tag management experience. --- .../de/davis/passwordmanager/ktx/String.kt | 5 +++++ .../ui/views/OptionBottomSheet.kt | 22 ++++++++++++++++--- .../davis/passwordmanager/ui/views/TagView.kt | 5 +---- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/de/davis/passwordmanager/ktx/String.kt diff --git a/app/src/main/java/de/davis/passwordmanager/ktx/String.kt b/app/src/main/java/de/davis/passwordmanager/ktx/String.kt new file mode 100644 index 00000000..d718a160 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ktx/String.kt @@ -0,0 +1,5 @@ +package de.davis.passwordmanager.ktx + +fun String.capitalize(): String { + return replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt index 22d8e21d..a46d673c 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt @@ -20,6 +20,7 @@ import de.davis.passwordmanager.database.entities.getLocalizedName import de.davis.passwordmanager.databinding.MoreBottomSheetContentBinding import de.davis.passwordmanager.dialog.DeleteDialog import de.davis.passwordmanager.dialog.EditDialogBuilder +import de.davis.passwordmanager.ktx.capitalize import de.davis.passwordmanager.manager.ActivityResultManager import de.davis.passwordmanager.ui.dashboard.DashboardFragment import kotlinx.coroutines.launch @@ -104,17 +105,32 @@ class OptionBottomSheet( DialogInterface.BUTTON_POSITIVE, R.string.ok ) { dialog, _, newText -> + val inputLayout = + (dialog as AlertDialog).findViewById(R.id.textInputLayout)!! + if (newText.isBlank()) { + inputLayout.error = context.getString(R.string.tag_cant_be_blank) + inputLayout.editText?.text?.clear() + return@setButtonListener + } + + if (newText == firstTag.tag.name) { + dialog.dismiss() + return@setButtonListener + } lifecycleScope.launch { val updatedRows = - SecureElementManager.updateTag(firstTag.tag.copy(name = newText)) + SecureElementManager.updateTag( + firstTag.tag.copy( + name = newText.trim().capitalize() + ) + ) if (updatedRows != 0) { dialog.dismiss() return@launch } - (dialog as AlertDialog).findViewById(R.id.textInputLayout)?.error = - context.getString(R.string.tag_already_existed) + inputLayout.error = context.getString(R.string.tag_already_existed) } } diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt index f2dc191b..3fef3746 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt @@ -22,6 +22,7 @@ import de.davis.passwordmanager.database.entities.Tag import de.davis.passwordmanager.database.entities.onlyCustoms import de.davis.passwordmanager.database.entities.shouldBeProtected import de.davis.passwordmanager.databinding.LayoutTagViewBinding +import de.davis.passwordmanager.ktx.capitalize import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -236,9 +237,5 @@ class TagView @JvmOverloads constructor( } } - private fun String.capitalize(): String { - return replaceFirstChar { it.uppercase() } - } - private val CharSequence.isProtectedTagName get() = startsWith(TAG_PREFIX) } \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index be3eb4a9..e407cc5d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -156,6 +156,7 @@ Tag bearbeiten Tags hier hinzufügen Dieser Tag existiert bereits + Tags dürfen nicht leer sein Dieser Prefix ist reserviert und kann nicht verwendet werden Alle Elemente: %d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bfd913ca..184725ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,6 +198,7 @@ Add tags here Edit tag This tag already exists + Tags can\'t be blank This prefix is reserved and can\'t be used All Items: %d From 9c3e02071475e77fb7e794ca20f024a220091632 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 24 Dec 2023 18:26:33 +0100 Subject: [PATCH 33/46] [Preferences] Implemented Dashboard View Switching Option Introduced a new preference setting enabling users to toggle between tag view and classic element list view in the dashboard. This enhancement offers a customizable user experience, catering to diverse user preferences for dashboard display. --- .../ui/dashboard/DashboardFragment.kt | 12 ++++++++++ .../ui/dashboard/DashboardViewModel.kt | 22 ++++++++++++++----- app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values/preferences.xml | 1 + app/src/main/res/values/strings.xml | 4 ++++ app/src/main/res/xml/root_preferences.xml | 8 +++++++ 6 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt index e7499dd1..cd484416 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt @@ -61,6 +61,8 @@ class DashboardFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + dashboardViewModel.initiateState() + binding.root.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED val menuProvider = DefaultElementMenuProvider(parentFragmentManager, adapter) @@ -230,6 +232,16 @@ class DashboardFragment : Fragment() { binding.root.open() } + override fun onPause() { + super.onPause() + scrollingViewModel.setVisibility(false) + } + + override fun onResume() { + super.onResume() + scrollingViewModel.setVisibility(true) + } + private fun update(update: AbsItemManager) { adapter.update(update) diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt index 78467249..4ea72ebf 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt @@ -1,7 +1,10 @@ package de.davis.passwordmanager.ui.dashboard -import androidx.lifecycle.ViewModel +import android.app.Application +import android.content.Context +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import de.davis.passwordmanager.R import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.SecureElementManager import de.davis.passwordmanager.database.dtos.SecureElement @@ -9,6 +12,7 @@ import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.entities.onlyCustoms import de.davis.passwordmanager.filter.Filter import de.davis.passwordmanager.filter.applyFilter +import de.davis.passwordmanager.utils.PreferenceUtil import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -17,15 +21,19 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.launch +private fun Context.getDefaultListState(): ListState { + return if (PreferenceUtil.getBoolean(this, R.string.preference_feature_tag_layout, false)) + ListState.Tag + else + ListState.AllElements +} -val DEFAULT_LIST_SATE = ListState.Tag - -class DashboardViewModel : ViewModel() { +class DashboardViewModel(private val application: Application) : AndroidViewModel(application) { private val _state = MutableStateFlow>?>(null) val state = _state.asStateFlow() - private val _listState = MutableStateFlow(DEFAULT_LIST_SATE) + private val _listState = MutableStateFlow(application.getDefaultListState()) private val elementsFlow = SecureElementManager.getSecureElementsFlow() private val tagsFlow = SecureElementManager.getTagsWithCount() @@ -41,6 +49,10 @@ class DashboardViewModel : ViewModel() { _listState.value = state } + fun initiateState() { + updateState(application.getDefaultListState()) + } + fun search(query: String) { searchJob?.cancel() diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e407cc5d..507538d6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -153,6 +153,10 @@ Authentifizieren um fortzufahren Tags + Tag-Layout + Dashboard gruppiert nach Tags + Dashboard im Listenformat + Tag bearbeiten Tags hier hinzufügen Dieser Tag existiert bereits diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index 5320696e..36dd3811 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -2,6 +2,7 @@ authenticate_fingerprint feature_autofill + feature_tag_layout feature_reauthenticate license import_csv diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 184725ec..6f292d2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,6 +195,10 @@ Authenticate to proceed Tags + Tag Layout + Dashboard grouped by tags + Dashboard in list format + Add tags here Edit tag This tag already exists diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index f50591ad..50c50828 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -37,6 +37,14 @@ app:icon="@drawable/ic_baseline_edit_24" android:widgetLayout="@layout/switch_layout" android:summary="@string/instruction_activate_autofill" /> + + From d24440b3ce7d0fe4f9e2f4b95badc0e04463af6b Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:35:11 +0100 Subject: [PATCH 34/46] [Kotlin 9] Use entries --- .../main/java/de/davis/passwordmanager/database/ElementType.kt | 2 +- .../de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt index 8cbe993e..c0d9cf74 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt @@ -47,7 +47,7 @@ enum class ElementType( companion object { @JvmStatic fun getTypeByTypeId(id: Int): ElementType { - return values().first { it.typeId == id } + return entries.first { it.typeId == id } } } } \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java index 681f68ed..9ef0393f 100644 --- a/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java +++ b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java @@ -21,7 +21,7 @@ public ElementDetail deserialize(JsonElement json, Type typeOfT, JsonDeserializa JsonElement strengthElement = json.getAsJsonObject().get("strength"); if(strengthElement != null && strengthElement.isJsonObject()){ - json.getAsJsonObject().addProperty("strength", Strength.values()[strengthElement.getAsJsonObject().get("type").getAsInt()].name()); + json.getAsJsonObject().addProperty("strength", Strength.getEntries().get(strengthElement.getAsJsonObject().get("type").getAsInt()).name()); } return context.deserialize(json, ElementType.getTypeByTypeId(type).getElementDetailClass()); From 686858efb8f93707df96bb1efb4f60662e01649d Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:37:08 +0100 Subject: [PATCH 35/46] [ProGuard] Added Rule for ElementDetail Interface Retention Included a new ProGuard rule to preserve classes implementing the ElementDetail interface. This ensures crucial functionalities related to database entities remain intact during the code obfuscation process. --- app/proguard-rules.pro | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6b07e08b..6f7a9b88 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -33,7 +33,9 @@ -keep class com.google.gson.stream.** { *; } # Application classes that will be serialized/deserialized over Gson --keep class de.davis.passwordmanager.security.element.** { ; } +-keep class * implements de.davis.passwordmanager.database.entities.details.ElementDetail{ + *; +} -keep class de.davis.passwordmanager.database.dtos.** # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, From 8a7b8421856453da905c6fe5c2c0ea16faa6b3df Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:40:46 +0100 Subject: [PATCH 36/46] [Database] Enabled Kotlin Generation for Room Updated the build configuration with a ksp block to enable Kotlin source generation for Room database operations. This addition optimizes the integration of Room with Kotlin, ensuring more efficient and type-safe database interactions. --- app/build.gradle.kts | 4 ++++ .../3.json | 8 ++++---- .../passwordmanager/database/converter/Converters.java | 4 ++++ .../davis/passwordmanager/database/entities/Timestamps.kt | 7 ++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f40c4c43..f428c461 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,4 +136,8 @@ dependencies { room { schemaDirectory("$projectDir/schemas") +} + +ksp { + arg("room.generateKotlin", "true") } \ No newline at end of file diff --git a/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json b/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json index df5935e0..30ba2c0c 100644 --- a/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json +++ b/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 3, - "identityHash": "ceb1caca6f62fb72bb85d7bfaaac98fd", + "identityHash": "0a97d13a94575bc2ed2ab009853b0086", "entities": [ { "tableName": "SecureElement", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `data` BLOB NOT NULL, `favorite` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `created_at` INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, `modified_at` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `data` BLOB NOT NULL, `favorite` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `created_at` INTEGER DEFAULT CURRENT_TIMESTAMP, `modified_at` INTEGER)", "fields": [ { "fieldPath": "title", @@ -42,7 +42,7 @@ "fieldPath": "timestamps.createdAt", "columnName": "created_at", "affinity": "INTEGER", - "notNull": true, + "notNull": false, "defaultValue": "CURRENT_TIMESTAMP" }, { @@ -170,7 +170,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ceb1caca6f62fb72bb85d7bfaaac98fd')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0a97d13a94575bc2ed2ab009853b0086')" ] } } \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java b/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java index 70a9972d..44b4355d 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java +++ b/app/src/main/java/de/davis/passwordmanager/database/converter/Converters.java @@ -1,5 +1,6 @@ package de.davis.passwordmanager.database.converter; +import androidx.annotation.Nullable; import androidx.room.TypeConverter; import com.google.gson.Gson; @@ -16,6 +17,7 @@ public class Converters { private static final Gson gson = new GsonBuilder().registerTypeAdapter(ElementDetail.class, new ElementDetailTypeAdapter()).create(); @TypeConverter + @Nullable public static byte[] convertDetails(ElementDetail elementDetail){ String json = gson.toJson(elementDetail, ElementDetail.class); return Cryptography.encryptAES(json.getBytes()); @@ -28,11 +30,13 @@ public static ElementDetail convertByteArray(byte[] data){ } @TypeConverter + @Nullable public static Date fromTimestamp(Long value) { return value == null ? null : new Date(value); } @TypeConverter + @Nullable public static Long dateToTimestamp(Date date) { return date == null ? null : date.getTime(); } diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt index 54add62a..bf959d0c 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt @@ -4,10 +4,15 @@ import androidx.room.ColumnInfo import java.util.Date data class Timestamps( - @ColumnInfo(name = "created_at", defaultValue = "CURRENT_TIMESTAMP") var createdAt: Date, + @ColumnInfo(name = "created_at", defaultValue = "CURRENT_TIMESTAMP") var createdAt: Date?, @ColumnInfo(name = "modified_at") var modifiedAt: Date? = null ) { + init { + if (createdAt == null) + createdAt = Date() + } + companion object { val CURRENT get() = Timestamps(Date()) } From 582163138117b9c78a71574cfe1441c9035eac31 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:46:12 +0100 Subject: [PATCH 37/46] [Database] Enhanced DAO with @Transaction for Efficient Operations Updated the DAO function with the @Transaction annotation to optimize database operations. This change not only improves efficiency but also resolves an issue related to view reloading, enhancing overall application performance and reliability. --- .../database/daos/SecureElementWithTagDao.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index bfdeaafb..02f54240 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -72,7 +72,8 @@ abstract class SecureElementWithTagDao { @Query("DELETE FROM Tag WHERE tagId NOT IN (SELECT tagId FROM SecureElementTagCrossRef)") protected abstract suspend fun deleteUnusedTags() - suspend fun insert(elementWithTags: CombinedElement): Long { + @Transaction + open suspend fun insert(elementWithTags: CombinedElement): Long { val elementId = insert(elementWithTags.secureElementEntity) insertTags(elementWithTags.tags, elementId) return elementId @@ -94,7 +95,8 @@ abstract class SecureElementWithTagDao { @Update(onConflict = OnConflictStrategy.IGNORE) abstract suspend fun updateTag(tag: Tag): Int - suspend fun update(elementWithTags: CombinedElement) { + @Transaction + open suspend fun update(elementWithTags: CombinedElement) { elementWithTags.secureElementEntity.run { timestamps.modifiedAt = Date() update(this) From 9b3602a84dae605e0d4745f8fa41760ad76f2e62 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:46:55 +0100 Subject: [PATCH 38/46] [Database] Improved inserting logic --- .../database/daos/SecureElementWithTagDao.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index 02f54240..ddbd6562 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -46,7 +46,7 @@ abstract class SecureElementWithTagDao { protected abstract suspend fun insert(secureElementEntity: SecureElementEntity): Long @Insert(onConflict = OnConflictStrategy.IGNORE) - protected abstract suspend fun insert(tags: List): List + protected abstract suspend fun insert(tag: Tag): Long @Insert protected abstract suspend fun insert(crossRef: SecureElementTagCrossRef) @@ -57,13 +57,10 @@ abstract class SecureElementWithTagDao { @Delete abstract suspend fun deleteTags(tags: List) - @Delete - abstract suspend fun delete(crossRef: SecureElementTagCrossRef) - @Update abstract suspend fun update(secureElementEntity: SecureElementEntity) - @Query("SELECT tagId FROM TAG WHERE name = :name") + @Query("SELECT tagId FROM TAG WHERE name = :name LIMIT 1") protected abstract suspend fun getIdByTagName(name: String): Long @Query("DELETE FROM SecureElementTagCrossRef WHERE id = :elementId") @@ -75,18 +72,22 @@ abstract class SecureElementWithTagDao { @Transaction open suspend fun insert(elementWithTags: CombinedElement): Long { val elementId = insert(elementWithTags.secureElementEntity) - insertTags(elementWithTags.tags, elementId) + insertTagsAndMakeRelation(elementWithTags.tags, elementId) return elementId } - private suspend fun insertTags(tags: List, elementId: Long) { - val tagIds = insert(tags) + private suspend fun insertTagsAndMakeRelation(tags: List, elementId: Long) { + tags.forEach { + var tagId = getIdByTagName(it.name) + + if (tagId == 0L) { + tagId = insert(it) + } - tagIds.forEachIndexed { index, l -> insert( SecureElementTagCrossRef( elementId, - if (l > 0) l else getIdByTagName(tags[index].name) + tagId ) ) } @@ -102,7 +103,7 @@ abstract class SecureElementWithTagDao { update(this) id.run { deleteTagRelationsTo(this) - insertTags(elementWithTags.tags, this) + insertTagsAndMakeRelation(elementWithTags.tags, this) deleteUnusedTags() } } From a1a874f2c9d612c9cca640db54480af97d0c98ce Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:48:14 +0100 Subject: [PATCH 39/46] [Code Visibility] Restricted Access --- .../passwordmanager/database/daos/SecureElementWithTagDao.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index ddbd6562..2cafdefb 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -32,7 +32,7 @@ abstract class SecureElementWithTagDao { abstract suspend fun getByTitle(query: String): List @Transaction - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.NONE) @Query("SELECT * FROM SecureElement WHERE id = :id") abstract suspend fun getCombinedElementById(id: Long): CombinedElement From 4c937cd074399596baefaf369d65c04b75b248e1 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:48:38 +0100 Subject: [PATCH 40/46] [KeyGoDatabase] Removed allowMainThreadQueries --- .../java/de/davis/passwordmanager/database/KeyGoDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt b/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt index 3521f9e3..9f8ed43e 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/KeyGoDatabase.kt @@ -37,7 +37,7 @@ abstract class KeyGoDatabase : RoomDatabase() { PasswordManagerApplication.getAppContext(), KeyGoDatabase::class.java, DB_NAME - ).fallbackToDestructiveMigration().allowMainThreadQueries().build() + ).fallbackToDestructiveMigration().build() .also { INSTANCE = it } } } From 5cdf602e88b832e33b786ecae7933a27a4f9a693 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 14:50:23 +0100 Subject: [PATCH 41/46] [Database Migration] Implemented Tag Management The migration process is enhanced with logic to handle tag insertion and association, ensuring data integrity and consistency post-migration. --- .../database/migration/MigrationSpec2To3.kt | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/migration/MigrationSpec2To3.kt b/app/src/main/java/de/davis/passwordmanager/database/migration/MigrationSpec2To3.kt index a1b69d56..47ff3538 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/migration/MigrationSpec2To3.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/migration/MigrationSpec2To3.kt @@ -1,7 +1,58 @@ package de.davis.passwordmanager.database.migration +import android.annotation.SuppressLint import androidx.room.DeleteTable import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase +import de.davis.passwordmanager.database.ElementType @DeleteTable(tableName = "MasterPassword") -class MigrationSpec2To3 : AutoMigrationSpec \ No newline at end of file +class MigrationSpec2To3 : AutoMigrationSpec { + + @SuppressLint("Range") + override fun onPostMigrate(db: SupportSQLiteDatabase) { + val cursor = db.query("SELECT id, type FROM SecureElement") + val tags = mutableMapOf() + + cursor.use { + while (cursor.moveToNext()) { + val elementId = cursor.getLong(cursor.getColumnIndex("id")) + val elementType = cursor.getInt(cursor.getColumnIndex("type")) + val tag = ElementType.getTypeByTypeId(elementType).tag.name + + // Check if the tag is already inserted and get its ID + val tagId = tags.getOrPut(tag) { insertTag(db, tag) } + + // Insert into SecureElementTagCrossRef + insertElementTagCrossRef(db, elementId, tagId) + } + } + } + + @SuppressLint("Range") + private fun insertTag(db: SupportSQLiteDatabase, tag: String): Long { + return db.run { + query("SELECT tagId FROM Tag WHERE name = ?", arrayOf(tag)).use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndex("tagId")) + } + } + + // Insert the tag if it doesn't exist + compileStatement("INSERT INTO Tag (name) VALUES (?)").use { stmt -> + stmt.bindString(1, tag) + stmt.executeInsert() + } + } + } + + private fun insertElementTagCrossRef(db: SupportSQLiteDatabase, elementId: Long, tagId: Long) { + db.compileStatement("INSERT INTO SecureElementTagCrossRef (id, tagId) VALUES (?, ?)") + .apply { + bindLong(1, elementId) + bindLong(2, tagId) + executeInsert() + close() + } + } +} \ No newline at end of file From c4cc1870ab37488bcc04ef98571f949da86da7be Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 25 Dec 2023 15:27:01 +0100 Subject: [PATCH 42/46] [OptionBottomSheet] Added tag prefix policy --- .../java/de/davis/passwordmanager/database/entities/Tag.kt | 4 +++- .../de/davis/passwordmanager/ui/views/OptionBottomSheet.kt | 6 ++++++ .../main/java/de/davis/passwordmanager/ui/views/TagView.kt | 4 +--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt index d87891c7..5ab272d4 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt @@ -15,10 +15,12 @@ data class Tag @JvmOverloads constructor( ) fun Collection.onlyCustoms(): Collection { - return filter { !it.name.startsWith(TAG_PREFIX) } + return filter { !it.shouldBeProtected } } val Tag.shouldBeProtected get() = this.name.startsWith(TAG_PREFIX) +val CharSequence.isProtectedTagName get() = startsWith(TAG_PREFIX) + fun Tag.getLocalizedName(context: Context) = if (shouldBeProtected) context.getString(ElementType.entries.first { e -> e.tag.name == name }.title) else name \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt index a46d673c..0054aa42 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt @@ -17,6 +17,7 @@ import de.davis.passwordmanager.database.SecureElementManager.switchFavStateCoro import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.entities.getLocalizedName +import de.davis.passwordmanager.database.entities.isProtectedTagName import de.davis.passwordmanager.databinding.MoreBottomSheetContentBinding import de.davis.passwordmanager.dialog.DeleteDialog import de.davis.passwordmanager.dialog.EditDialogBuilder @@ -113,6 +114,11 @@ class OptionBottomSheet( return@setButtonListener } + if (newText.isProtectedTagName) { + inputLayout.error = context.getString(R.string.prefix_not_allowed) + return@setButtonListener + } + if (newText == firstTag.tag.name) { dialog.dismiss() return@setButtonListener diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt index 3fef3746..d2b966ec 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt @@ -17,8 +17,8 @@ import com.google.android.material.chip.Chip import de.davis.passwordmanager.R import de.davis.passwordmanager.database.ElementType import de.davis.passwordmanager.database.SecureElementManager -import de.davis.passwordmanager.database.TAG_PREFIX import de.davis.passwordmanager.database.entities.Tag +import de.davis.passwordmanager.database.entities.isProtectedTagName import de.davis.passwordmanager.database.entities.onlyCustoms import de.davis.passwordmanager.database.entities.shouldBeProtected import de.davis.passwordmanager.databinding.LayoutTagViewBinding @@ -236,6 +236,4 @@ class TagView @JvmOverloads constructor( chip.isSelected = true } } - - private val CharSequence.isProtectedTagName get() = startsWith(TAG_PREFIX) } \ No newline at end of file From 3963865b49de2bbcf38c7f1a4b14b66689a08391 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Tue, 26 Dec 2023 01:30:36 +0100 Subject: [PATCH 43/46] [OptionBottomSheet] Added tag merge feature --- .../database/SecureElementManager.kt | 4 ++ .../database/daos/SecureElementWithTagDao.kt | 35 ++++++++++- .../ui/views/OptionBottomSheet.kt | 62 +++++++++++++++---- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 91 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt index b518ec21..eb9c8222 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -151,6 +151,10 @@ object SecureElementManager { } } + suspend fun mergeTags(tags: List, resultingTagName: String) { + dao.mergeTags(tags, resultingTagName) + } + @JvmStatic @Deprecated("Calling this blocks the Main Thread", ReplaceWith("Kotlin Coroutine")) fun getLastCreatedSync(limit: Int = 5): List { diff --git a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt index 2cafdefb..c3d17995 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -48,7 +48,7 @@ abstract class SecureElementWithTagDao { @Insert(onConflict = OnConflictStrategy.IGNORE) protected abstract suspend fun insert(tag: Tag): Long - @Insert + @Insert(onConflict = OnConflictStrategy.IGNORE) protected abstract suspend fun insert(crossRef: SecureElementTagCrossRef) @Delete @@ -116,6 +116,39 @@ abstract class SecureElementWithTagDao { } } + @Query( + """ + INSERT OR IGNORE INTO SecureElementTagCrossRef (id, tagId) + SELECT DISTINCT sec.id, (SELECT tagId FROM Tag WHERE name = :newTagName) + FROM SecureElementTagCrossRef sec + INNER JOIN Tag t ON sec.tagId = t.tagId + WHERE t.name IN (:tagsToDelete) + """ + ) + protected abstract suspend fun reassignSecureElementsToNewTag( + newTagName: String, + tagsToDelete: List + ) + + @Transaction + open suspend fun mergeTags(tags: List, resultingTagName: String) { + if (tags.any { it.tagId == 0L }) + throw IllegalStateException("Tags must be reference to database entries") + + val tagId = + tags.find { it.name == resultingTagName }?.tagId ?: getIdByTagName(resultingTagName) + + + if (tagId == 0L) + insert(Tag(resultingTagName)) + + + val cleanedTags = tags.filter { it.name != resultingTagName } + reassignSecureElementsToNewTag(resultingTagName, cleanedTags.map { it.name }) + + deleteTags(cleanedTags) + } + @Transaction @Query("SELECT * FROM SecureElement WHERE favorite ORDER BY ROWID ASC LIMIT :limit") abstract suspend fun getFavorites(limit: Int = 5): List diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt index 0054aa42..f0598610 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt @@ -95,7 +95,35 @@ class OptionBottomSheet( binding.favorite.visibility = View.GONE if (items.size > 1) { - binding.edit.visibility = View.GONE + binding.edit.text = getString(R.string.merge_tag) + + binding.edit.setOnClickListener { + dismiss() + + EditDialogBuilder(requireContext()).apply { + setTitle(R.string.merge_tag) + withInformation(InformationView.Information().apply { + text = list.first().tag.name + hint = getString(R.string.tag) + inputType = InputType.TYPE_CLASS_TEXT + isSecret = false + }) + setButtonListener( + DialogInterface.BUTTON_POSITIVE, + R.string.ok + ) { dialog, _, newText -> + if (!checkInput(newText, dialog as AlertDialog)) + return@setButtonListener + + + lifecycleScope.launch { + SecureElementManager.mergeTags(list.map { it.tag }, newText) + + dialog.dismiss() + } + } + }.show() + } } else { val firstTag = list.first() binding.edit.setOnClickListener { @@ -106,18 +134,8 @@ class OptionBottomSheet( DialogInterface.BUTTON_POSITIVE, R.string.ok ) { dialog, _, newText -> - val inputLayout = - (dialog as AlertDialog).findViewById(R.id.textInputLayout)!! - if (newText.isBlank()) { - inputLayout.error = context.getString(R.string.tag_cant_be_blank) - inputLayout.editText?.text?.clear() + if (!checkInput(newText, dialog as AlertDialog)) return@setButtonListener - } - - if (newText.isProtectedTagName) { - inputLayout.error = context.getString(R.string.prefix_not_allowed) - return@setButtonListener - } if (newText == firstTag.tag.name) { dialog.dismiss() @@ -136,13 +154,15 @@ class OptionBottomSheet( return@launch } + val inputLayout = + dialog.findViewById(R.id.textInputLayout)!! inputLayout.error = context.getString(R.string.tag_already_existed) } } withInformation(InformationView.Information().apply { text = title - hint = getString(R.string.title) + hint = getString(R.string.tag) inputType = InputType.TYPE_CLASS_TEXT isSecret = false }) @@ -151,6 +171,22 @@ class OptionBottomSheet( } } + private fun EditDialogBuilder.checkInput(input: String, dialog: AlertDialog): Boolean { + val inputLayout = dialog.findViewById(R.id.textInputLayout)!! + if (input.isBlank()) { + inputLayout.error = context.getString(R.string.tag_cant_be_blank) + inputLayout.editText?.text?.clear() + return false + } + + if (input.isProtectedTagName) { + inputLayout.error = context.getString(R.string.prefix_not_allowed) + return false + } + + return true + } + private fun setTitle(list: List, titleProvider: (I) -> String): String { val title = if (list.size > 1) requireContext().getString(R.string.options) else titleProvider(list[0]) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 507538d6..eb739d41 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -152,6 +152,7 @@ Um ein Backup laden zu können, ist eine Authentifizierung erforderlich Authentifizieren um fortzufahren + Tag Tags Tag-Layout Dashboard gruppiert nach Tags @@ -159,6 +160,7 @@ Tag bearbeiten Tags hier hinzufügen + Merge tags Dieser Tag existiert bereits Tags dürfen nicht leer sein Dieser Prefix ist reserviert und kann nicht verwendet werden diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f292d2b..137a09a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -194,6 +194,7 @@ To load a backup, authentication is required Authenticate to proceed + Tag Tags Tag Layout Dashboard grouped by tags @@ -201,6 +202,7 @@ Add tags here Edit tag + Merge tags This tag already exists Tags can\'t be blank This prefix is reserved and can\'t be used From d556c0c35ed88c91818e5c9fc7fcb0a87e755658 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Tue, 26 Dec 2023 01:33:19 +0100 Subject: [PATCH 44/46] [SecureElement] Fixed tag list initiation issue Could have caused a problem setting tags directly via the constructor -> default tags would not have been set --- .../de/davis/passwordmanager/database/dtos/SecureElement.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt index 4b38406e..d7942ee6 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt @@ -55,12 +55,16 @@ private object TimestampsParceler : Parceler { data class SecureElement @JvmOverloads constructor( var title: String, var detail: ElementDetail, - var tags: List = listOf(detail.elementType.tag), + var tags: List = listOf(), var favorite: Boolean = false, private var timestamps: Timestamps = Timestamps.CURRENT, @Exclude override val id: Long = 0 ) : Item, Comparable, Parcelable { + init { + tags += detail.elementType.tag + } + val letter get() = title[0].uppercaseChar() val elementType: ElementType get() = detail.elementType From d401430ce848bcc2f7de1e1b5c90a88b9dec7c03 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Tue, 26 Dec 2023 11:11:12 +0100 Subject: [PATCH 45/46] [KeyGoDatabaseTest] Added tag merge test --- .../database/KeyGoDatabaseTest.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt index fa9ee146..3aabade2 100644 --- a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt +++ b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt @@ -6,9 +6,11 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import de.davis.passwordmanager.database.daos.SecureElementWithTagDao import de.davis.passwordmanager.database.dtos.SecureElement +import de.davis.passwordmanager.database.entities.Tag import de.davis.passwordmanager.database.entities.details.creditcard.CreditCardDetails import de.davis.passwordmanager.database.entities.details.creditcard.Name import de.davis.passwordmanager.database.entities.details.password.PasswordDetails +import de.davis.passwordmanager.database.entities.onlyCustoms import de.davis.passwordmanager.database.entities.wrappers.CombinedElement import de.davis.passwordmanager.utils.GeneratorUtil import kotlinx.coroutines.test.runTest @@ -84,6 +86,63 @@ class KeyGoDatabaseTest { } } + @Test + fun testMergeTags() = runTest { + val tag1 = "TAG 1" + val tag2 = "TAG 2" + val tag32 = "TAG 32" + + val tagIgnore = "TAG IGN" + val mergeTag = "TAG 1" + + var creditCard = writeAndRead( + SecureElement( + "C", + CreditCardDetails(Name.fromFullName(""), "", "0000000000000000", ""), + listOf(Tag(tag1), Tag(tag2)) + ) + ) + + var password = writeAndRead( + SecureElement( + "P", + PasswordDetails("pwd", "", ""), + listOf(Tag(tag32), Tag(tagIgnore)) + ) + ) + + secureElementWithTagDao.mergeTags( + creditCard.tags.onlyCustoms().filter { it.name != tagIgnore } + + password.tags.onlyCustoms().filter { it.name != tagIgnore }, + mergeTag + ) + + creditCard = + secureElementWithTagDao.getCombinedElementById(creditCard.secureElementEntity.id) + password = secureElementWithTagDao.getCombinedElementById(password.secureElementEntity.id) + + val creditCardTags = creditCard.tags.map { it.name } + val passwordTags = password.tags.map { it.name } + + assertTrue( + creditCardTags.size == 2 && creditCardTags.containsAll( + listOf( + ElementType.CREDIT_CARD.tag.name, + mergeTag + ) + ) + ) + assertTrue( + passwordTags.size == 3 && passwordTags.containsAll( + listOf( + ElementType.PASSWORD.tag.name, + tagIgnore, + mergeTag + ) + ) + ) + } + private suspend fun writeAndRead(element: SecureElement): CombinedElement { val id: Long = secureElementWithTagDao.insert(element.toEntity()) From 66c3d4a95bb60649a2f115301439f3ece546a5c2 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Tue, 26 Dec 2023 11:20:21 +0100 Subject: [PATCH 46/46] [Dashboard Files] Moved to UI package --- .../passwordmanager/dashboard/Header.java | 9 ---- .../davis/passwordmanager/dashboard/Item.kt | 5 --- .../SelectionStateChangedDiffCallback.java | 41 ------------------- .../database/SecureElementManager.kt | 2 +- .../passwordmanager/database/dtos/Item.kt | 5 +++ .../database/dtos/SecureElement.kt | 1 - .../database/dtos/TagWithCount.kt | 1 - .../passwordmanager/dialog/DeleteDialog.java | 2 +- .../ui/dashboard/DashboardAdapter.kt | 13 +++--- .../ui/dashboard/DashboardFragment.kt | 2 +- .../ui/dashboard/DashboardViewModel.kt | 2 +- .../dashboard/SecureElementDiffCallback.kt | 3 +- .../ui/dashboard/managers/AbsItemManager.kt | 4 +- .../dashboard/managers/ElementItemManager.kt | 10 +++-- .../ui/dashboard/managers/TagItemManager.kt | 2 +- .../DefaultElementMenuProvider.kt | 2 +- .../dashboard/selection/KeyProvider.java | 2 +- .../selection/SecureElementDetailsLookup.java | 2 +- .../viewholders/BasicViewHolder.java | 6 +-- .../viewholders/SecureElementViewHolder.java | 2 +- .../ui/highlights/HighlightsFragment.java | 2 +- .../ui/views/OptionBottomSheet.kt | 2 +- 22 files changed, 37 insertions(+), 83 deletions(-) delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/Header.java delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt delete mode 100644 app/src/main/java/de/davis/passwordmanager/dashboard/SelectionStateChangedDiffCallback.java create mode 100644 app/src/main/java/de/davis/passwordmanager/database/dtos/Item.kt rename app/src/main/java/de/davis/passwordmanager/{ => ui}/dashboard/SecureElementDiffCallback.kt (89%) rename app/src/main/java/de/davis/passwordmanager/{ => ui}/dashboard/selection/KeyProvider.java (93%) rename app/src/main/java/de/davis/passwordmanager/{ => ui}/dashboard/selection/SecureElementDetailsLookup.java (94%) rename app/src/main/java/de/davis/passwordmanager/{ => ui}/dashboard/viewholders/BasicViewHolder.java (81%) rename app/src/main/java/de/davis/passwordmanager/{ => ui}/dashboard/viewholders/SecureElementViewHolder.java (98%) diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/Header.java b/app/src/main/java/de/davis/passwordmanager/dashboard/Header.java deleted file mode 100644 index b14da57c..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/Header.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.davis.passwordmanager.dashboard; - -public record Header(char header) implements Item { - - @Override - public long getId() { - return -header; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt b/app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt deleted file mode 100644 index e1d08a7d..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/Item.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.davis.passwordmanager.dashboard - -interface Item { - val id: Long -} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/SelectionStateChangedDiffCallback.java b/app/src/main/java/de/davis/passwordmanager/dashboard/SelectionStateChangedDiffCallback.java deleted file mode 100644 index 6de45374..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/SelectionStateChangedDiffCallback.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.davis.passwordmanager.dashboard; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.DiffUtil; - -public class SelectionStateChangedDiffCallback extends DiffUtil.Callback { - - private final int listSize; - private final boolean selectable; - - public SelectionStateChangedDiffCallback(int listSize, boolean selectable) { - this.listSize = listSize; - this.selectable = selectable; - } - - @Override - public int getOldListSize() { - return listSize; - } - - @Override - public int getNewListSize() { - return getOldListSize(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return true; - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return false; - } - - @Nullable - @Override - public Object getChangePayload(int oldItemPosition, int newItemPosition) { - return selectable; - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt index eb9c8222..17ee16b2 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -2,8 +2,8 @@ package de.davis.passwordmanager.database import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData -import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.daos.SecureElementWithTagDao +import de.davis.passwordmanager.database.dtos.Item import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.dtos.toDto diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/Item.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/Item.kt new file mode 100644 index 00000000..ed92285d --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/Item.kt @@ -0,0 +1,5 @@ +package de.davis.passwordmanager.database.dtos + +interface Item { + val id: Long +} \ No newline at end of file diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt index d7942ee6..10908023 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt @@ -7,7 +7,6 @@ import android.os.Parcelable import android.util.TypedValue import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator -import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.ElementType import de.davis.passwordmanager.database.entities.SecureElementEntity import de.davis.passwordmanager.database.entities.Tag diff --git a/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt b/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt index b9490d31..dbd9c1c5 100644 --- a/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt @@ -1,6 +1,5 @@ package de.davis.passwordmanager.database.dtos -import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.entities.Tag import de.davis.passwordmanager.database.entities.TagWithCountEntity diff --git a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java index 9b6e81cb..162cec97 100644 --- a/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java +++ b/app/src/main/java/de/davis/passwordmanager/dialog/DeleteDialog.java @@ -8,8 +8,8 @@ import java.util.List; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.Item; import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dtos.Item; public class DeleteDialog { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt index 648d63c7..41cb6f64 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt @@ -7,14 +7,13 @@ import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import de.davis.passwordmanager.dashboard.Item -import de.davis.passwordmanager.dashboard.SecureElementDiffCallback -import de.davis.passwordmanager.dashboard.selection.KeyProvider -import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.database.dtos.Item import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.entities.shouldBeProtected import de.davis.passwordmanager.ui.dashboard.managers.AbsItemManager +import de.davis.passwordmanager.ui.dashboard.selection.KeyProvider +import de.davis.passwordmanager.ui.dashboard.selection.SecureElementDetailsLookup +import de.davis.passwordmanager.ui.dashboard.viewholders.BasicViewHolder class DashboardAdapter(private val onUpdate: (DashboardAdapter) -> Unit) : RecyclerView.Adapter>() { @@ -61,7 +60,9 @@ class DashboardAdapter(private val onUpdate: (DashboardAdapter) -> Unit) : "tracker", this, KeyProvider(this), - SecureElementDetailsLookup(this), + SecureElementDetailsLookup( + this + ), StorageStrategy.createLongStorage() ).withSelectionPredicate(object : SelectionPredicate() { override fun canSetStateForKey(key: Long, nextState: Boolean): Boolean { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt index cd484416..3d2b7383 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt @@ -20,7 +20,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnScrollListener import androidx.slidingpanelayout.widget.SlidingPaneLayout import de.davis.passwordmanager.R -import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.dtos.Item import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.databinding.FragmentDashboardBinding diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt index 4ea72ebf..b5f69106 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt @@ -5,8 +5,8 @@ import android.content.Context import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import de.davis.passwordmanager.R -import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.SecureElementManager +import de.davis.passwordmanager.database.dtos.Item import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.entities.onlyCustoms diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/SecureElementDiffCallback.kt similarity index 89% rename from app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt rename to app/src/main/java/de/davis/passwordmanager/ui/dashboard/SecureElementDiffCallback.kt index a67756b7..a0306085 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/SecureElementDiffCallback.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/SecureElementDiffCallback.kt @@ -1,6 +1,7 @@ -package de.davis.passwordmanager.dashboard +package de.davis.passwordmanager.ui.dashboard import androidx.recyclerview.widget.DiffUtil +import de.davis.passwordmanager.database.dtos.Item class SecureElementDiffCallback( private val oldItems: List, diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt index 0d366a90..98ca3ed3 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/AbsItemManager.kt @@ -4,8 +4,8 @@ import android.content.Context import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView.ItemDecoration import androidx.recyclerview.widget.RecyclerView.LayoutManager -import de.davis.passwordmanager.dashboard.Item -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.database.dtos.Item +import de.davis.passwordmanager.ui.dashboard.viewholders.BasicViewHolder sealed class AbsItemManager( initialItems: List, diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt index 2b569b96..71f362f4 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt @@ -10,10 +10,10 @@ import android.widget.TextView import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView import de.davis.passwordmanager.R -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder -import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.ui.LinearLayoutManager +import de.davis.passwordmanager.ui.dashboard.viewholders.BasicViewHolder +import de.davis.passwordmanager.ui.dashboard.viewholders.SecureElementViewHolder class ElementItemManager( initialItems: List, @@ -23,7 +23,11 @@ class ElementItemManager( AbsItemManager(initialItems, onClick) { override fun createViewHolder(parent: ViewGroup): BasicViewHolder { - return SecureElementViewHolder(LayoutInflater.from(parent.context), parent, fragmentManager) + return SecureElementViewHolder( + LayoutInflater.from(parent.context), + parent, + fragmentManager + ) } override fun bind( diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt index aa4d8ade..0f4c0685 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/TagItemManager.kt @@ -10,10 +10,10 @@ import androidx.recyclerview.selection.ItemDetailsLookup import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import de.davis.passwordmanager.R -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.entities.getLocalizedName import de.davis.passwordmanager.ui.GridLayoutManager +import de.davis.passwordmanager.ui.dashboard.viewholders.BasicViewHolder import de.davis.passwordmanager.ui.views.InformationView class TagItemManager( diff --git a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt index 3ed6084a..8b9f0830 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt @@ -7,7 +7,7 @@ import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentManager import de.davis.passwordmanager.R -import de.davis.passwordmanager.dashboard.Item +import de.davis.passwordmanager.database.dtos.Item import de.davis.passwordmanager.ui.dashboard.DashboardAdapter import de.davis.passwordmanager.ui.views.FilterBottomSheet import de.davis.passwordmanager.ui.views.OptionBottomSheet diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/selection/KeyProvider.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/selection/KeyProvider.java similarity index 93% rename from app/src/main/java/de/davis/passwordmanager/dashboard/selection/KeyProvider.java rename to app/src/main/java/de/davis/passwordmanager/ui/dashboard/selection/KeyProvider.java index e0e88de2..b3a22cb9 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/selection/KeyProvider.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/selection/KeyProvider.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.dashboard.selection; +package de.davis.passwordmanager.ui.dashboard.selection; import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/selection/SecureElementDetailsLookup.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/selection/SecureElementDetailsLookup.java similarity index 94% rename from app/src/main/java/de/davis/passwordmanager/dashboard/selection/SecureElementDetailsLookup.java rename to app/src/main/java/de/davis/passwordmanager/ui/dashboard/selection/SecureElementDetailsLookup.java index 75b8002a..e4239ae0 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/selection/SecureElementDetailsLookup.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/selection/SecureElementDetailsLookup.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.dashboard.selection; +package de.davis.passwordmanager.ui.dashboard.selection; import android.view.MotionEvent; import android.view.View; diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/BasicViewHolder.java similarity index 81% rename from app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java rename to app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/BasicViewHolder.java index c7c76149..df49cb28 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/BasicViewHolder.java @@ -1,12 +1,12 @@ -package de.davis.passwordmanager.dashboard.viewholders; +package de.davis.passwordmanager.ui.dashboard.viewholders; import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import de.davis.passwordmanager.dashboard.Item; -import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup; +import de.davis.passwordmanager.database.dtos.Item; +import de.davis.passwordmanager.ui.dashboard.selection.SecureElementDetailsLookup; public abstract class BasicViewHolder extends RecyclerView.ViewHolder implements SecureElementDetailsLookup.ItemDetailsLookup { diff --git a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java similarity index 98% rename from app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java rename to app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java index ba37f761..df34761f 100644 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/SecureElementViewHolder.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java @@ -1,4 +1,4 @@ -package de.davis.passwordmanager.dashboard.viewholders; +package de.davis.passwordmanager.ui.dashboard.viewholders; import android.annotation.SuppressLint; import android.content.Context; diff --git a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java index 492286cc..7dbf191f 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java +++ b/app/src/main/java/de/davis/passwordmanager/ui/highlights/HighlightsFragment.java @@ -18,9 +18,9 @@ import java.util.List; import de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.viewholders.SecureElementViewHolder; import de.davis.passwordmanager.database.dtos.SecureElement; import de.davis.passwordmanager.databinding.FragmentHighlightsBinding; +import de.davis.passwordmanager.ui.dashboard.viewholders.SecureElementViewHolder; import de.davis.passwordmanager.ui.viewmodels.HighlightsViewModel; public class HighlightsFragment extends Fragment { diff --git a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt index f0598610..9e95102e 100644 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt @@ -11,9 +11,9 @@ import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.textfield.TextInputLayout import de.davis.passwordmanager.R -import de.davis.passwordmanager.dashboard.Item import de.davis.passwordmanager.database.SecureElementManager import de.davis.passwordmanager.database.SecureElementManager.switchFavStateCoroutine +import de.davis.passwordmanager.database.dtos.Item import de.davis.passwordmanager.database.dtos.SecureElement import de.davis.passwordmanager.database.dtos.TagWithCount import de.davis.passwordmanager.database.entities.getLocalizedName