diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ad542c3..f428c461 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") @@ -120,10 +121,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") @@ -131,4 +136,8 @@ dependencies { room { schemaDirectory("$projectDir/schemas") +} + +ksp { + arg("room.generateKotlin", "true") } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9730751c..6f7a9b88 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -33,7 +33,10 @@ -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, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) diff --git a/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json b/app/schemas/de.davis.passwordmanager.database.KeyGoDatabase/3.json index 54678e40..30ba2c0c 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": "0a97d13a94575bc2ed2ab009853b0086", "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 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": false, + "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, '0a97d13a94575bc2ed2ab009853b0086')" ] } } \ 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.java b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.java deleted file mode 100644 index cb0e73ec..00000000 --- a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.java +++ /dev/null @@ -1,94 +0,0 @@ -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; - - @Before - public void createDB() { - Context context = ApplicationProvider.getApplicationContext(); - db = Room.inMemoryDatabaseBuilder(context, KeyGoDatabase.class).build(); - secureElementDao = db.secureElementDao(); - } - - @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(); - - 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()); - } - - private SecureElement writeAndRead(SecureElement element) { - long id = secureElementDao.insert(element); - - return secureElementDao.getById(id); - } - - private String generateTestPassword(){ - return GeneratorUtil.generatePassword(15_000, GeneratorUtil.USE_DIGITS | - GeneratorUtil.USE_LOWERCASE | - GeneratorUtil.USE_PUNCTUATION | - GeneratorUtil.USE_UPPERCASE); - } - - @After - public void cleanUp(){ - db.close(); - } -} diff --git a/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt new file mode 100644 index 00000000..3aabade2 --- /dev/null +++ b/app/src/androidTest/java/de/davis/passwordmanager/database/KeyGoDatabaseTest.kt @@ -0,0 +1,165 @@ +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.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 +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 + fun createDB() { + val context = ApplicationProvider.getApplicationContext() + db = inMemoryDatabaseBuilder(context, KeyGoDatabase::class.java).build() + secureElementWithTagDao = db.combinedDao() + } + + @Test + 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) + } + + } + + @Test + 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) + } + } + + @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()) + + return secureElementWithTagDao.getCombinedElementById(id); + } + + private fun generateTestPassword(): String { + return GeneratorUtil.generatePassword( + 15000, GeneratorUtil.USE_DIGITS or + GeneratorUtil.USE_LOWERCASE or + GeneratorUtil.USE_PUNCTUATION or + GeneratorUtil.USE_UPPERCASE + ) + } + + @After + fun cleanUp() { + db.close() + } +} \ No newline at end of file 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/backup/csv/CsvBackup.kt b/app/src/main/java/de/davis/passwordmanager/backup/csv/CsvBackup.kt index 30b32886..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 @@ -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.dtos.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..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 @@ -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.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 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 deleted file mode 100644 index 41bba79d..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.security.element.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/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.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/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/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/dashboard/viewholders/BasicViewHolder.java b/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java deleted file mode 100644 index 0e815793..00000000 --- a/app/src/main/java/de/davis/passwordmanager/dashboard/viewholders/BasicViewHolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.davis.passwordmanager.dashboard.viewholders; - -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import de.davis.passwordmanager.dashboard.selection.SecureElementDetailsLookup; -import de.davis.passwordmanager.security.element.SecureElement; - -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 abstract void onBindSelectablePayload(boolean selectable, boolean selected); - - public interface OnItemClickedListener { - void onClicked(SecureElement 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/database/ElementType.kt b/app/src/main/java/de/davis/passwordmanager/database/ElementType.kt new file mode 100644 index 00000000..c0d9cf74 --- /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 + +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 entries.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..9f8ed43e 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().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..17ee16b2 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/SecureElementManager.kt @@ -0,0 +1,181 @@ +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.dtos.Item +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 +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) } + } + + 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 { + 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) + } + } + + suspend fun updateTag(tag: Tag): Int { + return dao.updateTag(tag) + } + + @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()) + } + + @JvmStatic + @JvmName("insertElement") + fun insertElementCoroutine(secureElement: SecureElement) { + scope.launch { + insertElement(secureElement) + } + } + + @JvmStatic + 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("delete") + fun deleteCoroutine(items: List) { + scope.launch { + deleteElements(items.filterIsInstance()) + deleteTags(items.filterIsInstance().map { it.tag }) + } + } + + 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 { + 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..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; @@ -7,15 +8,16 @@ 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 { 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/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..c3d17995 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/daos/SecureElementWithTagDao.kt @@ -0,0 +1,163 @@ +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.TagWithCountEntity +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(otherwise = VisibleForTesting.NONE) + @Query("SELECT * FROM SecureElement WHERE id = :id") + abstract suspend fun getCombinedElementById(id: Long): CombinedElement + + @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 + + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun insert(tag: Tag): Long + + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun insert(crossRef: SecureElementTagCrossRef) + + @Delete + abstract suspend fun deleteElements(secureElementEntities: List) + + @Delete + abstract suspend fun deleteTags(tags: List) + + @Update + abstract suspend fun update(secureElementEntity: SecureElementEntity) + + @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") + protected abstract suspend fun deleteTagRelationsTo(elementId: Long) + + @Query("DELETE FROM Tag WHERE tagId NOT IN (SELECT tagId FROM SecureElementTagCrossRef)") + protected abstract suspend fun deleteUnusedTags() + + @Transaction + open suspend fun insert(elementWithTags: CombinedElement): Long { + val elementId = insert(elementWithTags.secureElementEntity) + insertTagsAndMakeRelation(elementWithTags.tags, elementId) + return elementId + } + + private suspend fun insertTagsAndMakeRelation(tags: List, elementId: Long) { + tags.forEach { + var tagId = getIdByTagName(it.name) + + if (tagId == 0L) { + tagId = insert(it) + } + + insert( + SecureElementTagCrossRef( + elementId, + tagId + ) + ) + } + } + + @Update(onConflict = OnConflictStrategy.IGNORE) + abstract suspend fun updateTag(tag: Tag): Int + + @Transaction + open suspend fun update(elementWithTags: CombinedElement) { + elementWithTags.secureElementEntity.run { + timestamps.modifiedAt = Date() + update(this) + id.run { + deleteTagRelationsTo(this) + insertTagsAndMakeRelation(elementWithTags.tags, this) + deleteUnusedTags() + } + } + } + + suspend fun updateModifiedAt(elementWithTags: CombinedElement) { + elementWithTags.secureElementEntity.run { + timestamps.modifiedAt = Date() + update(this) + } + } + + @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 + + @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/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 new file mode 100644 index 00000000..10908023 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/SecureElement.kt @@ -0,0 +1,106 @@ +package de.davis.passwordmanager.database.dtos + +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.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.readLong()) + } + + override fun Tag.write(parcel: Parcel, flags: Int) { + parcel.apply { + writeString(name) + writeLong(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 +data class SecureElement @JvmOverloads constructor( + var title: String, + var detail: ElementDetail, + 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 + + 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, combinedElement.tags, favorite, timestamps, id) + } + return@run secureElement + } + } +} \ No newline at end of file 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..dbd9c1c5 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/dtos/TagWithCount.kt @@ -0,0 +1,12 @@ +package de.davis.passwordmanager.database.dtos + +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/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..5ab272d4 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Tag.kt @@ -0,0 +1,26 @@ +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)]) +data class Tag @JvmOverloads constructor( + val name: String, + @Exclude @PrimaryKey(autoGenerate = true) val tagId: Long = 0 +) + +fun Collection.onlyCustoms(): Collection { + 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/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 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..bf959d0c --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/Timestamps.kt @@ -0,0 +1,19 @@ +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 +) { + + init { + if (createdAt == null) + createdAt = Date() + } + + companion object { + val CURRENT get() = Timestamps(Date()) + } +} 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 89% 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..7aaa3a49 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,10 +1,10 @@ -package de.davis.passwordmanager.security.element.creditcard; +package de.davis.passwordmanager.database.entities.details.creditcard; 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/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/database/entities/details/password/PasswordDetails.kt b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt new file mode 100644 index 00000000..3d034475 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/database/entities/details/password/PasswordDetails.kt @@ -0,0 +1,58 @@ +package de.davis.passwordmanager.database.entities.details.password + +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 + +class PasswordDetails( + password: String, + var origin: String, + var username: String, +) : ElementDetail { + + var strength: Strength = EstimationHandler.estimate(password) + private set + + @SerializedName("password") + private var passwordEncrypted: ByteArray = Cryptography.encryptAES(password.toByteArray()) + + fun setPassword(password: String) { + strength = EstimationHandler.estimate(password) + passwordEncrypted = Cryptography.encryptAES(password.toByteArray()) + } + + val password get() = String(Cryptography.decryptAES(passwordEncrypted)) + + + override fun getElementType(): ElementType { + return ElementType.PASSWORD + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PasswordDetails + + if (origin != other.origin) return false + if (username != other.username) return false + if (!passwordEncrypted.contentEquals(other.passwordEncrypted)) return false + + return true + } + + override fun hashCode(): Int { + var result = origin.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + passwordEncrypted.contentHashCode() + return result + } + + + 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.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/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/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 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..162cec97 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.dtos.Item; 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) -> { + SecureElementManager.delete(toDelete); - 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/dialog/EditDialogBuilder.java b/app/src/main/java/de/davis/passwordmanager/dialog/EditDialogBuilder.java index 80224c39..78988bdd 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,8 @@ 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; @@ -25,6 +28,9 @@ public class EditDialogBuilder extends BaseDialogBuilder { @LayoutRes private int additionalCustomLayout; + private DialogEditViewBinding binding; + private final OnClickListener[] listeners = new OnClickListener[3]; + public EditDialogBuilder(@NonNull Context context) { super(context); } @@ -35,7 +41,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 +79,41 @@ 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, getText())); + } + + @NonNull + 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 { + + void onClick(DialogInterface dialog, int which, String newText); + } } 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 577cecd2..00000000 --- a/app/src/main/java/de/davis/passwordmanager/filter/Filter.java +++ /dev/null @@ -1,96 +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.security.element.SecureElement; -import de.davis.passwordmanager.security.element.password.PasswordDetails; -import de.davis.passwordmanager.security.element.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 final List selectedIds = new ArrayList<>(); - - private Filter(){} - - public void setType(ChipGroup type) { - this.type = type; - } - - public void setStrength(ChipGroup strength) { - this.strength = strength; - } - - public Runnable updater; - - public void setUpdater(Runnable updater) { - this.updater = updater; - } - - public void update() { - if(updater != null) - updater.run(); - - if(!groupsSet()) - return; - - selectedIds.clear(); - selectedIds.addAll(type.getCheckedChipIds()); - selectedIds.addAll(strength.getCheckedChipIds()); - } - - public List getSelectedIds() { - return selectedIds; - } - - private boolean groupsSet(){ - return type != null && strength != null; - } - - public List filter(List elements) { - if(!groupsSet()) - return elements; - - List toFilter = new ArrayList<>(elements); - List typeIds = type.getCheckedChipIds(); - List strengthIds = strength.getCheckedChipIds(); - if(!typeIds.contains(ID_CREDIT_CARD)) - toFilter.removeIf(element -> element.getType() == SecureElement.TYPE_CREDIT_CARD); - - - if(!typeIds.contains(ID_PASSWORD)) { - toFilter.removeIf(element -> element.getType() == SecureElement.TYPE_PASSWORD); - return toFilter; - } - - toFilter.removeIf(element -> { - if(element.getType() != SecureElement.TYPE_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 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/gson/ElementDetailTypeAdapter.java b/app/src/main/java/de/davis/passwordmanager/gson/ElementDetailTypeAdapter.java index 803bd003..9ef0393f 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,28 @@ 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; +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(); - return context.deserialize(json, SecureElementDetail.getFor(type).getElementDetailClass()); + + JsonElement strengthElement = json.getAsJsonObject().get("strength"); + if(strengthElement != null && strengthElement.isJsonObject()){ + json.getAsJsonObject().addProperty("strength", Strength.getEntries().get(strengthElement.getAsJsonObject().get("type").getAsInt()).name()); + } + + 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/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 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/listeners/OnInformationChangedListener.java b/app/src/main/java/de/davis/passwordmanager/listeners/OnInformationChangedListener.java index 1dfff886..dbd5a346 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.dtos.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..9be87e08 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.dtos.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/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(); -} 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/security/element/password/PasswordDetails.java b/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java deleted file mode 100644 index 877f6782..00000000 --- a/app/src/main/java/de/davis/passwordmanager/security/element/password/PasswordDetails.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.davis.passwordmanager.security.element.password; - -import java.io.Serial; -import java.util.Objects; - -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 { - - @Serial - private static final long serialVersionUID = 4938873580704485021L; - - 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; - } - - public String getOrigin() { - return origin; - } - - public void setOrigin(String origin) { - this.origin = origin; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public Strength getStrength() { - return strength; - } - - public void setPassword(String password){ - this.password = Cryptography.encryptAES(password.getBytes()); - this.strength = Strength.estimateStrength(password); - } - - public byte[] getPasswordData() { - return password; - } - - public String getPassword(){ - return new String(Cryptography.decryptAES(getPasswordData())); - } - - @SecureElement.ElementType - @Override - public int getType() { - return SecureElement.TYPE_PASSWORD; - } - - @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()); - } -} diff --git a/app/src/main/java/de/davis/passwordmanager/security/element/password/Strength.java b/app/src/main/java/de/davis/passwordmanager/security/element/password/Strength.java deleted file mode 100644 index 20afb7f8..00000000 --- a/app/src/main/java/de/davis/passwordmanager/security/element/password/Strength.java +++ /dev/null @@ -1,73 +0,0 @@ -package de.davis.passwordmanager.security.element.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/service/AutoFillService.java b/app/src/main/java/de/davis/passwordmanager/service/AutoFillService.java index 4511dd7c..4fe5eb8d 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.dtos.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..adba0b8e 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.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; -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/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/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); } } 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() ) ) 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..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 @@ -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 @@ -33,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) { @@ -44,6 +52,6 @@ public void onPanelOpened(@NonNull View panel) { @Override public void onPanelClosed(@NonNull View panel) { - + updateState(); } } 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..41cb6f64 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardAdapter.kt @@ -0,0 +1,143 @@ +package de.davis.passwordmanager.ui.dashboard + +import android.os.Bundle +import android.view.ViewGroup +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 +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>() { + + 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(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 + + 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 f0967f4d..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.java +++ /dev/null @@ -1,286 +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 de.davis.passwordmanager.R; -import de.davis.passwordmanager.dashboard.DashboardAdapter; -import de.davis.passwordmanager.dashboard.viewholders.BasicViewHolder; -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; -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 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); - - 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); - - BasicViewHolder.OnItemClickedListener onItemClickedListener = element -> { - scrollingViewModel.setVisibility(false); - binding.listPane.searchView.hide(); - Bundle bundle = new Bundle(); - bundle.putSerializable("element", element); - navController.popBackStack(); - navController.navigate(SecureElementDetail.getFor(element).getViewFragmentId(), bundle); - - binding.getRoot().open(); - }; - - dashboardAdapter.setOnItemClickedListener(onItemClickedListener); - - DashboardAdapter searchResultAdapter = new DashboardAdapter(); - searchResultAdapter.setOnItemClickedListener(onItemClickedListener); - binding.listPane.recyclerViewResults.setAdapter(searchResultAdapter); - - - addMenu(dashboardAdapter); - - 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(), secureElements -> SecureElementManager.getInstance().update(secureElements)); - 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 = (SecureElement) getArguments().getSerializable("element"); - if(element == null) - return; - - onItemClickedListener.onClicked(element); - oldState = false; - } - - @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(DashboardAdapter dashboardAdapter){ - requireActivity().addMenuProvider(new MenuProvider() { - @Override - public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { - menuInflater.inflate(R.menu.view_menu, menu); - dashboardAdapter.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(dashboardAdapter.getTracker().hasSelection()); - } - - @Override - public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { - if(menuItem.getItemId() == R.id.more){ - OptionBottomSheet optionBottomSheet = new OptionBottomSheet(requireContext(), null); - optionBottomSheet.show(); - }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); - SecureElementManager.getInstance().getAdapter().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); - SecureElementManager.getInstance().getAdapter().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..3d2b7383 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardFragment.kt @@ -0,0 +1,267 @@ +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.database.dtos.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) + + dashboardViewModel.initiateState() + + 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() + } + + override fun onPause() { + super.onPause() + scrollingViewModel.setVisibility(false) + } + + override fun onResume() { + super.onResume() + scrollingViewModel.setVisibility(true) + } + + 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..b5f69106 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,105 @@ +package de.davis.passwordmanager.ui.dashboard + +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.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 +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 +import kotlinx.coroutines.flow.asStateFlow +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 +} + +class DashboardViewModel(private val application: Application) : AndroidViewModel(application) { + + private val _state = MutableStateFlow>?>(null) + val state = _state.asStateFlow() + + private val _listState = MutableStateFlow(application.getDefaultListState()) + + 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 initiateState() { + updateState(application.getDefaultListState()) + } + + 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/SecureElementDiffCallback.kt b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/SecureElementDiffCallback.kt new file mode 100644 index 00000000..a0306085 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/SecureElementDiffCallback.kt @@ -0,0 +1,29 @@ +package de.davis.passwordmanager.ui.dashboard + +import androidx.recyclerview.widget.DiffUtil +import de.davis.passwordmanager.database.dtos.Item + +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 { + 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 oldItem == newItem + } +} \ 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..98ca3ed3 --- /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.database.dtos.Item +import de.davis.passwordmanager.ui.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..71f362f4 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/managers/ElementItemManager.kt @@ -0,0 +1,231 @@ +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.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, + 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..0f4c0685 --- /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.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( + 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..8b9f0830 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/menuprovider/DefaultElementMenuProvider.kt @@ -0,0 +1,51 @@ +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.database.dtos.Item +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) + 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/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/ui/dashboard/viewholders/BasicViewHolder.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/BasicViewHolder.java new file mode 100644 index 00000000..df49cb28 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/BasicViewHolder.java @@ -0,0 +1,29 @@ +package de.davis.passwordmanager.ui.dashboard.viewholders; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +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 { + + public BasicViewHolder(@NonNull View itemView) { + super(itemView); + } + + 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); + + protected abstract void handleSelectionState(boolean selected); + + 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/SecureElementViewHolder.java b/app/src/main/java/de/davis/passwordmanager/ui/dashboard/viewholders/SecureElementViewHolder.java similarity index 59% 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 3371a4e6..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,27 +1,34 @@ -package de.davis.passwordmanager.dashboard.viewholders; +package de.davis.passwordmanager.ui.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; + 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.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; public class SecureElementViewHolder extends BasicViewHolder { @@ -29,21 +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) { - 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); - checkBox = itemView.findViewById(R.id.checkboxSelection); + 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(); @@ -58,12 +68,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,14 +87,19 @@ 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<>(List.of(item), SecureElement.class).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 @@ -92,7 +107,7 @@ public ItemDetailsLookup.ItemDetails getItemDetails() { return new ItemDetailsLookup.ItemDetails<>() { @Override public int getPosition() { - return getAdapterPosition(); + return getAbsoluteAdapterPosition(); } @NonNull 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..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 @@ -10,7 +10,9 @@ 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.dtos.SecureElement; +import de.davis.passwordmanager.ui.views.TagView; public abstract class CreateSecureElementActivity extends SEViewActivity { @@ -22,7 +24,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 +51,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 +67,11 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { @Override public void fillInElement(@NonNull SecureElement secureElement) { - setTitle(secureElement.getTypeName()); + setTitle(secureElement.getElementType().getTitle()); + ((TagView) findViewById(R.id.tagView)).setTags(secureElement.getTags(), true); } - @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..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.security.element.SecureElement; +import de.davis.passwordmanager.database.dtos.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..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.security.element.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 ec89b23a..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.security.element.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 810807a0..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 @@ -11,12 +11,12 @@ import com.google.android.material.appbar.MaterialToolbar; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.SecureElementManager; +import de.davis.passwordmanager.database.dtos.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; +import de.davis.passwordmanager.ui.views.TagView; public abstract class ViewSecureElementFragment extends SEViewFragment { @@ -46,16 +46,17 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void fillInElement(@NonNull SecureElement e){ + SecureElementManager.updateModifiedAt(e); 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; } @@ -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 c26c8f90..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 @@ -24,14 +24,15 @@ import java.util.TimerTask; import de.davis.passwordmanager.R; +import de.davis.passwordmanager.database.ElementType; +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; 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,8 +201,10 @@ protected SecureElement toElement() { CreditCardDetails details = new CreditCardDetails(name, expiryDate, creditCardNumber, cvv); SecureElement card = getElement() == null ? - SecureElement.createEmpty() : + new SecureElement(title, details) : getElement(); + + card.setTags(binding.tagView.getTags()); card.setTitle(title); card.setDetail(details); @@ -209,8 +212,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..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,14 +13,14 @@ import com.google.android.material.textfield.TextInputLayout; import de.davis.passwordmanager.R; +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; 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..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,13 +8,16 @@ 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; -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 { @@ -85,17 +88,19 @@ 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(details, title); + return new SecureElement(title, details, tags); + getElement().setTags(tags); getElement().setTitle(title); getElement().setDetail(details); return getElement(); } @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..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,8 +25,9 @@ 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.security.element.password.Strength; import de.davis.passwordmanager.utils.AssetsUtil; import de.davis.passwordmanager.utils.GeneratorUtil; import de.davis.passwordmanager.utils.TimeoutUtil; @@ -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/elements/password/ViewPasswordFragment.java b/app/src/main/java/de/davis/passwordmanager/ui/elements/password/ViewPasswordFragment.java index 6e0553cd..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 @@ -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.dtos.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; @@ -33,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(); @@ -53,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()); @@ -71,8 +73,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..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 @@ -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.dtos.SecureElement; import de.davis.passwordmanager.databinding.FragmentHighlightsBinding; -import de.davis.passwordmanager.security.element.SecureElement; +import de.davis.passwordmanager.ui.dashboard.viewholders.SecureElementViewHolder; import de.davis.passwordmanager.ui.viewmodels.HighlightsViewModel; public class HighlightsFragment extends Fragment { @@ -47,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)); - 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); }); @@ -56,25 +59,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 deleted file mode 100644 index ec7e74d6..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/viewmodels/DashboardViewModel.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.davis.passwordmanager.ui.viewmodels; - -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.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.KeyGoDatabase; -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 SavedStateHandle savedStateHandle; - - private final MutableLiveData> elements; - - - 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 +"%"); - }); - - 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(KeyGoDatabase.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..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 @@ -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.dtos.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..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 @@ -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.dtos.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/FilterBottomSheet.java b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.java deleted file mode 100644 index cc278ed4..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.java +++ /dev/null @@ -1,64 +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 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); - } - - @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); - - - updateSelection(strengthGroup, typeGroup); - - - typeGroup.setOnCheckedStateChangeListener((group, checkedIds) -> { - strength.setEnabled(checkedIds.contains(ID_PASSWORD)); - Filter.DEFAULT.update(); - }); - - strengthGroup.setOnCheckedStateChangeListener((group, checkedIds) -> Filter.DEFAULT.update()); - } - - private void updateSelection(ChipGroup... groups){ - List ids = Filter.DEFAULT.getSelectedIds(); - if(ids.isEmpty()) - return; - - for (ChipGroup group : groups) { - group.clearCheck(); - ids.forEach(group::check); - } - } -} 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 new file mode 100644 index 00000000..60a538c3 --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/FilterBottomSheet.kt @@ -0,0 +1,147 @@ +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.Chip +import de.davis.passwordmanager.R +import de.davis.passwordmanager.databinding.DialogFilterBinding +import de.davis.passwordmanager.databinding.LayoutChipBinding +import de.davis.passwordmanager.filter.Filter +import de.davis.passwordmanager.ktx.doFlowInLifecycle +import kotlinx.coroutines.flow.collectLatest + +class FilterBottomSheet : BottomSheetDialogFragment() { + + private lateinit var binding: DialogFilterBinding + + private val tags = mutableSetOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogFilterBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.typeGroup.setOnCheckedStateChangeListener { _, checkedIds -> + binding.strength.isEnabled = checkedIds.contains(R.id.password) + + 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) + } + } + + //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() + Filter.updateFilter { + tags.clear() + } + 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 + } + + 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 = 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 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.updateFilter { + if (isChecked) { + tags += text.toString() + } else + tags -= text.toString() + } + } + } + } +} \ No newline at end of file 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()) 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 7f953bea..00000000 --- a/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.java +++ /dev/null @@ -1,68 +0,0 @@ -package de.davis.passwordmanager.ui.views; - -import android.content.Context; -import android.os.Bundle; -import android.view.View; - -import com.google.android.material.bottomsheet.BottomSheetDialog; - -import de.davis.passwordmanager.R; -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; - - public OptionBottomSheet(Context context, SecureElement element) { - super(context); - this.element = element; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(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(); - }); - - - binding.favorite.setOnClickListener(v -> { - if(element == null) - return; - - SecureElementManager.getInstance().switchFavoriteState(element); - dismiss(); - }); - - if(element == null) { - binding.edit.setVisibility(View.GONE); - binding.favorite.setVisibility(View.GONE); - }else { - binding.favorite.setCompoundDrawablesRelativeWithIntrinsicBounds( - element.isFavorite() ? - 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.delete.setOnClickListener(v -> { - new DeleteDialog(getContext()).show(null, element); - 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..9e95102e --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/OptionBottomSheet.kt @@ -0,0 +1,197 @@ +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.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 +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 +import de.davis.passwordmanager.ktx.capitalize +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.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 { + dismiss() + EditDialogBuilder(requireContext()).apply { + setTitle(R.string.edit_tag) + setButtonListener( + DialogInterface.BUTTON_POSITIVE, + R.string.ok + ) { dialog, _, newText -> + if (!checkInput(newText, dialog as AlertDialog)) + return@setButtonListener + + if (newText == firstTag.tag.name) { + dialog.dismiss() + return@setButtonListener + } + + lifecycleScope.launch { + val updatedRows = + SecureElementManager.updateTag( + firstTag.tag.copy( + name = newText.trim().capitalize() + ) + ) + if (updatedRows != 0) { + dialog.dismiss() + 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.tag) + inputType = InputType.TYPE_CLASS_TEXT + isSecret = false + }) + }.show() + } + } + } + + 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]) + binding.title.text = title + + return title + } +} \ No newline at end of file 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..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,7 +25,8 @@ 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.EstimationHandler; +import de.davis.passwordmanager.database.entities.details.password.Strength; import de.davis.passwordmanager.utils.TimeoutUtil; public class PasswordStrengthBar extends LinearLayout implements TextWatcher { @@ -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); 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..d2b966ec --- /dev/null +++ b/app/src/main/java/de/davis/passwordmanager/ui/views/TagView.kt @@ -0,0 +1,239 @@ +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.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 +import de.davis.passwordmanager.ktx.capitalize +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 + } + } +} \ 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/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml index 059b652a..4cff320c 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" /> + + + + + + + + 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/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_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/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/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/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 diff --git a/app/src/main/res/navigation/element_nav_graph.xml b/app/src/main/res/navigation/element_nav_graph.xml index 10ab6b88..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" /> - 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 + + Tag + Tags + Tag-Layout + Dashboard gruppiert nach Tags + Dashboard im Listenformat + + 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 + Alle + Elemente: %d \ 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/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 2725554d..137a09a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,4 +193,20 @@ This might take a little time. Please be patient To load a backup, authentication is required Authenticate to proceed + + Tag + Tags + Tag Layout + Dashboard grouped by tags + Dashboard in list format + + 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 + All + Items: %d + \ No newline at end of file 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" /> + +