diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb951348..4c322898 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [Unreleased]
+
+### Added
+
+- Ability to have both file and seed phrase wallets in the app and switch between them
+
+### Removed
+
+- Ability to create new accounts and identities in a file wallet.
+ We recommend that you migrate to a seed phrase wallet
+ in order to make use of the full range of CryptoX features.
+
## [1.4.0] - 2024-12-16
### Added
diff --git a/app/build.gradle b/app/build.gradle
index c86a790f..226be8fc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -285,7 +285,7 @@ dependencies {
implementation 'androidx.browser:browser:1.4.0'
implementation 'com.github.Redman1037:TSnackBar:V2.0.0'
- implementation 'com.github.Dimezis:BlurView:version-2.0.3'
+ implementation 'com.github.Dimezis:BlurView:version-2.0.6'
// OkHttp/Retrofit
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
diff --git a/app/schemas/com.concordium.wallet.data.room.WalletDatabase/10.json b/app/schemas/com.concordium.wallet.data.room.WalletDatabase/10.json
new file mode 100644
index 00000000..556abb5b
--- /dev/null
+++ b/app/schemas/com.concordium.wallet.data.room.WalletDatabase/10.json
@@ -0,0 +1,499 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 10,
+ "identityHash": "8e6839ae303bff1c2c78e11537326637",
+ "entities": [
+ {
+ "tableName": "identity_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `status` TEXT NOT NULL, `detail` TEXT, `code_uri` TEXT NOT NULL, `next_account_number` INTEGER NOT NULL, `identity_provider` TEXT NOT NULL, `identity_object` TEXT, `private_id_object_data_encrypted` TEXT, `identity_provider_id` INTEGER NOT NULL, `identity_index` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detail",
+ "columnName": "detail",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "codeUri",
+ "columnName": "code_uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nextAccountNumber",
+ "columnName": "next_account_number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "identityProvider",
+ "columnName": "identity_provider",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "identityObject",
+ "columnName": "identity_object",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "privateIdObjectDataEncrypted",
+ "columnName": "private_id_object_data_encrypted",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "identityProviderId",
+ "columnName": "identity_provider_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "identityIndex",
+ "columnName": "identity_index",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_identity_table_identity_provider_id_identity_index",
+ "unique": true,
+ "columnNames": [
+ "identity_provider_id",
+ "identity_index"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_identity_table_identity_provider_id_identity_index` ON `${TABLE_NAME}` (`identity_provider_id`, `identity_index`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `identity_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `submission_id` TEXT NOT NULL, `transaction_status` INTEGER NOT NULL, `encrypted_account_data` TEXT, `credential` TEXT, `cred_number` INTEGER NOT NULL, `revealed_attributes` TEXT NOT NULL, `finalized_balance` TEXT NOT NULL, `balance_at_disposal` TEXT NOT NULL, `total_shielded_balance` TEXT NOT NULL, `finalized_encrypted_balance` TEXT, `current_balance_status` INTEGER NOT NULL, `read_only` INTEGER NOT NULL, `finalized_account_release_schedule` TEXT, `cooldowns` TEXT NOT NULL, `account_delegation` TEXT, `account_baker` TEXT, `accountIndex` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "identityId",
+ "columnName": "identity_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "submissionId",
+ "columnName": "submission_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionStatus",
+ "columnName": "transaction_status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedAccountData",
+ "columnName": "encrypted_account_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "credential",
+ "columnName": "credential",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "credNumber",
+ "columnName": "cred_number",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revealedAttributes",
+ "columnName": "revealed_attributes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "balance",
+ "columnName": "finalized_balance",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "balanceAtDisposal",
+ "columnName": "balance_at_disposal",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shieldedBalance",
+ "columnName": "total_shielded_balance",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedBalance",
+ "columnName": "finalized_encrypted_balance",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "encryptedBalanceStatus",
+ "columnName": "current_balance_status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readOnly",
+ "columnName": "read_only",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "releaseSchedule",
+ "columnName": "finalized_account_release_schedule",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "cooldowns",
+ "columnName": "cooldowns",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "delegation",
+ "columnName": "account_delegation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "baker",
+ "columnName": "account_baker",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "accountIndex",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_account_table_address",
+ "unique": true,
+ "columnNames": [
+ "address"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_account_table_address` ON `${TABLE_NAME}` (`address`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "transfer_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `amount` TEXT NOT NULL, `cost` TEXT NOT NULL, `from_address` TEXT NOT NULL, `to_address` TEXT NOT NULL, `expiry` INTEGER NOT NULL, `memo` TEXT, `created_at` INTEGER NOT NULL, `submission_id` TEXT NOT NULL, `transaction_status` INTEGER NOT NULL, `outcome` INTEGER NOT NULL, `transactionType` TEXT NOT NULL, `newSelfEncryptedAmount` TEXT, `newStartIndex` INTEGER NOT NULL, `nonce` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "cost",
+ "columnName": "cost",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fromAddress",
+ "columnName": "from_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "toAddress",
+ "columnName": "to_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expiry",
+ "columnName": "expiry",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "memo",
+ "columnName": "memo",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "submissionId",
+ "columnName": "submission_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionStatus",
+ "columnName": "transaction_status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "outcome",
+ "columnName": "outcome",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionType",
+ "columnName": "transactionType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "newSelfEncryptedAmount",
+ "columnName": "newSelfEncryptedAmount",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "newStartIndex",
+ "columnName": "newStartIndex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nonce",
+ "columnName": "nonce",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "recipient_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "encrypted_amount_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`encryptedkey` TEXT NOT NULL, `amount` TEXT, PRIMARY KEY(`encryptedkey`))",
+ "fields": [
+ {
+ "fieldPath": "encryptedkey",
+ "columnName": "encryptedkey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "encryptedkey"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "contract_token_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `contract_index` TEXT NOT NULL, `contract_name` TEXT NOT NULL DEFAULT '', `token_id` TEXT NOT NULL, `account_address` TEXT, `is_fungible` INTEGER NOT NULL, `token_metadata` TEXT, `is_newly_received` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contractIndex",
+ "columnName": "contract_index",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contractName",
+ "columnName": "contract_name",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountAddress",
+ "columnName": "account_address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFungible",
+ "columnName": "is_fungible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "tokenMetadata",
+ "columnName": "token_metadata",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isNewlyReceived",
+ "columnName": "is_newly_received",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_contract_token_table_contract_index_token_id_account_address",
+ "unique": true,
+ "columnNames": [
+ "contract_index",
+ "token_id",
+ "account_address"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_contract_token_table_contract_index_token_id_account_address` ON `${TABLE_NAME}` (`contract_index`, `token_id`, `account_address`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "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, '8e6839ae303bff1c2c78e11537326637')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.concordium.wallet.data.room.app.AppDatabase/1.json b/app/schemas/com.concordium.wallet.data.room.app.AppDatabase/1.json
new file mode 100644
index 00000000..0036a075
--- /dev/null
+++ b/app/schemas/com.concordium.wallet.data.room.app.AppDatabase/1.json
@@ -0,0 +1,71 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "57f1ba95820dc5b52f6358ac3399a98c",
+ "entities": [
+ {
+ "tableName": "wallets",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `is_active` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isActive",
+ "columnName": "is_active",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_wallets_created_at",
+ "unique": false,
+ "columnNames": [
+ "created_at"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_created_at` ON `${TABLE_NAME}` (`created_at`)"
+ },
+ {
+ "name": "index_wallets_is_active",
+ "unique": false,
+ "columnNames": [
+ "is_active"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_wallets_is_active` ON `${TABLE_NAME}` (`is_active`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "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, '57f1ba95820dc5b52f6358ac3399a98c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperTest.kt b/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperTest.kt
new file mode 100644
index 00000000..41ea4e41
--- /dev/null
+++ b/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperTest.kt
@@ -0,0 +1,230 @@
+package com.concordium.wallet.core.security
+
+import com.concordium.wallet.data.model.EncryptedData
+import com.concordium.wallet.util.toHex
+import kotlinx.coroutines.runBlocking
+import okio.ByteString.Companion.decodeHex
+import org.junit.Assert
+import org.junit.Test
+
+class EncryptionHelperTest {
+ private val sharedKey = "12345678912345678912345678912345".decodeHex().toByteArray()
+
+ @Test
+ fun encryptSuccessfully() {
+ val data = "This should be kept in secret 🤫".toByteArray()
+ val (ed1, ed2) = runBlocking {
+ List(2) {
+ EncryptionHelper.encrypt(
+ key = sharedKey,
+ data = data,
+ ).also(::println)
+ }
+ }
+
+ Assert.assertNotEquals(
+ "Cipher text must not be repeated",
+ ed1.ciphertext, ed2.ciphertext
+ )
+ Assert.assertNotEquals(
+ "IV must not be reused",
+ ed1.iv, ed2.iv
+ )
+ Assert.assertEquals(
+ "Transformation must remain constant",
+ ed1.transformation, ed2.transformation
+ )
+ Assert.assertEquals("AES/GCM/NoPadding", ed1.transformation)
+ }
+
+ @Test
+ fun encryptSuccessfully_IfNoData() {
+ val ed = runBlocking {
+ EncryptionHelper.encrypt(
+ key = sharedKey,
+ data = byteArrayOf(),
+ ).also(::println)
+ }
+
+ Assert.assertEquals(
+ "Only the 128 bit tag must be present in the cipher text",
+ 128 / 8, ed.decodeCiphertext().size
+ )
+ }
+
+ @Test
+ fun decryptSuccessfully() {
+ val ed = EncryptedData(
+ ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovePjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4",
+ transformation = "AES/GCM/NoPadding",
+ iv = "QkQj9YofBqR7actQ",
+ )
+
+ val data = runBlocking {
+ EncryptionHelper.decrypt(
+ key = sharedKey,
+ encryptedData = ed,
+ )
+ }
+
+ Assert.assertEquals(
+ "This should be kept in secret 🤫",
+ String(data)
+ )
+ }
+
+ @Test
+ fun decryptSuccessfully_IfLegacyTransformation() {
+ val ed = EncryptedData(
+ ciphertext = "P1ZQet2bjxGmpGkzKOM680jgtnA+UOAo5ZYB3q7mwW7euZxMwwGF/yGtNm6zBYK3",
+ transformation = "AES/CBC/PKCS7Padding",
+ iv = "92b5AYQVTTr0fQXgmUAIAQ",
+ )
+
+ val data = runBlocking {
+ EncryptionHelper.decrypt(
+ key = sharedKey,
+ encryptedData = ed,
+ )
+ }
+
+ Assert.assertEquals(
+ "This should be kept in secret 🤫",
+ String(data)
+ )
+ }
+
+ @Test(expected = EncryptionException::class)
+ fun failToDecrypt_IfDifferentKey() {
+ val ed = EncryptedData(
+ ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovePjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4",
+ transformation = "AES/GCM/NoPadding",
+ iv = "QkQj9YofBqR7actQ",
+ )
+
+ runBlocking {
+ EncryptionHelper.decrypt(
+ key = byteArrayOf(1, 2, 3),
+ encryptedData = ed,
+ )
+ }
+ }
+
+ @Test(expected = EncryptionException::class)
+ fun failToDecrypt_IfDifferentIv() {
+ val ed = EncryptedData(
+ ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovePjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4",
+ transformation = "AES/GCM/NoPadding",
+ iv = "QkQj9YofCdR7actQ",
+ )
+
+ runBlocking {
+ EncryptionHelper.decrypt(
+ key = sharedKey,
+ encryptedData = ed,
+ )
+ }
+ }
+
+ @Test(expected = EncryptionException::class)
+ fun failToDecrypt_IfAlteredCipherText() {
+ val ed = EncryptedData(
+ ciphertext = "YQQvlr6O/Wr5YnLAS1wlNql6P/ovmPjGe6ebH5tchLQ6Z9jz6aFAFONCktsFGihxlg4",
+ transformation = "AES/GCM/NoPadding",
+ iv = "QkQj9YofBqR7actQ",
+ )
+
+ runBlocking {
+ EncryptionHelper.decrypt(
+ key = sharedKey,
+ encryptedData = ed,
+ )
+ }
+ }
+
+ @Test
+ fun generateKeySuccessfully() {
+ val (mc1, mc2) = runBlocking {
+ List(2) {
+ EncryptionHelper.generateKey().toHex().also(::println)
+ }
+ }
+
+ Assert.assertNotEquals(
+ "Generated keys must not be repeated",
+ mc1, mc2
+ )
+ Assert.assertEquals(
+ "Generated key size must remain constant",
+ mc1.length, mc2.length
+ )
+ Assert.assertTrue(
+ "Generated keys must be strong",
+ mc1.length >= 64,
+ )
+ }
+
+ @Test
+ fun generatePasswordKeySaltSuccessfully() {
+ val (s1, s2) = runBlocking {
+ List(2) {
+ EncryptionHelper.generatePasswordKeySalt().toHex().also(::println)
+ }
+ }
+
+ Assert.assertNotEquals(
+ "Generated salt must not be repeated",
+ s1, s2
+ )
+ }
+
+ @Test
+ fun generatePasswordKeySuccessfully() {
+ val p1 = "123456".toCharArray()
+ val p2 = "qwe123".toCharArray()
+ val s1 = "7804836792fbecf04961fe286e8ec950d2e105448e61a290262711154ec59026".decodeHex().toByteArray()
+ val s2 = "ac6e5196afd67b0e69c42fcba198420391b79a6ba9a916fa76a905f09017acc5".decodeHex().toByteArray()
+
+ val (k1, k2) = runBlocking {
+ List(2) {
+ EncryptionHelper.generatePasswordKey(
+ password = p1,
+ salt = s1,
+ ).toHex().also(::println)
+ }
+ }
+
+ Assert.assertEquals(
+ "Generated keys must remain constant if the inputs are constant",
+ k1, k2
+ )
+ Assert.assertTrue(
+ "Generated keys must be strong",
+ k1.length >= 64
+ )
+
+ val k1s2 = runBlocking {
+ EncryptionHelper.generatePasswordKey(
+ password = p1,
+ salt = s2,
+ ).toHex()
+ }
+
+ Assert.assertNotEquals(
+ "Keys generated with the same password but different salt must be different",
+ k1, k1s2
+ )
+
+ val k2s1 = runBlocking {
+ EncryptionHelper.generatePasswordKey(
+ password = p2,
+ salt = s1,
+ ).toHex()
+ }
+
+ Assert.assertNotEquals(
+ "Keys generated with the same salt but different passwords must be different",
+ k1, k2s1
+ )
+ }
+}
diff --git a/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperUnitTest.kt b/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperUnitTest.kt
deleted file mode 100644
index 5b1b9daf..00000000
--- a/app/src/androidTest/java/com/concordium/wallet/core/security/EncryptionHelperUnitTest.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.concordium.wallet.core.security
-
-import android.util.Base64
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class EncryptionHelperUnitTest {
- @Test
- fun encryption() {
- val password = "123"
- val text = "some_text"
-
- val (salt, iv) = EncryptionHelper.createEncryptionData()
- val decodedEncrypted = EncryptionHelper.encrypt(password, salt, iv, text)
-
- val toBeDecryptedByteArray = Base64.decode(decodedEncrypted, Base64.DEFAULT)
- val decrypted = EncryptionHelper.decrypt(password, salt, iv, toBeDecryptedByteArray)
-
- // decrypting the encrypted gives the original
- assertEquals(text, decrypted)
-
- // Same result every time
- val decodedEncrypted2 = EncryptionHelper.encrypt(password, salt, iv, text)
- assertEquals(decodedEncrypted, decodedEncrypted2)
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f292c8a5..701f7b65 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -207,35 +207,18 @@
android:theme="@style/CCX_Screen"
android:windowSoftInputMode="stateAlwaysVisible|adjustResize" />
-
-
-
@@ -562,6 +545,11 @@
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/CCX_Screen"/>
+
diff --git a/app/src/main/java/com/concordium/wallet/App.kt b/app/src/main/java/com/concordium/wallet/App.kt
index 39ba6922..292b1232 100644
--- a/app/src/main/java/com/concordium/wallet/App.kt
+++ b/app/src/main/java/com/concordium/wallet/App.kt
@@ -2,6 +2,7 @@ package com.concordium.wallet
import android.app.Application
import android.content.Context
+import com.concordium.wallet.core.AppCore
import com.concordium.wallet.core.notifications.AnnouncementNotificationManager
import com.concordium.wallet.data.backend.ws.WsCreds
import com.concordium.wallet.util.Log
@@ -39,7 +40,7 @@ class App : Application() {
}
fun initAppCore() {
- appCore = AppCore(this.applicationContext)
+ appCore = AppCore(this@App)
}
private fun initWalletConnect() {
diff --git a/app/src/main/java/com/concordium/wallet/AppCore.kt b/app/src/main/java/com/concordium/wallet/AppCore.kt
deleted file mode 100644
index 0def63f7..00000000
--- a/app/src/main/java/com/concordium/wallet/AppCore.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.concordium.wallet
-
-import android.content.Context
-import com.concordium.wallet.core.authentication.AuthenticationManager
-import com.concordium.wallet.core.authentication.Session
-import com.concordium.wallet.core.crypto.CryptoLibrary
-import com.concordium.wallet.core.crypto.CryptoLibraryReal
-import com.concordium.wallet.core.gson.BigIntegerTypeAdapter
-import com.concordium.wallet.core.gson.RawJsonTypeAdapter
-import com.concordium.wallet.core.tracking.AppTracker
-import com.concordium.wallet.core.tracking.MatomoAppTracker
-import com.concordium.wallet.core.tracking.NoOpAppTracker
-import com.concordium.wallet.data.backend.ProxyBackend
-import com.concordium.wallet.data.backend.ProxyBackendConfig
-import com.concordium.wallet.data.backend.airdrop.AirDropBackend
-import com.concordium.wallet.data.backend.airdrop.AirDropBackendConfig
-import com.concordium.wallet.data.backend.news.NewsfeedRssBackend
-import com.concordium.wallet.data.backend.news.NewsfeedRssBackendConfig
-import com.concordium.wallet.data.backend.notifications.NotificationsBackend
-import com.concordium.wallet.data.backend.notifications.NotificationsBackendConfig
-import com.concordium.wallet.data.backend.tokens.TokensBackend
-import com.concordium.wallet.data.backend.tokens.TokensBackendConfig
-import com.concordium.wallet.data.model.RawJson
-import com.concordium.wallet.data.preferences.TrackingPreferences
-import com.concordium.wallet.data.room.Identity
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import org.matomo.sdk.Matomo
-import org.matomo.sdk.TrackerBuilder
-import java.math.BigInteger
-
-class AppCore(val context: Context) {
-
- val gson: Gson = initializeGson()
- val proxyBackendConfig = ProxyBackendConfig(gson)
- private val tokenBackendConfig = TokensBackendConfig(gson)
- private val airdropBackendConfig = AirDropBackendConfig(gson)
- private val newsfeedRssBackendConfig: NewsfeedRssBackendConfig by lazy(::NewsfeedRssBackendConfig)
- private val notificationsBackendConfig: NotificationsBackendConfig =
- NotificationsBackendConfig(gson)
- val cryptoLibrary: CryptoLibrary = CryptoLibraryReal(gson)
-
- private val trackingPreferences = TrackingPreferences(context)
- private val noOpAppTracker: AppTracker = NoOpAppTracker()
- private val matomoAppTracker: AppTracker by lazy {
- TrackerBuilder.createDefault("https://concordium.matomo.cloud/matomo.php", 8)
- .build(Matomo.getInstance(context))
- .let(::MatomoAppTracker)
- }
- val tracker: AppTracker
- get() =
- if (trackingPreferences.isTrackingEnabled)
- matomoAppTracker
- else
- noOpAppTracker
-
- val session: Session = Session(App.appContext)
- var newIdentities = mutableMapOf()
-
- private val authenticationManagerGeneric: AuthenticationManager =
- AuthenticationManager(session.getBiometricAuthKeyName())
- private var authenticationManagerReset: AuthenticationManager = authenticationManagerGeneric
- private var authenticationManager: AuthenticationManager = authenticationManagerGeneric
- private var resetBiometricKeyNameAppendix: String = ""
-
- fun getNotificationsBackend(): NotificationsBackend {
- return notificationsBackendConfig.backend
- }
-
- fun getNewsfeedRssBackend(): NewsfeedRssBackend {
- return newsfeedRssBackendConfig.backend
- }
-
- fun getTokensBackend(): TokensBackend {
- return tokenBackendConfig.backend
- }
-
- fun getAirdropBackend(): AirDropBackend {
- return airdropBackendConfig.backend
- }
-
- fun getProxyBackend(): ProxyBackend {
- return proxyBackendConfig.backend
- }
-
- private fun initializeGson(): Gson {
- val gsonBuilder = GsonBuilder()
- gsonBuilder.registerTypeAdapter(RawJson::class.java, RawJsonTypeAdapter())
- gsonBuilder.registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter())
- return gsonBuilder.create()
- }
-
- fun getOriginalAuthenticationManager(): AuthenticationManager {
- return authenticationManagerReset
- }
-
- fun getCurrentAuthenticationManager(): AuthenticationManager {
- return authenticationManager
- }
-
- fun startResetAuthFlow() {
- resetBiometricKeyNameAppendix = System.currentTimeMillis().toString()
- authenticationManagerReset = AuthenticationManager(resetBiometricKeyNameAppendix)
- authenticationManager = authenticationManagerReset
- }
-
- fun finalizeResetAuthFlow() {
- session.setBiometricAuthKeyName(resetBiometricKeyNameAppendix)
- session.hasFinishedSetupPassword()
- }
-
- fun cancelResetAuthFlow() {
- authenticationManagerReset = authenticationManagerGeneric
- authenticationManager = authenticationManagerReset
- session.hasFinishedSetupPassword()
- }
-}
diff --git a/app/src/main/java/com/concordium/wallet/core/AppAuth.kt b/app/src/main/java/com/concordium/wallet/core/AppAuth.kt
new file mode 100644
index 00000000..9834cb7a
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/AppAuth.kt
@@ -0,0 +1,289 @@
+package com.concordium.wallet.core
+
+import com.concordium.wallet.core.security.EncryptionException
+import com.concordium.wallet.core.security.EncryptionHelper
+import com.concordium.wallet.core.security.KeystoreEncryptionException
+import com.concordium.wallet.core.security.KeystoreHelper
+import com.concordium.wallet.data.model.EncryptedData
+import com.concordium.wallet.data.preferences.AppSetupPreferences
+import com.concordium.wallet.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.crypto.BadPaddingException
+import javax.crypto.Cipher
+import javax.crypto.IllegalBlockSizeException
+
+/**
+ * Handles app-wide password auth and encryption.
+ *
+ * In the app, all the encryption is done with a single master key.
+ * The master key is stored encrypted with a key derived from the password.
+ * If the biometric auth is set up, the password is also stored encrypted
+ * with a key from Android keystore and a dedicated cipher for biometrics.
+ *
+ * When the password is changed, only the master key gets re-encrypted
+ * as well as the encrypted password for biometrics, if set up.
+ * But all the encrypted data remains intact.
+ *
+ * @param slot identifier of a slot to read/write the auth data into.
+ * Use AuthenticationManagers with different slots to safely update the auth
+ * in the same way the A/B flashing works in smartphones.
+ *
+ * @see commitCurrentSlot to save changes done in a given slot
+ */
+class AppAuth(
+ private val appSetupPreferences: AppSetupPreferences,
+ private val slot: String = appSetupPreferences.getCurrentAuthSlot(),
+) {
+ // region Biometrics
+
+ fun initBiometricAuth(
+ password: String,
+ cipher: Cipher,
+ ): Boolean {
+ try {
+ appSetupPreferences.setEncryptedPassword(
+ slot,
+ EncryptedData(
+ ciphertext = cipher.doFinal(password.toByteArray()),
+ iv = cipher.iv,
+ transformation = cipher.algorithm,
+ )
+ )
+ appSetupPreferences.setUseBiometrics(slot, true)
+ return true
+ } catch (e: java.lang.Exception) {
+ when (e) {
+ is BadPaddingException,
+ is IllegalBlockSizeException,
+ -> {
+ Log.e("Failed to encrypt the data with the generated key. ${e.message}")
+ return false
+ }
+
+ else -> throw e
+ }
+ }
+ }
+
+ fun generateBiometricsSecretKey(): Boolean {
+ return try {
+ KeystoreHelper().generateSecretKey(slot)
+ true
+ } catch (e: KeystoreEncryptionException) {
+ false
+ }
+ }
+
+ /**
+ * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException
+ */
+ fun getBiometricsCipherForEncryption(): Cipher? {
+ return KeystoreHelper().initCipherForEncryption(slot)
+ }
+
+ /**
+ * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException
+ */
+ fun getBiometricsCipherForDecryption(): Cipher? {
+ try {
+ val cipher = KeystoreHelper().initCipherForDecryption(
+ keyName = slot,
+ initVector = appSetupPreferences.getEncryptedPassword(slot).decodeIv(),
+ )
+
+ if (cipher == null) {
+ appSetupPreferences.setUseBiometrics(slot, false)
+ }
+
+ return cipher
+ } catch (e: KeystoreEncryptionException) {
+ appSetupPreferences.setUseBiometrics(slot, false)
+ throw e
+ }
+ }
+
+ suspend fun decryptPasswordWithBiometricsCipher(
+ cipher: Cipher,
+ ): String? = withContext(Dispatchers.Default) {
+ runCatching {
+ val encryptedPassword = appSetupPreferences.getEncryptedPassword(slot)
+ cipher.doFinal(encryptedPassword.decodeCiphertext())
+ }
+ .getOrNull()
+ ?.let(::String)
+ }
+
+ //endregion
+
+ /**
+ * Initializes the password auth generating a new encryption master key.
+ * Use this method to init the auth for the first time.
+ */
+ @Throws(EncryptionException::class)
+ suspend fun initPasswordAuth(
+ password: String,
+ isPasscode: Boolean,
+ ) =
+ initPasswordAuth(
+ password = password,
+ isPasscode = isPasscode,
+ masterKey = EncryptionHelper.generateKey(),
+ )
+
+ /**
+ * Initializes the password auth reusing the existing master key.
+ * Use this method in combination with the current master key
+ * to change the existing password.
+ */
+ @Throws(EncryptionException::class)
+ suspend fun initPasswordAuth(
+ password: String,
+ isPasscode: Boolean,
+ masterKey: ByteArray,
+ ) {
+ val passwordKeySalt = EncryptionHelper.generatePasswordKeySalt()
+ val passwordKey = EncryptionHelper.generatePasswordKey(
+ password = password.toCharArray(),
+ salt = passwordKeySalt,
+ )
+ val encryptedMasterKey: EncryptedData = EncryptionHelper.encrypt(
+ key = passwordKey,
+ data = masterKey,
+ )
+ appSetupPreferences.setPasswordKeySalt(slot, passwordKeySalt)
+ appSetupPreferences.setEncryptedMasterKey(slot, encryptedMasterKey)
+ appSetupPreferences.setUsePasscode(slot, isPasscode)
+ }
+
+ /**
+ * Saves the current slot as the main one,
+ * therefore committing all the changes done in this slot.
+ */
+ fun commitCurrentSlot() {
+ appSetupPreferences.setCurrentAuthSlot(slot)
+ }
+
+ @Throws(EncryptionException::class)
+ suspend fun getMasterKey(
+ password: String,
+ ): ByteArray {
+ val passwordKey = EncryptionHelper.generatePasswordKey(
+ password = password.toCharArray(),
+ salt = appSetupPreferences.getPasswordKeySalt(slot),
+ )
+ return getMasterKey(passwordKey)
+ }
+
+ @Throws(EncryptionException::class)
+ private suspend fun getMasterKey(
+ passwordKey: ByteArray,
+ ): ByteArray {
+ val encryptedMasterKey = appSetupPreferences.getEncryptedMasterKey(slot)
+ return EncryptionHelper.decrypt(
+ key = passwordKey,
+ encryptedData = encryptedMasterKey
+ )
+ }
+
+ suspend fun checkPassword(password: String): Boolean {
+ val passwordKey = EncryptionHelper.generatePasswordKey(
+ password = password.toCharArray(),
+ salt = appSetupPreferences.getPasswordKeySalt(slot),
+ )
+
+ if (runCatching { getMasterKey(passwordKey) }.getOrNull() != null) {
+ return true
+ }
+
+ return checkPasswordLegacySavingMasterKey(passwordKey)
+ }
+
+ private suspend fun checkPasswordLegacySavingMasterKey(passwordKey: ByteArray): Boolean {
+ // Legacy password check is a string and its encrypted representation,
+ // which must be compared once decrypted.
+ // It was in place before the Two wallets feature.
+
+ val legacyPasswordCheck = appSetupPreferences.getLegacyPasswordCheck(slot)
+ ?: return false
+ val legacyEncryptedPasswordCheck = appSetupPreferences.getLegacyEncryptedPasswordCheck(slot)
+ ?: return false
+ val legacyDecryptedPasswordCheck = runCatching {
+ EncryptionHelper.decrypt(
+ key = passwordKey,
+ encryptedData = legacyEncryptedPasswordCheck,
+ )
+ }.getOrNull() ?: return false
+
+ if (String(legacyDecryptedPasswordCheck) == legacyPasswordCheck) {
+ // If the password is correct, then the current master key is the password key.
+ // This was the approach to the encryption before the Two wallets feature.
+ val encryptedMasterKey: EncryptedData = EncryptionHelper.encrypt(
+ key = passwordKey,
+ data = passwordKey,
+ )
+ appSetupPreferences.setEncryptedMasterKey(slot, encryptedMasterKey)
+ } else {
+ return false
+ }
+ return String(legacyDecryptedPasswordCheck) == legacyPasswordCheck
+ }
+
+ suspend fun encrypt(
+ password: String,
+ data: ByteArray,
+ ): EncryptedData? =
+ runCatching {
+ encrypt(
+ masterKey = getMasterKey(password),
+ data = data,
+ )
+ }.getOrNull()
+
+ suspend fun encrypt(
+ masterKey: ByteArray,
+ data: ByteArray,
+ ): EncryptedData? =
+ runCatching {
+ EncryptionHelper.encrypt(
+ key = masterKey,
+ data = data,
+ )
+ }.getOrNull()
+
+ suspend fun decrypt(
+ password: String,
+ encryptedData: EncryptedData,
+ ): ByteArray? =
+ runCatching {
+ decrypt(
+ masterKey = getMasterKey(password),
+ encryptedData = encryptedData,
+ )
+ }.getOrNull()
+
+ suspend fun decrypt(
+ masterKey: ByteArray,
+ encryptedData: EncryptedData,
+ ): ByteArray? =
+ runCatching {
+ EncryptionHelper.decrypt(
+ key = masterKey,
+ encryptedData = encryptedData,
+ )
+ }.getOrNull()
+
+ fun isPasswordAuthInitialized(): Boolean {
+ return (appSetupPreferences.hasEncryptedMasterKey(slot)
+ || appSetupPreferences.getLegacyPasswordCheck(slot) != null)
+ && appSetupPreferences.hasPasswordKeySalt(slot)
+ }
+
+ fun isPasscodeUsed(): Boolean {
+ return appSetupPreferences.getUsePasscode(slot)
+ }
+
+ fun isBiometricsUsed(): Boolean {
+ return appSetupPreferences.getUseBiometrics(slot)
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/AppCore.kt b/app/src/main/java/com/concordium/wallet/core/AppCore.kt
new file mode 100644
index 00000000..f20fc880
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/AppCore.kt
@@ -0,0 +1,148 @@
+package com.concordium.wallet.core
+
+import android.os.Handler
+import com.concordium.wallet.App
+import com.concordium.wallet.core.crypto.CryptoLibrary
+import com.concordium.wallet.core.crypto.CryptoLibraryReal
+import com.concordium.wallet.core.gson.BigIntegerTypeAdapter
+import com.concordium.wallet.core.gson.RawJsonTypeAdapter
+import com.concordium.wallet.core.migration.TwoWalletsMigration
+import com.concordium.wallet.core.multiwallet.AppWallet
+import com.concordium.wallet.core.tracking.AppTracker
+import com.concordium.wallet.core.tracking.MatomoAppTracker
+import com.concordium.wallet.core.tracking.NoOpAppTracker
+import com.concordium.wallet.data.AppWalletRepository
+import com.concordium.wallet.data.backend.ProxyBackend
+import com.concordium.wallet.data.backend.ProxyBackendConfig
+import com.concordium.wallet.data.backend.airdrop.AirDropBackend
+import com.concordium.wallet.data.backend.airdrop.AirDropBackendConfig
+import com.concordium.wallet.data.backend.news.NewsfeedRssBackend
+import com.concordium.wallet.data.backend.news.NewsfeedRssBackendConfig
+import com.concordium.wallet.data.backend.notifications.NotificationsBackend
+import com.concordium.wallet.data.backend.notifications.NotificationsBackendConfig
+import com.concordium.wallet.data.backend.tokens.TokensBackend
+import com.concordium.wallet.data.backend.tokens.TokensBackendConfig
+import com.concordium.wallet.data.model.RawJson
+import com.concordium.wallet.data.preferences.AppSetupPreferences
+import com.concordium.wallet.data.preferences.AppTrackingPreferences
+import com.concordium.wallet.data.room.app.AppDatabase
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.runBlocking
+import org.matomo.sdk.Matomo
+import org.matomo.sdk.TrackerBuilder
+import java.math.BigInteger
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class AppCore(val app: App) {
+
+ val gson: Gson = getGson()
+ val proxyBackendConfig = ProxyBackendConfig(gson)
+ private val tokenBackendConfig = TokensBackendConfig(gson)
+ private val airdropBackendConfig = AirDropBackendConfig(gson)
+ private val newsfeedRssBackendConfig: NewsfeedRssBackendConfig by lazy(::NewsfeedRssBackendConfig)
+ private val notificationsBackendConfig: NotificationsBackendConfig =
+ NotificationsBackendConfig(gson)
+ val cryptoLibrary: CryptoLibrary = CryptoLibraryReal(gson)
+ private val appTrackingPreferences = AppTrackingPreferences(App.appContext)
+ private val noOpAppTracker: AppTracker = NoOpAppTracker()
+ private val matomoAppTracker: AppTracker by lazy {
+ TrackerBuilder.createDefault("https://concordium.matomo.cloud/matomo.php", 8)
+ .build(Matomo.getInstance(App.appContext))
+ .let(::MatomoAppTracker)
+ }
+ val tracker: AppTracker
+ get() =
+ if (appTrackingPreferences.isTrackingEnabled)
+ matomoAppTracker
+ else
+ noOpAppTracker
+
+ // Migrations are invoked once vital core components are initialized:
+ // Gson, crypto lib, etc.
+ init {
+ with(TwoWalletsMigration(app, gson)) {
+ if (isPreferenceMigrationNeeded()) {
+ migratePreferencesOnce()
+ }
+
+ runBlocking {
+ if (isAppDatabaseMigrationNeeded()) {
+ migrateAppDatabaseOnce()
+ }
+ }
+ }
+ }
+
+ val database = AppDatabase.getDatabase(app)
+ val walletRepository = AppWalletRepository(database.appWalletDao())
+
+ var session: Session =
+ runBlocking {
+ Session(
+ context = app,
+ activeWallet = walletRepository.getActiveWallet(),
+ )
+ }
+ private set
+
+ val setup = AppSetup(
+ appSetupPreferences = AppSetupPreferences(
+ context = App.appContext,
+ gson = gson,
+ ),
+ getSession = { session },
+ )
+ val auth: AppAuth
+ get() = setup.auth
+
+ fun getNotificationsBackend(): NotificationsBackend {
+ return notificationsBackendConfig.backend
+ }
+
+ fun getNewsfeedRssBackend(): NewsfeedRssBackend {
+ return newsfeedRssBackendConfig.backend
+ }
+
+ fun getTokensBackend(): TokensBackend {
+ return tokenBackendConfig.backend
+ }
+
+ fun getAirdropBackend(): AirDropBackend {
+ return airdropBackendConfig.backend
+ }
+
+ fun getProxyBackend(): ProxyBackend {
+ return proxyBackendConfig.backend
+ }
+
+ suspend fun startNewSession(
+ activeWallet: AppWallet,
+ isLoggedIn: Boolean = session.isLoggedIn.value == true,
+ ) = suspendCoroutine { continuation ->
+ // Session must be created in the main thread as it contains logout timer.
+ Handler(app.mainLooper).post {
+ continuation.context.ensureActive()
+
+ session.inactivityCountDownTimer.cancel()
+ session = Session(
+ context = app,
+ activeWallet = activeWallet,
+ isLoggedIn = isLoggedIn,
+ )
+
+ continuation.resume(Unit)
+ }
+ }
+
+ companion object {
+ fun getGson(): Gson {
+ val gsonBuilder = GsonBuilder()
+ gsonBuilder.registerTypeAdapter(RawJson::class.java, RawJsonTypeAdapter())
+ gsonBuilder.registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter())
+ return gsonBuilder.create()
+ }
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/AppSetup.kt b/app/src/main/java/com/concordium/wallet/core/AppSetup.kt
new file mode 100644
index 00000000..2283c246
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/AppSetup.kt
@@ -0,0 +1,66 @@
+package com.concordium.wallet.core
+
+import com.concordium.wallet.data.preferences.AppSetupPreferences
+
+class AppSetup(
+ private val appSetupPreferences: AppSetupPreferences,
+ private val getSession: () -> Session,
+) {
+ var auth = AppAuth(appSetupPreferences)
+ private set
+ private var oldAuth = auth
+ var authSetupPassword: String? = null
+ private set
+ var authResetMasterKey: ByteArray? = null
+ private set
+ val isInitialSetupCompleted: Boolean
+ get() = appSetupPreferences.getHasCompletedInitialSetup()
+ val isAuthSetupCompleted: Boolean
+ get() = auth.isPasswordAuthInitialized()
+
+ fun finishInitialSetup() {
+ appSetupPreferences.setHasCompletedInitialSetup(true)
+ }
+
+ fun beginAuthSetup(password: String) {
+ authSetupPassword = password
+ }
+
+ fun finishAuthSetup() {
+ authSetupPassword = null
+ getSession().setUserLoggedIn()
+ }
+
+ /**
+ * Allows manipulating the [auth] without making the change permanent
+ * until [commitAuthReset] is called. In case of failure, call [cancelAuthReset].
+ *
+ * @param decryptedMasterKey to be used for during the reset, see [authResetMasterKey]
+ */
+ fun beginAuthReset(decryptedMasterKey: ByteArray) {
+ // Back up the current auth manager and replace it
+ // with the one with an alternative slot.
+ oldAuth = auth
+ auth = AppAuth(
+ appSetupPreferences = appSetupPreferences,
+ slot = System.currentTimeMillis().toString()
+ )
+ // Store the decrypted master key in memory
+ // to re-init the auth with it later.
+ this.authResetMasterKey = decryptedMasterKey
+ }
+
+ fun commitAuthReset() {
+ authResetMasterKey = null
+ // Save the current (alternative) slot and continue using it.
+ auth.commitCurrentSlot()
+ finishAuthSetup()
+ }
+
+ fun cancelAuthReset() {
+ authResetMasterKey = null
+ // Restore the auth manager discarding the alternative slot.
+ auth = oldAuth
+ finishAuthSetup()
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/Session.kt b/app/src/main/java/com/concordium/wallet/core/Session.kt
new file mode 100644
index 00000000..3b3fbcc4
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/Session.kt
@@ -0,0 +1,93 @@
+package com.concordium.wallet.core
+
+import android.content.Context
+import android.os.CountDownTimer
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.concordium.wallet.core.multiwallet.AppWallet
+import com.concordium.wallet.data.WalletStorage
+import com.concordium.wallet.data.preferences.Preferences
+import com.concordium.wallet.data.room.Identity
+
+class Session(
+ context: Context,
+ val activeWallet: AppWallet,
+ isLoggedIn: Boolean = false,
+) {
+ val walletStorage = WalletStorage(
+ activeWallet = activeWallet,
+ context = context,
+ )
+ var newIdentities = mutableMapOf()
+
+ private val _isLoggedIn = MutableLiveData(isLoggedIn)
+ val isLoggedIn: LiveData
+ get() = _isLoggedIn
+
+ // The notice must be shown once per app start.
+ private var isUnshieldingNoticeShown = false
+
+ fun setHasShowRewards(id: Int, value: Boolean) {
+ walletStorage.filterPreferences.setHasShowRewards(id, value)
+ }
+
+ fun getHasShowRewards(id: Int): Boolean {
+ return walletStorage.filterPreferences.getHasShowRewards(id)
+ }
+
+ fun setHasShowFinalizationRewards(id: Int, value: Boolean) {
+ walletStorage.filterPreferences.setHasShowFinalizationRewards(id, value)
+ }
+
+ fun getHasShowFinalizationRewards(id: Int): Boolean {
+ return walletStorage.filterPreferences.getHasShowFinalizationRewards(id)
+ }
+
+ fun setUnshieldingNoticeShown() {
+ isUnshieldingNoticeShown = true
+ }
+
+ fun isUnshieldingNoticeShown(): Boolean =
+ isUnshieldingNoticeShown
+
+ fun setUserLoggedIn() {
+ _isLoggedIn.value = true
+ resetLogoutTimeout()
+ }
+
+ fun resetLogoutTimeout() {
+ if (_isLoggedIn.value!!) {
+ inactivityCountDownTimer.cancel()
+ inactivityCountDownTimer.start()
+ }
+ }
+
+ var inactivityCountDownTimer =
+ object : CountDownTimer(60 * 5 * 1000.toLong(), 1000) {
+ override fun onTick(millisUntilFinished: Long) {}
+
+ override fun onFinish() {
+ _isLoggedIn.value = false
+ }
+ }
+
+ fun isAccountsBackupPossible(): Boolean {
+ return activeWallet.type == AppWallet.Type.FILE
+ }
+
+ fun areAccountsBackedUp(): Boolean {
+ return walletStorage.setupPreferences.areAccountsBackedUp()
+ }
+
+ fun setAccountsBackedUp(value: Boolean) {
+ return walletStorage.setupPreferences.setAccountsBackedUp(value)
+ }
+
+ fun addAccountsBackedUpListener(listener: Preferences.Listener) {
+ walletStorage.setupPreferences.addAccountsBackedUpListener(listener)
+ }
+
+ fun removeAccountsBackedUpListener(listener: Preferences.Listener) {
+ walletStorage.setupPreferences.removeListener(listener)
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/authentication/AuthenticationManager.kt b/app/src/main/java/com/concordium/wallet/core/authentication/AuthenticationManager.kt
deleted file mode 100644
index 68703f5c..00000000
--- a/app/src/main/java/com/concordium/wallet/core/authentication/AuthenticationManager.kt
+++ /dev/null
@@ -1,263 +0,0 @@
-package com.concordium.wallet.core.authentication
-
-import android.util.Base64
-import com.concordium.wallet.App
-import com.concordium.wallet.core.security.EncryptionException
-import com.concordium.wallet.core.security.EncryptionHelper
-import com.concordium.wallet.core.security.KeystoreEncryptionException
-import com.concordium.wallet.core.security.KeystoreHelper
-import com.concordium.wallet.data.preferences.AuthPreferences
-import com.concordium.wallet.util.Log
-import com.concordium.wallet.util.RandomUtil
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import javax.crypto.BadPaddingException
-import javax.crypto.Cipher
-import javax.crypto.IllegalBlockSizeException
-import javax.crypto.SecretKey
-
-class AuthenticationManager(biometricKeyName: String) {
-
- private val t = this.javaClass.simpleName
-
- private val biometricKeyName: String = biometricKeyName
- private val authPreferences = AuthPreferences(App.appContext)
-
- //region Biometrics
-
- fun setupBiometrics(password: String, cipher: Cipher): Boolean {
- try {
- val initVector = cipher.iv
- val encrypted = cipher.doFinal(password.toByteArray())
- val encodedEncryptedPassword = Base64.encodeToString(encrypted, Base64.DEFAULT)
- authPreferences.setEncryptedPassword(biometricKeyName, encodedEncryptedPassword)
- authPreferences.setEncryptedPasswordDerivedKeyInitVector(
- biometricKeyName,
- Base64.encodeToString(
- initVector,
- Base64.DEFAULT
- )
- )
- authPreferences.setUseBiometrics(biometricKeyName, true)
- return true
- } catch (e: java.lang.Exception) {
- when (e) {
- is BadPaddingException,
- is IllegalBlockSizeException -> {
- Log.e("Failed to encrypt the data with the generated key. ${e.message}")
- return false
- }
- else -> throw e
- }
- }
- }
-
- fun generateBiometricsSecretKey(): Boolean {
- return try {
- KeystoreHelper().generateSecretKey(biometricKeyName)
- true
- } catch (e: KeystoreEncryptionException) {
- false
- }
- }
-
- /**
- * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException
- */
- fun initBiometricsCipherForEncryption(): Cipher? {
- return KeystoreHelper().initCipherForEncryption(biometricKeyName)
- }
-
- /**
- * Can throw KeystoreEncryptionException or return null in case of KeyPermanentlyInvalidatedException
- */
- fun initBiometricsCipherForDecryption(): Cipher? {
- try {
- val initVector = authPreferences.getBiometricsKeyEncryptionInitVector(biometricKeyName)
- val cipher = KeystoreHelper().initCipherForDecryption(
- biometricKeyName,
- Base64.decode(initVector, Base64.DEFAULT)
- )
-
- if (cipher == null) {
- authPreferences.setUseBiometrics(biometricKeyName, false)
- }
- return cipher
- } catch (e: KeystoreEncryptionException) {
- authPreferences.setUseBiometrics(biometricKeyName, false)
- throw e
- }
- }
-
- //endregion
-
- fun createPasswordCheck(password: String): Boolean {
- // Create encryption data used for encryption and decryption with password derived key
- try {
- val (salt, iv) = EncryptionHelper.createEncryptionData()
- // Save encryption data
- val encodedSalt = Base64.encodeToString(salt, Base64.DEFAULT)
- val encodedIV = Base64.encodeToString(iv, Base64.DEFAULT)
- authPreferences.setPasswordEncryptionSalt(biometricKeyName, encodedSalt)
- authPreferences.setPasswordEncryptionInitVector(biometricKeyName, encodedIV)
-
- val passwordCheck = RandomUtil.randomString(20)
- Log.d("PasswordCheck: $passwordCheck")
-
- // Derive password key and encrypt password check
- val result = EncryptionHelper.encrypt(password, salt, iv, passwordCheck)
- authPreferences.setPasswordCheckEncrypted(biometricKeyName, result)
- authPreferences.setPasswordCheck(biometricKeyName, passwordCheck)
- return true
- } catch (e: EncryptionException) {
- return false
- }
- }
-
- fun checkPassword(password: String): Boolean {
- Log.i("$t#checkPassword -> password: $password")
- val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName)
- Log.i("$t#checkPassword -> encodedSalt: $encodedSalt")
- val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName)
- Log.i("$t#checkPassword -> encodedIV: $encodedIV")
- val encodedPasswordCheckEncrypted = authPreferences.getPasswordCheckEncrypted(biometricKeyName)
- Log.i("$t#checkPassword -> encodedPasswordCheckEncrypted: $encodedPasswordCheckEncrypted")
- val salt = Base64.decode(encodedSalt, Base64.DEFAULT)
- Log.i("$t#checkPassword -> salt: $salt")
- val iv = Base64.decode(encodedIV, Base64.DEFAULT)
- Log.i("$t#checkPassword -> iv: $iv")
- val passwordCheckEncrypted = Base64.decode(encodedPasswordCheckEncrypted, Base64.DEFAULT)
- Log.i("$t#checkPassword -> passwordCheckEncrypted: $passwordCheckEncrypted")
- // Derive password key and decrypt password check
- try {
- val decryptedString =
- EncryptionHelper.decrypt(password, salt, iv, passwordCheckEncrypted)
- val savedPasswordCheck = authPreferences.getPasswordCheck(biometricKeyName)
- return decryptedString.equals(savedPasswordCheck)
- } catch (e: EncryptionException) {
- return false
- }
- }
-
- suspend fun checkPasswordInBackground(password: String): Boolean = withContext(Dispatchers.Default) {
- return@withContext checkPassword(password)
- }
-
- suspend fun checkPasswordInBackground(cipher: Cipher): String? =
- withContext(Dispatchers.Default) {
- try {
- val encodedEncryptedPassword = getEncryptedPassword()
- var encryptedPassword = Base64.decode(encodedEncryptedPassword, Base64.DEFAULT)
-
- val decryptedByteArray = cipher.doFinal(encryptedPassword)
- val decryptedPasswordString = String(decryptedByteArray, charset("UTF-8"))
-
- val res = checkPassword(decryptedPasswordString)
- return@withContext if (res) {
- decryptedPasswordString
- } else {
- null
- }
- } catch (e: Exception) {
- when (e) {
- is BadPaddingException,
- is IllegalBlockSizeException -> {
- Log.e("Failed to decrypt the data with the generated key. ${e.message}")
- return@withContext null
- }
- else -> throw e
- }
- }
- }
-
- suspend fun encryptInBackground(password: String, toBeEncrypted: String): String? =
- withContext(Dispatchers.Default) {
- val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName)
- val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName)
- val salt = Base64.decode(encodedSalt, Base64.DEFAULT)
- val iv = Base64.decode(encodedIV, Base64.DEFAULT)
-
- // Derive password key and encrypt
- try {
- val encodedEncrypted = EncryptionHelper.encrypt(password, salt, iv, toBeEncrypted)
- return@withContext encodedEncrypted
- } catch (e: EncryptionException) {
- return@withContext null
- }
- }
-
- suspend fun decryptInBackground(password: String, encodedToBeDecrypted: String): String? =
- withContext(Dispatchers.Default) {
- val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName)
- val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName)
- val salt = Base64.decode(encodedSalt, Base64.DEFAULT)
- val iv = Base64.decode(encodedIV, Base64.DEFAULT)
-
- // Derive password key and decrypt
- val toBeDecryptedByteArray = Base64.decode(encodedToBeDecrypted, Base64.DEFAULT)
- try {
- val decrypted = EncryptionHelper.decrypt(password, salt, iv, toBeDecryptedByteArray)
- return@withContext decrypted
- } catch (e: EncryptionException) {
- return@withContext null
- }
- }
-
- suspend fun derivePasswordKeyInBackground(password: String): SecretKey? = withContext(Dispatchers.Default) {
- Log.i("$t#derivePasswordKeyInBackground: password -> $password")
- val encodedSalt = authPreferences.getPasswordEncryptionSalt(biometricKeyName)
- Log.i("$t#derivePasswordKeyInBackground: encodedSalt -> $encodedSalt")
- val salt = Base64.decode(encodedSalt, Base64.DEFAULT)
- Log.i("$t#derivePasswordKeyInBackground: salt -> $salt")
- // Derive password key
- try {
- val key = EncryptionHelper.generateKey(password, salt)
- return@withContext key
- } catch (e: EncryptionException) {
- return@withContext null
- }
- }
-
- suspend fun encryptInBackground(key: SecretKey, toBeEncrypted: String): String? = withContext(Dispatchers.Default) {
- val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName)
- val iv = Base64.decode(encodedIV, Base64.DEFAULT)
-
- // Derive password key and encrypt
- try {
- val encodedEncrypted = EncryptionHelper.encrypt(key, iv, toBeEncrypted)
- return@withContext encodedEncrypted
- } catch (e: EncryptionException) {
- return@withContext null
- }
- }
-
- suspend fun decryptInBackground(key: SecretKey, encodedToBeDecrypted: String): String? = withContext(Dispatchers.Default) {
- val encodedIV = authPreferences.getPasswordEncryptionInitVector(biometricKeyName)
- val iv = Base64.decode(encodedIV, Base64.DEFAULT)
-
- // Derive password key and decrypt
- val toBeDecryptedByteArray = Base64.decode(encodedToBeDecrypted, Base64.DEFAULT)
- try {
- val decrypted = EncryptionHelper.decrypt(key, iv, toBeDecryptedByteArray)
- return@withContext decrypted
- } catch (e: EncryptionException) {
- return@withContext null
- }
- }
-
- fun usePasscode(): Boolean {
- return authPreferences.getUsePasscode(biometricKeyName)
- }
-
- fun useBiometrics(): Boolean {
- return authPreferences.getUseBiometrics(biometricKeyName)
- }
-
- fun getEncryptedPassword(): String {
- return authPreferences.getEncryptedPassword(biometricKeyName)
- }
-
- fun setUsePassCode(passcodeUsed: Boolean) {
- authPreferences.setUsePasscode(biometricKeyName, passcodeUsed)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/concordium/wallet/core/authentication/Session.kt b/app/src/main/java/com/concordium/wallet/core/authentication/Session.kt
deleted file mode 100644
index 06cc35fc..00000000
--- a/app/src/main/java/com/concordium/wallet/core/authentication/Session.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.concordium.wallet.core.authentication
-
-import android.content.Context
-import android.os.CountDownTimer
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import com.concordium.wallet.App
-import com.concordium.wallet.data.preferences.AuthPreferences
-import com.concordium.wallet.data.preferences.FilterPreferences
-import com.concordium.wallet.data.preferences.Preferences
-
-class Session {
-
- private var authPreferences: AuthPreferences
- private var filterPreferences: FilterPreferences
-
- var hasSetupPassword = false
- private set
-
- var tempPassword: String? = null
- private set
-
- /**
- * The value is positive at the fresh app start until the setup start screen is visited.
- */
- var hasCompletedInitialSetup = true
- private set
-
- var hasCompleteOnboarding = false
- private set
-
- private val _isLoggedIn = MutableLiveData(false)
- val isLoggedIn: LiveData
- get() = _isLoggedIn
-
- // The notice must be shown once per app start.
- private var isUnshieldingNoticeShown = false
-
- constructor(context: Context) {
- authPreferences = AuthPreferences(context)
- hasSetupPassword = authPreferences.getHasSetupUser()
- hasCompletedInitialSetup = authPreferences.getHasCompletedInitialSetup()
- hasCompleteOnboarding = authPreferences.getHasCompletedOnboarding()
- filterPreferences = FilterPreferences(context)
- }
-
- fun setHasShowRewards(id: Int, value: Boolean) {
- filterPreferences.setHasShowRewards(id, value)
- }
-
- fun getHasShowRewards(id: Int): Boolean {
- return filterPreferences.getHasShowRewards(id)
- }
-
- fun setHasShowFinalizationRewards(id: Int, value: Boolean) {
- filterPreferences.setHasShowFinalizationRewards(id, value)
- }
-
- fun getHasShowFinalizationRewards(id: Int): Boolean {
- return filterPreferences.getHasShowFinalizationRewards(id)
- }
-
- fun unshieldingNoticeShown() {
- isUnshieldingNoticeShown = true
- }
-
- fun isUnshieldingNoticeShown(): Boolean =
- isUnshieldingNoticeShown
-
- fun hasSetupPassword(passcodeUsed: Boolean = false) {
- _isLoggedIn.value = true
- authPreferences.setHasSetupUser(true)
- App.appCore.getCurrentAuthenticationManager().setUsePassCode(passcodeUsed)
- hasSetupPassword = true
- }
-
- fun hasFinishedSetupPassword() {
- tempPassword = null
- }
-
- fun startedInitialSetup() {
- authPreferences.setHasCompletedInitialSetup(false)
- hasCompletedInitialSetup = false
- }
-
- fun hasCompletedInitialSetup() {
- authPreferences.setHasCompletedInitialSetup(true)
- hasCompletedInitialSetup = true
- }
-
- fun hasCompletedOnboarding() {
- authPreferences.setHasCompletedOnboarding(true)
- hasCompleteOnboarding = true
- }
-
- fun setHasShowedInitialAnimation() {
- authPreferences.setHasShowedInitialAnimation(true)
- }
-
- fun getHasShowedInitialAnimation(): Boolean {
- return authPreferences.getShowedInitialAnimation()
- }
-
- fun startPasswordSetup(password: String) {
- tempPassword = password
- }
-
- fun checkPassword(password: String): Boolean {
- return password.equals(tempPassword)
- }
-
- fun getPasswordToSetUp(): String? = tempPassword
-
- fun hasLoggedInUser() {
- _isLoggedIn.value = true
- resetLogoutTimeout()
- }
-
- fun resetLogoutTimeout() {
- if (_isLoggedIn.value!!) {
- inactivityCountDownTimer.cancel()
- inactivityCountDownTimer.start()
- }
- }
-
- var inactivityCountDownTimer =
- object : CountDownTimer(60 * 5 * 1000.toLong(), 1000) {
- override fun onTick(millisUntilFinished: Long) {}
-
- override fun onFinish() {
- _isLoggedIn.value = false
- }
- }
-
- fun getBiometricAuthKeyName(): String {
- return authPreferences.getAuthKeyName()
- }
-
- fun setBiometricAuthKeyName(resetBiometricKeyNameAppendix: String) {
- authPreferences.setAuthKeyName(resetBiometricKeyNameAppendix)
- }
-
- fun isAccountsBackupPossible(): Boolean {
- return !authPreferences.hasEncryptedSeed()
- }
-
- fun isAccountsBackedUp(): Boolean {
- return authPreferences.isAccountsBackedUp()
- }
-
- fun setAccountsBackedUp(value: Boolean) {
- return authPreferences.setAccountsBackedUp(value)
- }
-
- fun addAccountsBackedUpListener(listener: Preferences.Listener) {
- authPreferences.addAccountsBackedUpListener(listener)
- }
-
- fun removeAccountsBackedUpListener(listener: Preferences.Listener) {
- authPreferences.removeListener(listener)
- }
-}
diff --git a/app/src/main/java/com/concordium/wallet/core/migration/TwoWalletsMigration.kt b/app/src/main/java/com/concordium/wallet/core/migration/TwoWalletsMigration.kt
new file mode 100644
index 00000000..83373725
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/migration/TwoWalletsMigration.kt
@@ -0,0 +1,211 @@
+package com.concordium.wallet.core.migration
+
+import android.content.Context
+import android.util.Base64
+import com.concordium.wallet.core.multiwallet.AppWallet
+import com.concordium.wallet.core.security.KeystoreHelper
+import com.concordium.wallet.data.model.EncryptedData
+import com.concordium.wallet.data.preferences.AppSetupPreferences
+import com.concordium.wallet.data.preferences.Preferences
+import com.concordium.wallet.data.preferences.WalletSetupPreferences
+import com.concordium.wallet.data.room.WalletDatabase
+import com.concordium.wallet.data.room.app.AppDatabase
+import com.concordium.wallet.data.room.app.AppWalletDao
+import com.concordium.wallet.data.room.app.AppWalletEntity
+import com.google.gson.Gson
+import java.io.File
+
+class TwoWalletsMigration(
+ private val context: Context,
+ private val gson: Gson,
+) {
+ private val oldAuthPreferences: OldAuthPreferences by lazy {
+ OldAuthPreferences(context)
+ }
+
+ private val oldEncryptionIv: ByteArray? by lazy {
+ oldAuthPreferences.passwordEncryptionInitVectorBase64
+ ?.let(::decodeOldBase64)
+ }
+
+ private val appWalletDao: AppWalletDao by lazy {
+ AppDatabase.getDatabase(context).appWalletDao()
+ }
+
+ fun migrateOldEncryptedData(oldEncryptedDataBase64: String) =
+ EncryptedData(
+ ciphertext = decodeOldBase64(oldEncryptedDataBase64),
+ iv = checkNotNull(oldEncryptionIv) {
+ "Can't migrate old encrypted data due to the missing old encryption IV"
+ },
+ transformation = OLD_ENCRYPTION_TRANSFORMATION,
+ )
+
+ fun isPreferenceMigrationNeeded(): Boolean =
+ File(context.dataDir, "/shared_prefs/$OLD_AUTH_PREFERENCES_FILE.xml").exists()
+ && !oldAuthPreferences.areMigrated
+
+ @Suppress("DEPRECATION")
+ fun migratePreferencesOnce() {
+ check(isPreferenceMigrationNeeded()) {
+ "The migration is not needed"
+ }
+
+ val newAppSetupPreferences = AppSetupPreferences(
+ context = context,
+ gson = gson,
+ )
+ val newWalletSetupPreferences = WalletSetupPreferences(
+ context = context,
+ gson = gson,
+ )
+
+ // Auth.
+ val authSlot = oldAuthPreferences.authKeyName
+ newAppSetupPreferences.setCurrentAuthSlot(authSlot)
+ newAppSetupPreferences.setUsePasscode(authSlot, oldAuthPreferences.usePasscode)
+ newAppSetupPreferences.setUseBiometrics(authSlot, oldAuthPreferences.useBiometrics)
+ oldAuthPreferences.passwordEncryptionSaltBase64
+ ?.let(::decodeOldBase64)
+ ?.also { newAppSetupPreferences.setPasswordKeySalt(authSlot, it) }
+ oldAuthPreferences.encryptedPasswordBase64
+ ?.let(::decodeOldBase64)
+ ?.also { encryptedPassword ->
+ val encryptedPasswordIv = oldAuthPreferences.encryptedPasswordIvBase64
+ ?.let(::decodeOldBase64)
+ ?: error("Can't save the encrypted password: missing IV")
+ newAppSetupPreferences.setEncryptedPassword(
+ authSlot,
+ EncryptedData(
+ ciphertext = encryptedPassword,
+ iv = encryptedPasswordIv,
+ transformation = KeystoreHelper.ENCRYPTION_CIPHER_TRANSFORMATION,
+ )
+ )
+ }
+ oldAuthPreferences.passwordCheck
+ ?.also { newAppSetupPreferences.setLegacyPasswordCheck(authSlot, it) }
+ oldAuthPreferences.encryptedPasswordCheckBase64
+ ?.let(::migrateOldEncryptedData)
+ ?.also { newAppSetupPreferences.setLegacyEncryptedPasswordCheck(authSlot, it) }
+
+ // App setup.
+ newAppSetupPreferences.setHasCompletedInitialSetup(oldAuthPreferences.hasCompletedInitialSetup)
+
+ // Wallet setup.
+ newWalletSetupPreferences.setHasCompletedOnboarding(oldAuthPreferences.hasCompletedOnboarding)
+ newWalletSetupPreferences.setAccountsBackedUp(oldAuthPreferences.areAccountsBackedUp)
+ newWalletSetupPreferences.setHasShownInitialAnimation(oldAuthPreferences.hasShownInitialAnimation)
+ oldAuthPreferences.encryptedSeedEntropyHexBase64
+ ?.let(::migrateOldEncryptedData)
+ ?.also { newEncryptedSeedEntropyHex ->
+ check(
+ newWalletSetupPreferences.tryToSetEncryptedSeedPhrase(newEncryptedSeedEntropyHex)
+ ) { "Failed setting encrypted seed phrase" }
+ }
+ oldAuthPreferences.encryptedSeedHexBase64
+ ?.let(::migrateOldEncryptedData)
+ ?.also { newEncryptedSeedHex ->
+ check(
+ newWalletSetupPreferences.tryToSetEncryptedSeedHex(newEncryptedSeedHex)
+ ) { "Failed setting encrypted seed" }
+ }
+
+ // Finalize.
+ oldAuthPreferences.areMigrated = true
+ }
+
+ suspend fun isAppDatabaseMigrationNeeded(): Boolean =
+ // The migration is needed when updated to the "two wallets" release
+ // as well as when the app is just installed so the DB must be prepared.
+ appWalletDao.getCount() == 0
+
+ @Suppress("DEPRECATION")
+ suspend fun migrateAppDatabaseOnce() {
+ check(isAppDatabaseMigrationNeeded()) {
+ "The migration is not needed"
+ }
+
+ val primaryWalletDatabase = WalletDatabase.getDatabase(context)
+ val primaryWalletType = when {
+ primaryWalletDatabase.identityDao().getCount() > 0
+ && oldAuthPreferences.encryptedSeedHexBase64 == null
+ && oldAuthPreferences.encryptedSeedEntropyHexBase64 == null ->
+ AppWallet.Type.FILE
+
+ else ->
+ AppWallet.Type.SEED
+ }
+
+ // Creation of the first (primary) wallet.
+ appWalletDao.insertAndActivate(
+ AppWalletEntity(
+ wallet = AppWallet.primary(
+ type = primaryWalletType,
+ )
+ )
+ )
+ }
+
+ private class OldAuthPreferences(
+ context: Context,
+ ) : Preferences(context, OLD_AUTH_PREFERENCES_FILE) {
+
+ val authKeyName: String
+ get() = getString("PREFKEY_BIOMETRIC_KEY", "default_key")
+
+ val passwordEncryptionInitVectorBase64: String?
+ get() = getString("PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR$authKeyName")
+
+ val passwordEncryptionSaltBase64: String?
+ get() = getString("PREFKEY_PASSWORD_ENCRYPTION_SALT$authKeyName")
+
+ val usePasscode: Boolean
+ get() = getBoolean("PREFKEY_USE_PASSCODE$authKeyName", false)
+
+ val useBiometrics: Boolean
+ get() = getBoolean("PREFKEY_USE_BIOMETRICS$authKeyName", false)
+
+ val passwordCheck: String?
+ get() = getString("PREFKEY_PASSWORD_CHECK$authKeyName")
+
+ val encryptedPasswordCheckBase64: String?
+ get() = getString("PREFKEY_PASSWORD_CHECK_ENCRYPTED$authKeyName")
+
+ val encryptedPasswordBase64: String?
+ get() = getString("PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY$authKeyName")
+
+ val encryptedPasswordIvBase64: String?
+ get() = getString("PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR$authKeyName")
+
+ val hasCompletedInitialSetup: Boolean
+ get() = getBoolean("PREFKEY_HAS_COMPLETED_INITIAL_SETUP", true)
+
+ val hasCompletedOnboarding: Boolean
+ get() = getBoolean("PREFKEY_HAS_COMPLETED_ONBOARDING", false)
+
+ val areAccountsBackedUp: Boolean
+ get() = getBoolean("PREFKEY_ACCOUNTS_BACKED_UP", true)
+
+ val encryptedSeedEntropyHexBase64: String?
+ get() = getString("PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX")
+
+ val encryptedSeedHexBase64: String?
+ get() = getString("SEED_PHRASE_ENCRYPTED")
+
+ val hasShownInitialAnimation: Boolean
+ get() = getBoolean("PREFKEY_HAS_SHOWED_INITIAL_ANIMATION", false)
+
+ var areMigrated: Boolean
+ get() = getBoolean("MIGRATED", false)
+ set(value) = setBoolean("MIGRATED", value)
+ }
+
+ companion object {
+ private const val OLD_ENCRYPTION_TRANSFORMATION = "AES/CBC/PKCS7Padding"
+ private const val OLD_AUTH_PREFERENCES_FILE = "PREF_FILE_AUTH"
+
+ private fun decodeOldBase64(encoded: String): ByteArray =
+ Base64.decode(encoded, Base64.DEFAULT)
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/AddAndActivateWalletUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/AddAndActivateWalletUseCase.kt
new file mode 100644
index 00000000..3b7c2fbe
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/AddAndActivateWalletUseCase.kt
@@ -0,0 +1,29 @@
+package com.concordium.wallet.core.multiwallet
+
+import com.concordium.wallet.App
+import com.concordium.wallet.core.AppCore
+
+class AddAndActivateWalletUseCase {
+ /**
+ * Adds and activates an extra wallet of the given [walletType].
+ * Once the wallet is added, a new session is started with it.
+ * On completion, the main screen must be re-started.
+ *
+ * @return ID if the added wallet
+ *
+ * @see AppCore.startNewSession
+ */
+ suspend operator fun invoke(
+ walletType: AppWallet.Type,
+ ): String {
+ val newWallet = AppWallet.extra(
+ type = walletType,
+ )
+
+ App.appCore.walletRepository.addWallet(newWallet)
+ App.appCore.startNewSession(
+ activeWallet = newWallet,
+ )
+ return newWallet.id
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/AppWallet.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/AppWallet.kt
new file mode 100644
index 00000000..cf8f0eba
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/AppWallet.kt
@@ -0,0 +1,58 @@
+package com.concordium.wallet.core.multiwallet
+
+import com.concordium.wallet.data.room.app.AppWalletEntity
+import java.util.Date
+
+class AppWallet
+private constructor(
+ val id: String,
+ val type: Type,
+ val createdAt: Date,
+) {
+ constructor(entity: AppWalletEntity) : this(
+ id = entity.id,
+ type = Type.valueOf(entity.type),
+ createdAt = Date(entity.createdAt),
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is AppWallet) return false
+
+ if (id != other.id) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return id.hashCode()
+ }
+
+ enum class Type {
+ FILE,
+ SEED,
+ ;
+ }
+
+ companion object {
+ fun primary(
+ type: Type,
+ ) = AppWallet(
+ id = "",
+ type = type,
+ createdAt = Date(0),
+ )
+
+ fun extra(
+ type: Type,
+ ): AppWallet {
+ val now = Date()
+
+ return AppWallet(
+ id = now.time.toString(),
+ type = type,
+ createdAt = now,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/DeleteActiveWalletUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/DeleteActiveWalletUseCase.kt
new file mode 100644
index 00000000..9e21d8a7
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/DeleteActiveWalletUseCase.kt
@@ -0,0 +1,30 @@
+package com.concordium.wallet.core.multiwallet
+
+import com.concordium.wallet.App
+
+class DeleteActiveWalletUseCase {
+ /**
+ * Deletes the current active wallet and switches it to the [newActiveWallet].
+ * Once the [newActiveWallet] is activated, a new session is started with it.
+ * On completion, the main screen must be re-started.
+ *
+ * @see SwitchActiveWalletUseCase
+ */
+ suspend operator fun invoke(
+ newActiveWallet: AppWallet,
+ ) {
+ check(App.appCore.walletRepository.getWallets().size > 1) {
+ "Can't delete the only wallet"
+ }
+
+ App.appCore.walletRepository.delete(
+ walletToDeleteId = App.appCore.session.activeWallet.id,
+ walletToActivateId = newActiveWallet.id,
+ )
+ App.appCore.session.walletStorage.erase()
+
+ SwitchActiveWalletUseCase().invoke(
+ newActiveWallet = newActiveWallet,
+ )
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletTypeUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletTypeUseCase.kt
new file mode 100644
index 00000000..c1eb669f
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletTypeUseCase.kt
@@ -0,0 +1,45 @@
+package com.concordium.wallet.core.multiwallet
+
+import com.concordium.wallet.App
+import com.concordium.wallet.core.AppCore
+import com.concordium.wallet.data.AccountRepository
+import com.concordium.wallet.data.IdentityRepository
+import com.concordium.wallet.util.Log
+
+class SwitchActiveWalletTypeUseCase {
+
+ /**
+ * Switches the current active wallet type to the given [newWalletType],
+ * which is done when setting up the first wallet.
+ * Once the wallet type is switched, a new session is started with it.
+ *
+ * @see AppCore.startNewSession
+ */
+ suspend operator fun invoke(
+ newWalletType: AppWallet.Type,
+ ) {
+ val activeWallet = App.appCore.session.activeWallet
+
+ if (activeWallet.type == newWalletType) {
+ Log.d("The active wallet is already $newWalletType, skipping")
+ return
+ }
+
+ val allAccounts =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao()).getAll()
+ val allIdentities =
+ IdentityRepository(App.appCore.session.walletStorage.database.identityDao()).getAll()
+
+ check(allAccounts.isEmpty() && allIdentities.isEmpty()) {
+ "Can't switch the wallet type when it is not empty"
+ }
+
+ App.appCore.walletRepository.switchWalletType(
+ walletId = activeWallet.id,
+ newType = newWalletType,
+ )
+ App.appCore.startNewSession(
+ activeWallet = App.appCore.walletRepository.getActiveWallet(),
+ )
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletUseCase.kt b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletUseCase.kt
new file mode 100644
index 00000000..a3fddd3f
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/core/multiwallet/SwitchActiveWalletUseCase.kt
@@ -0,0 +1,22 @@
+package com.concordium.wallet.core.multiwallet
+
+import com.concordium.wallet.App
+import com.concordium.wallet.core.AppCore
+
+class SwitchActiveWalletUseCase {
+ /**
+ * Switches the active wallet to the given [newActiveWallet].
+ * Once the wallet is activated, a new session is started with it.
+ * On completion, the main screen must be re-started.
+ *
+ * @see AppCore.startNewSession
+ */
+ suspend operator fun invoke(
+ newActiveWallet: AppWallet,
+ ) {
+ App.appCore.walletRepository.activate(newActiveWallet)
+ App.appCore.startNewSession(
+ activeWallet = newActiveWallet,
+ )
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt b/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt
index 0ce815e6..262eef8d 100644
--- a/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt
+++ b/app/src/main/java/com/concordium/wallet/core/notifications/FcmNotificationsService.kt
@@ -6,7 +6,6 @@ import com.concordium.wallet.data.ContractTokensRepository
import com.concordium.wallet.data.cryptolib.ContractAddress
import com.concordium.wallet.data.model.Token
import com.concordium.wallet.data.room.ContractToken
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.cis2.retrofit.MetadataApiInstance
import com.concordium.wallet.util.Log
import com.concordium.wallet.util.toBigInteger
@@ -20,23 +19,6 @@ import kotlinx.coroutines.runBlocking
class FcmNotificationsService : FirebaseMessagingService() {
private val serviceCoroutineContext = Dispatchers.IO + SupervisorJob()
- private val announcementNotificationManager: AnnouncementNotificationManager by lazy {
- AnnouncementNotificationManager(application)
- }
- private val transactionNotificationsManager: TransactionNotificationsManager by lazy {
- TransactionNotificationsManager(application)
- }
- private val accountRepository: AccountRepository by lazy {
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- AccountRepository(accountDao)
- }
- private val contractTokensRepository: ContractTokensRepository by lazy {
- val contractTokenDao = WalletDatabase.getDatabase(application).contractTokenDao()
- ContractTokensRepository(contractTokenDao)
- }
- private val updateNotificationsSubscriptionUseCase by lazy {
- UpdateNotificationsSubscriptionUseCase(application)
- }
override fun onNewToken(token: String) = runBlocking(serviceCoroutineContext) {
Log.d(
@@ -45,9 +27,9 @@ class FcmNotificationsService : FirebaseMessagingService() {
)
try {
- updateNotificationsSubscriptionUseCase()
+ UpdateNotificationsSubscriptionUseCase().invoke()
Log.d("trying update subscriptions with new token")
- } catch (error: Exception){
+ } catch (error: Exception) {
Log.e("failed_updating_subscriptions", error)
}
}
@@ -115,7 +97,7 @@ class FcmNotificationsService : FirebaseMessagingService() {
"\nmessageId=$messageId"
)
- announcementNotificationManager.notifyAnnouncement(
+ AnnouncementNotificationManager(application).notifyAnnouncement(
title = notificationTitle,
text = notificationBody,
reference = messageId ?: System.currentTimeMillis(),
@@ -137,10 +119,12 @@ class FcmNotificationsService : FirebaseMessagingService() {
val recipientAccountAddress = data["recipient"]
?: error("Recipient is missing or invalid")
- val recipientAccount = accountRepository.findByAddress(recipientAccountAddress)
- ?: error("Recipient account not found in the wallet: $recipientAccountAddress")
+ val recipientAccount =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
+ .findByAddress(recipientAccountAddress)
+ ?: error("Recipient account not found in the wallet: $recipientAccountAddress")
- transactionNotificationsManager.notifyCcdTransaction(
+ TransactionNotificationsManager(application).notifyCcdTransaction(
receivedAmount = amount,
account = recipientAccount,
reference = data["reference"] ?: System.currentTimeMillis(),
@@ -162,8 +146,10 @@ class FcmNotificationsService : FirebaseMessagingService() {
val recipientAccountAddress = data["recipient"]
?: error("Recipient is missing or invalid")
- val recipientAccount = accountRepository.findByAddress(recipientAccountAddress)
- ?: error("Recipient account not found in the wallet: $recipientAccountAddress")
+ val recipientAccount =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
+ .findByAddress(recipientAccountAddress)
+ ?: error("Recipient account not found in the wallet: $recipientAccountAddress")
val contractAddress: ContractAddress = data["contract_address"]
?.let { App.appCore.gson.fromJson(it, ContractAddress::class.java) }
@@ -177,6 +163,9 @@ class FcmNotificationsService : FirebaseMessagingService() {
?: error("tokenMetadata is missing or invalid")
Log.d("tokenMetadata: $tokenMetadata")
+ val contractTokensRepository =
+ ContractTokensRepository(App.appCore.session.walletStorage.database.contractTokenDao())
+
val existingContractToken = contractTokensRepository.find(
accountAddress = recipientAccountAddress,
contractIndex = contractAddress.index.toString(),
@@ -228,7 +217,7 @@ class FcmNotificationsService : FirebaseMessagingService() {
token = Token(existingContractToken)
}
- transactionNotificationsManager.notifyCis2Transaction(
+ TransactionNotificationsManager(application).notifyCis2Transaction(
receivedAmount = amount,
token = token,
account = recipientAccount,
diff --git a/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt b/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt
index a656cd7f..c60f77f6 100644
--- a/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt
+++ b/app/src/main/java/com/concordium/wallet/core/notifications/TransactionNotificationsManager.kt
@@ -8,9 +8,10 @@ import android.content.Intent
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import com.concordium.wallet.App
import com.concordium.wallet.R
import com.concordium.wallet.data.model.Token
-import com.concordium.wallet.data.preferences.NotificationsPreferences
+import com.concordium.wallet.data.preferences.WalletNotificationsPreferences
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.util.CurrencyUtil
import com.concordium.wallet.ui.account.accountdetails.AccountDetailsActivity
@@ -24,18 +25,18 @@ class TransactionNotificationsManager(
NotificationManagerCompat.from(context)
}
- private val notificationsPreferences: NotificationsPreferences by lazy {
- NotificationsPreferences(context)
+ private val walletNotificationsPreferences: WalletNotificationsPreferences by lazy {
+ App.appCore.session.walletStorage.notificationsPreferences
}
private val areNotificationsEnabled: Boolean
get() = notificationsManager.areNotificationsEnabled()
private val areCcdTxNotificationsEnabled: Boolean
- get() = areNotificationsEnabled && notificationsPreferences.areCcdTxNotificationsEnabled
+ get() = areNotificationsEnabled && walletNotificationsPreferences.areCcdTxNotificationsEnabled
private val areCis2TxNotificationsEnabled: Boolean
- get() = areNotificationsEnabled && notificationsPreferences.areCis2TxNotificationsEnabled
+ get() = areNotificationsEnabled && walletNotificationsPreferences.areCis2TxNotificationsEnabled
@SuppressLint("MissingPermission")
fun notifyCcdTransaction(
diff --git a/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt b/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt
index f5abf314..6965e083 100644
--- a/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt
+++ b/app/src/main/java/com/concordium/wallet/core/notifications/UpdateNotificationsSubscriptionUseCase.kt
@@ -1,14 +1,13 @@
package com.concordium.wallet.core.notifications
-import android.app.Application
+import android.content.Context
import com.concordium.wallet.App
import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.backend.notifications.NotificationsBackend
import com.concordium.wallet.data.backend.notifications.UpdateSubscriptionRequest
import com.concordium.wallet.data.model.NotificationsTopic
-import com.concordium.wallet.data.preferences.NotificationsPreferences
+import com.concordium.wallet.data.preferences.WalletNotificationsPreferences
import com.concordium.wallet.data.room.Account
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
@@ -19,27 +18,26 @@ import java.net.HttpURLConnection
class UpdateNotificationsSubscriptionUseCase(
private val accountRepository: AccountRepository,
- private val notificationsPreferences: NotificationsPreferences,
+ private val walletNotificationsPreferences: WalletNotificationsPreferences,
private val notificationsBackend: NotificationsBackend,
- private val application: Application,
+ private val context: Context,
) {
- constructor(application: Application) : this(
- accountRepository = WalletDatabase.getDatabase(application).accountDao()
- .let(::AccountRepository),
- notificationsPreferences = NotificationsPreferences(application),
+ constructor() : this(
+ accountRepository = AccountRepository(App.appCore.session.walletStorage.database.accountDao()),
+ walletNotificationsPreferences = App.appCore.session.walletStorage.notificationsPreferences,
notificationsBackend = App.appCore.getNotificationsBackend(),
- application = application
+ context = App.appContext,
)
/**
* @return **true** on successful update
*/
suspend operator fun invoke(
- isCcdTxEnabled: Boolean = notificationsPreferences.areCcdTxNotificationsEnabled,
- isCis2TxEnabled: Boolean = notificationsPreferences.areCis2TxNotificationsEnabled,
+ isCcdTxEnabled: Boolean = walletNotificationsPreferences.areCcdTxNotificationsEnabled,
+ isCis2TxEnabled: Boolean = walletNotificationsPreferences.areCis2TxNotificationsEnabled
): Boolean {
val googleApiAvailability = GoogleApiAvailability.getInstance()
- val fcmToken = when (googleApiAvailability.isGooglePlayServicesAvailable(application)) {
+ val fcmToken = when (googleApiAvailability.isGooglePlayServicesAvailable(context)) {
ConnectionResult.SUCCESS -> {
try {
FirebaseMessaging.getInstance().token.await()
diff --git a/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt b/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt
index 17606ca3..554a9464 100644
--- a/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt
+++ b/app/src/main/java/com/concordium/wallet/core/security/EncryptionException.kt
@@ -1,7 +1,3 @@
package com.concordium.wallet.core.security
-class EncryptionException : Exception {
- constructor(message: String?) : super(message)
- constructor(cause: Throwable?) : super(cause)
- constructor(message: String?, cause: Throwable?) : super(message, cause)
-}
\ No newline at end of file
+class EncryptionException(cause: Throwable?) : Exception(cause)
diff --git a/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt b/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt
index 3c67bac2..9d62d4e3 100644
--- a/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt
+++ b/app/src/main/java/com/concordium/wallet/core/security/EncryptionHelper.kt
@@ -1,122 +1,51 @@
package com.concordium.wallet.core.security
import android.security.keystore.KeyProperties
-import android.util.Base64
+import com.concordium.wallet.data.model.EncryptedData
import com.concordium.wallet.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import java.io.UnsupportedEncodingException
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
-import java.security.spec.InvalidKeySpecException
-import java.security.spec.KeySpec
import javax.crypto.*
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
/**
- * Encryption functions. Beware that these are used for both authentication and export.
+ * Encryption and key derivation functions.
*/
object EncryptionHelper {
+ private const val KDF = "PBKDF2WithHmacSHA256"
+ private const val KDF_ITERATION_COUNT = 10000
- private val t = this.javaClass.simpleName
-
- private const val DEFAULT_ITERATION_COUNT = 10000
- private const val KEY_LENGTH = 256
- private const val CIPHER_TRANSFORMATION =
- "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/PKCS7Padding"
-
- /**
- * @exception EncryptionException
- */
- fun createEncryptionData(): Pair {
- val saltLength = KEY_LENGTH / 8 // same size as key output
- val random = SecureRandom()
- val salt = ByteArray(saltLength)
- random.nextBytes(salt)
-
- try {
- val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
- val iv = ByteArray(cipher.blockSize)
- random.nextBytes(iv)
-
- return Pair(salt, iv)
- } catch (e: Exception) {
- when (e) {
- is NoSuchAlgorithmException,
- is NoSuchPaddingException -> {
- Log.d("$t: Failed creating encryption data", e)
- throw EncryptionException(e)
- }
- else -> throw e
- }
- }
- }
+ private const val ENCRYPTION_KEY_SIZE_BITS = 256
+ private const val ENCRYPTION_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
+ private const val ENCRYPTION_CIPHER_TRANSFORMATION =
+ "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/NoPadding"
/**
- * @exception EncryptionException
+ * Encrypts given [data] with the given [key] using the best suitable cipher.
+ *
+ * @return [EncryptedData] with a unique IV.
*/
@Throws(EncryptionException::class)
- fun generateKey(
- password: String,
- salt: ByteArray,
- iterationCount: Int = DEFAULT_ITERATION_COUNT
- ): SecretKey {
- Log.i("$t: generateKey. Params --> password: $password, salt: $salt")
- val keyBytes = generateKeyAsByteArray(password, salt, iterationCount)
- val key: SecretKey = SecretKeySpec(keyBytes, "AES")
- return key
- }
-
- /**
- * @exception EncryptionException
- */
- private fun generateKeyAsByteArray(
- password: String,
- salt: ByteArray,
- iterationCount: Int = DEFAULT_ITERATION_COUNT
- ): ByteArray {
+ suspend fun encrypt(
+ key: ByteArray,
+ data: ByteArray,
+ cipherTransformation: String = ENCRYPTION_CIPHER_TRANSFORMATION,
+ ): EncryptedData = withContext(Dispatchers.Default) {
try {
- val keySpec: KeySpec =
- PBEKeySpec(
- password.toCharArray(), salt,
- iterationCount,
- KEY_LENGTH
- )
- val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
- val keyBytes = keyFactory.generateSecret(keySpec).encoded
- return keyBytes
- } catch (e: Exception) {
- when (e) {
- is NoSuchAlgorithmException,
- is InvalidKeySpecException -> {
- Log.d("Failed to create key", e)
- throw EncryptionException(e)
- }
- else -> throw e
- }
- }
- }
-
- /**
- * @exception EncryptionException
- */
- fun encrypt(
- key: SecretKey,
- iv: ByteArray,
- toBeEncrypted: String,
- base64EncodeFlags: Int = Base64.DEFAULT
- ): String {
- try {
- val toBeEncryptedByteArray = toBeEncrypted.toByteArray(charset("UTF-8"))
- val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
- val ivParams = IvParameterSpec(iv)
- cipher.init(Cipher.ENCRYPT_MODE, key, ivParams)
- val cipherText = cipher.doFinal(toBeEncryptedByteArray)
- val encodedEncrypted = Base64.encodeToString(cipherText, base64EncodeFlags)
- Log.d("Encrypted text: $encodedEncrypted")
- return encodedEncrypted
+ val cipher = Cipher.getInstance(cipherTransformation)
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, cipher.algorithm))
+ EncryptedData(
+ ciphertext = cipher.doFinal(data),
+ transformation = cipher.algorithm,
+ iv = cipher.iv,
+ )
} catch (e: Exception) {
when (e) {
is NoSuchAlgorithmException,
@@ -129,27 +58,25 @@ object EncryptionHelper {
Log.d("Failed to encrypt data", e)
throw EncryptionException(e)
}
+
else -> throw e
}
}
}
- /**
- * @exception EncryptionException
- */
- fun decrypt(
- key: SecretKey,
- iv: ByteArray,
- toBeDecrypted: ByteArray
- ): String {
+ @Throws(EncryptionException::class)
+ suspend fun decrypt(
+ key: ByteArray,
+ encryptedData: EncryptedData,
+ ): ByteArray = withContext(Dispatchers.Default) {
try {
- val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
- val ivParams = IvParameterSpec(iv)
- cipher.init(Cipher.DECRYPT_MODE, key, ivParams)
- val plainText = cipher.doFinal(toBeDecrypted)
- val decryptedString = String(plainText, charset("UTF-8"))
- Log.d("Decrypted text: $decryptedString")
- return decryptedString
+ val cipher = Cipher.getInstance(encryptedData.transformation)
+ cipher.init(
+ Cipher.DECRYPT_MODE,
+ SecretKeySpec(key, cipher.algorithm),
+ IvParameterSpec(encryptedData.decodeIv())
+ )
+ cipher.doFinal(encryptedData.decodeCiphertext())
} catch (e: Exception) {
when (e) {
is NoSuchAlgorithmException,
@@ -162,35 +89,56 @@ object EncryptionHelper {
Log.d("Failed to decrypt data", e)
throw EncryptionException(e)
}
+
else -> throw e
}
}
}
- /**
- * @exception EncryptionException
- */
- fun encrypt(
- password: String,
- salt: ByteArray,
- iv: ByteArray,
- toBeEncrypted: String,
- base64EncodeFlags: Int = Base64.DEFAULT
- ): String {
- val key = generateKey(password, salt)
- return encrypt(key, iv, toBeEncrypted, base64EncodeFlags)
+ @Throws(EncryptionException::class)
+ suspend fun generateKey(): ByteArray = withContext(Dispatchers.Default) {
+ try {
+ val generator = KeyGenerator.getInstance(ENCRYPTION_KEY_ALGORITHM)
+ generator.init(ENCRYPTION_KEY_SIZE_BITS)
+ generator.generateKey().encoded
+ } catch (e: Exception) {
+ Log.d("Failed to generate a key", e)
+ throw EncryptionException(e)
+ }
}
- /**
- * @exception EncryptionException
- */
- fun decrypt(
- password: String,
+ @Throws(EncryptionException::class)
+ suspend fun generatePasswordKeySalt(): ByteArray = withContext(Dispatchers.Default) {
+ try {
+ SecureRandom().generateSeed(ENCRYPTION_KEY_SIZE_BITS / 8)
+ } catch (e: Exception) {
+ Log.d("Failed to generate password key salt", e)
+ throw EncryptionException(e)
+ }
+ }
+
+ @Throws(EncryptionException::class)
+ suspend fun generatePasswordKey(
+ password: CharArray,
salt: ByteArray,
- iv: ByteArray,
- toBeDecrypted: ByteArray
- ): String {
- val key = generateKey(password, salt)
- return decrypt(key, iv, toBeDecrypted)
+ sizeBits: Int = ENCRYPTION_KEY_SIZE_BITS,
+ kdf: String = KDF,
+ kdfIterationCount: Int = KDF_ITERATION_COUNT,
+ ): ByteArray = withContext(Dispatchers.Default) {
+ try {
+ SecretKeyFactory.getInstance(kdf)
+ .generateSecret(
+ PBEKeySpec(
+ password,
+ salt,
+ kdfIterationCount,
+ sizeBits,
+ )
+ )
+ .encoded
+ } catch (e: Exception) {
+ Log.d("Failed to generate password key", e)
+ throw EncryptionException(e)
+ }
}
}
diff --git a/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt b/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt
index 52df6fbc..d5334059 100644
--- a/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt
+++ b/app/src/main/java/com/concordium/wallet/core/security/KeystoreHelper.kt
@@ -5,7 +5,13 @@ import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import com.concordium.wallet.util.Log
import java.io.IOException
-import java.security.*
+import java.security.InvalidAlgorithmParameterException
+import java.security.InvalidKeyException
+import java.security.KeyStore
+import java.security.KeyStoreException
+import java.security.NoSuchAlgorithmException
+import java.security.NoSuchProviderException
+import java.security.UnrecoverableKeyException
import java.security.cert.CertificateException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
@@ -17,11 +23,15 @@ class KeystoreHelper {
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
+ private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
+ private const val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
+ private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
+ const val ENCRYPTION_CIPHER_TRANSFORMATION =
+ "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING"
}
private fun generateSecretKeyWithSpecs(keyGenParameterSpec: KeyGenParameterSpec) {
- val keyGenerator =
- KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
+ val keyGenerator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEY_STORE)
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
}
@@ -30,9 +40,9 @@ class KeystoreHelper {
try {
val keyProperties = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
val builder = KeyGenParameterSpec.Builder(keyName, keyProperties)
- .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ .setBlockModes(ENCRYPTION_BLOCK_MODE)
.setUserAuthenticationRequired(true)
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+ .setEncryptionPaddings(ENCRYPTION_PADDING)
.setInvalidatedByBiometricEnrollment(true)
generateSecretKeyWithSpecs(builder.build())
@@ -42,20 +52,19 @@ class KeystoreHelper {
is NoSuchProviderException,
is InvalidAlgorithmParameterException,
is CertificateException,
- is IOException -> {
+ is IOException,
+ -> {
Log.d("Failed to generate secret key", e)
throw KeystoreEncryptionException("Failed to generate secret key", e)
}
+
else -> throw e
}
}
}
- private fun setupCipher(): Cipher {
- val cipherString =
- "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}"
- return Cipher.getInstance(cipherString)
- }
+ private fun getCipher(): Cipher =
+ Cipher.getInstance(ENCRYPTION_CIPHER_TRANSFORMATION)
private fun getSecretKey(keyName: String): SecretKey {
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
@@ -65,7 +74,7 @@ class KeystoreHelper {
fun initCipherForEncryption(keyName: String): Cipher? {
try {
- val cipher = setupCipher()
+ val cipher = getCipher()
val secretKey = getSecretKey(keyName)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
return cipher
@@ -78,10 +87,12 @@ class KeystoreHelper {
is IOException,
is NoSuchAlgorithmException,
is NoSuchPaddingException,
- is InvalidKeyException -> {
+ is InvalidKeyException,
+ -> {
Log.d("Failed to init Cipher", e)
throw KeystoreEncryptionException("Failed to init Cipher", e)
}
+
else -> throw e
}
}
@@ -89,7 +100,7 @@ class KeystoreHelper {
fun initCipherForDecryption(keyName: String, initVector: ByteArray): Cipher? {
try {
- val cipher = setupCipher()
+ val cipher = getCipher()
val secretKey = getSecretKey(keyName)
val ivParams = IvParameterSpec(initVector)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParams)
@@ -104,12 +115,14 @@ class KeystoreHelper {
is NoSuchAlgorithmException,
is NoSuchPaddingException,
is InvalidAlgorithmParameterException,
- is InvalidKeyException -> {
+ is InvalidKeyException,
+ -> {
Log.d("Failed to init Cipher", e)
throw KeystoreEncryptionException("Failed to init Cipher", e)
}
+
else -> throw e
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/AppWalletRepository.kt b/app/src/main/java/com/concordium/wallet/data/AppWalletRepository.kt
new file mode 100644
index 00000000..a9b86ec1
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/AppWalletRepository.kt
@@ -0,0 +1,59 @@
+package com.concordium.wallet.data
+
+import com.concordium.wallet.core.multiwallet.AppWallet
+import com.concordium.wallet.data.room.app.AppWalletDao
+import com.concordium.wallet.data.room.app.AppWalletEntity
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+class AppWalletRepository(
+ private val appWalletDao: AppWalletDao,
+) {
+ // The first (primary) wallet is created by the Two wallets migration.
+
+ suspend fun getActiveWallet(): AppWallet =
+ appWalletDao.getActive()
+ .let(::AppWallet)
+
+ fun getWalletsFlow(): Flow> =
+ appWalletDao.getAll()
+ .map { rows ->
+ rows.map(::AppWallet)
+ }
+
+ suspend fun getWallets(): List =
+ getWalletsFlow().first()
+
+ suspend fun addWallet(wallet: AppWallet) {
+ appWalletDao.insertAndActivate(AppWalletEntity(wallet))
+ }
+
+ suspend fun switchWalletType(
+ walletId: String,
+ newType: AppWallet.Type
+ ) {
+ appWalletDao.switchType(
+ walletId = walletId,
+ newType = newType.name,
+ )
+ }
+
+ suspend fun activate(
+ newActiveWallet: AppWallet,
+ ) {
+ appWalletDao.activate(
+ walletId = newActiveWallet.id,
+ )
+ }
+
+ suspend fun delete(
+ walletToDeleteId: String,
+ walletToActivateId: String,
+ ) {
+ appWalletDao.deleteAndActivateAnother(
+ walletToDeleteId = walletToDeleteId,
+ walletToActivateId = walletToActivateId,
+ )
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/WalletStorage.kt b/app/src/main/java/com/concordium/wallet/data/WalletStorage.kt
new file mode 100644
index 00000000..c76e1bfd
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/WalletStorage.kt
@@ -0,0 +1,79 @@
+package com.concordium.wallet.data
+
+import android.content.Context
+import com.concordium.wallet.core.multiwallet.AppWallet
+import com.concordium.wallet.data.preferences.Preferences
+import com.concordium.wallet.data.preferences.WalletFilterPreferences
+import com.concordium.wallet.data.preferences.WalletIdentityCreationDataPreferences
+import com.concordium.wallet.data.preferences.WalletNotificationsPreferences
+import com.concordium.wallet.data.preferences.WalletProviderPreferences
+import com.concordium.wallet.data.preferences.WalletSendFundsPreferences
+import com.concordium.wallet.data.preferences.WalletSetupPreferences
+import com.concordium.wallet.data.room.WalletDatabase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * A provider for all the persistence for a single wallet.
+ *
+ * @param fileNameSuffix a part to append to default persistence file names.
+ * For the primary wallet, use an empty string to keep backward compatibility.
+ */
+@Suppress("DEPRECATION")
+class WalletStorage(
+ private val context: Context,
+ private val fileNameSuffix: String,
+) {
+ constructor(
+ context: Context,
+ activeWallet: AppWallet,
+ ) : this(
+ context = context,
+ fileNameSuffix =
+ if (activeWallet.id.isEmpty())
+ ""
+ else
+ "_${activeWallet.id}"
+ )
+
+ val database: WalletDatabase by lazy {
+ WalletDatabase.getDatabase(context, fileNameSuffix)
+ }
+
+ val filterPreferences: WalletFilterPreferences by lazy {
+ WalletFilterPreferences(context, fileNameSuffix)
+ }
+
+ val sendFundsPreferences: WalletSendFundsPreferences by lazy {
+ WalletSendFundsPreferences(context, fileNameSuffix)
+ }
+
+ val identityCreationDataPreferences: WalletIdentityCreationDataPreferences by lazy {
+ WalletIdentityCreationDataPreferences(context, fileNameSuffix)
+ }
+
+ val notificationsPreferences: WalletNotificationsPreferences by lazy {
+ WalletNotificationsPreferences(context, fileNameSuffix)
+ }
+
+ val providerPreferences: WalletProviderPreferences by lazy {
+ WalletProviderPreferences(context, fileNameSuffix)
+ }
+
+ val setupPreferences: WalletSetupPreferences by lazy {
+ WalletSetupPreferences(context, fileNameSuffix)
+ }
+
+ suspend fun erase() = withContext(Dispatchers.IO) {
+ database.clearAllTables()
+
+ arrayOf(
+ filterPreferences,
+ sendFundsPreferences,
+ identityCreationDataPreferences,
+ notificationsPreferences,
+ providerPreferences,
+ setupPreferences,
+ ).forEach(Preferences::clearAll)
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt b/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt
index ac5b608b..bfe8c679 100644
--- a/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt
+++ b/app/src/main/java/com/concordium/wallet/data/backend/ProxyBackend.kt
@@ -101,10 +101,6 @@ interface ProxyBackend {
@PUT("v0/testnetGTUDrop/{accountAddress}")
fun requestGTUDrop(@Path("accountAddress") accountAddress: String): Call
- // Identity Provider
- @GET("v0/ip_info")
- fun getIdentityProviderInfo(): Call>
-
@GET("v2/ip_info")
fun getV2IdentityProviderInfo(): Call>
diff --git a/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt b/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt
index fade8722..2c195704 100644
--- a/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt
+++ b/app/src/main/java/com/concordium/wallet/data/backend/repository/IdentityProviderRepository.kt
@@ -14,16 +14,11 @@ class IdentityProviderRepository {
private val backend = App.appCore.getProxyBackend()
fun getIdentityProviderInfo(
- useLegacy: Boolean,
success: (ArrayList) -> Unit,
failure: ((Throwable) -> Unit)?
): BackendRequest> {
- val call =
- if (useLegacy)
- backend.getIdentityProviderInfo()
- else
- backend.getV2IdentityProviderInfo()
- call.enqueue(object : BackendCallback>() {
+ val call = backend.getV2IdentityProviderInfo()
+ backend.getV2IdentityProviderInfo().enqueue(object : BackendCallback>() {
override fun onResponseData(response: ArrayList) {
success(response)
diff --git a/app/src/main/java/com/concordium/wallet/data/model/EncryptedData.kt b/app/src/main/java/com/concordium/wallet/data/model/EncryptedData.kt
new file mode 100644
index 00000000..39e7ef17
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/model/EncryptedData.kt
@@ -0,0 +1,62 @@
+package com.concordium.wallet.data.model
+
+import android.util.Base64
+import com.google.gson.annotations.SerializedName
+import java.io.Serializable
+
+/**
+ * An encrypted data container.
+ */
+class EncryptedData(
+ /**
+ * Base64-encoded ciphertext.
+ *
+ * @see decodeCiphertext
+ */
+ @SerializedName("ct")
+ val ciphertext: String,
+
+ /**
+ * Java-style cipher transformation, e.g. "AES/CBC/PKCS7Padding".
+ */
+ @SerializedName("t")
+ val transformation: String,
+
+ /**
+ * Base64-encoded cipher initialization vector.
+ *
+ * @see decodeIv
+ */
+ @SerializedName("iv")
+ val iv: String,
+): Serializable {
+
+ /**
+ * @param ciphertext raw ciphertext
+ * @param transformation Java-style cipher transformation, e.g. "AES/CBC/PKCS7Padding"
+ * @param iv raw cipher initialization vector
+ */
+ constructor(
+ ciphertext: ByteArray,
+ transformation: String,
+ iv: ByteArray,
+ ) : this(
+ ciphertext = Base64.encodeToString(ciphertext, BASE64_FLAGS),
+ transformation = transformation,
+ iv = Base64.encodeToString(iv, BASE64_FLAGS)
+ )
+
+ fun decodeCiphertext(): ByteArray =
+ Base64.decode(ciphertext, BASE64_FLAGS)
+
+ fun decodeIv(): ByteArray =
+ Base64.decode(iv, BASE64_FLAGS)
+
+ override fun toString(): String {
+ return "EncryptedData(ciphertext='$ciphertext', transformation='$transformation', iv='$iv')"
+ }
+
+ private companion object {
+ private const val BASE64_FLAGS = Base64.NO_WRAP or Base64.NO_PADDING
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt b/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt
index b3964afe..b337ecd4 100644
--- a/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt
+++ b/app/src/main/java/com/concordium/wallet/data/model/IdentityCreationData.kt
@@ -4,38 +4,10 @@ import com.concordium.wallet.core.gson.RawJsonTypeAdapter
import com.google.gson.annotations.JsonAdapter
import java.io.Serializable
-sealed class IdentityCreationData(
+class IdentityCreationData(
val identityProvider: IdentityProvider,
@JsonAdapter(RawJsonTypeAdapter::class)
val idObjectRequest: RawJson,
val identityName: String,
val identityIndex: Int,
-) : Serializable {
- class V0(
- val privateIdObjectDataEncrypted: String,
- val accountName: String,
- val encryptedAccountData: String,
- val accountAddress: String,
- identityProvider: IdentityProvider,
- idObjectRequest: RawJson,
- identityName: String,
- identityIndex: Int
- ) : IdentityCreationData(
- identityProvider,
- idObjectRequest,
- identityName,
- identityIndex,
- )
-
- class V1(
- identityProvider: IdentityProvider,
- idObjectRequest: RawJson,
- identityName: String,
- identityIndex: Int,
- ) : IdentityCreationData(
- identityProvider,
- idObjectRequest,
- identityName,
- identityIndex,
- )
-}
+): Serializable
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/AppSetupPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/AppSetupPreferences.kt
new file mode 100644
index 00000000..fc9b854d
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/AppSetupPreferences.kt
@@ -0,0 +1,109 @@
+package com.concordium.wallet.data.preferences
+
+import android.content.Context
+import com.concordium.wallet.data.model.EncryptedData
+import com.concordium.wallet.util.toHex
+import com.google.gson.Gson
+import okio.ByteString.Companion.decodeHex
+
+class AppSetupPreferences(
+ context: Context,
+ private val gson: Gson,
+) : Preferences(context, SharedPreferenceFiles.APP_SETUP.key) {
+
+ fun setUsePasscode(slot: String, value: Boolean) {
+ setBoolean(PREFKEY_USE_PASSCODE + slot, value)
+ }
+
+ fun getUsePasscode(slot: String): Boolean {
+ return getBoolean(PREFKEY_USE_PASSCODE + slot)
+ }
+
+ fun setUseBiometrics(slot: String, value: Boolean) {
+ setBoolean(PREFKEY_USE_BIOMETRICS + slot, value)
+ }
+
+ fun getUseBiometrics(slot: String): Boolean {
+ return getBoolean(PREFKEY_USE_BIOMETRICS + slot)
+ }
+
+ fun setPasswordKeySalt(slot: String, value: ByteArray) {
+ setString(PREFKEY_PASSWORD_KEY_SALT_HEX + slot, value.toHex())
+ }
+
+ fun getPasswordKeySalt(slot: String): ByteArray {
+ return getString(PREFKEY_PASSWORD_KEY_SALT_HEX + slot, "").decodeHex().toByteArray()
+ }
+
+ fun hasPasswordKeySalt(slot: String): Boolean {
+ return getString(PREFKEY_PASSWORD_KEY_SALT_HEX + slot) != null
+ }
+
+ fun setEncryptedPassword(slot: String, value: EncryptedData) {
+ setJsonSerialized(PREFKEY_ENCRYPTED_PASSWORD_JSON + slot, value, gson)
+ }
+
+ fun getEncryptedPassword(slot: String): EncryptedData {
+ return getJsonSerialized(PREFKEY_ENCRYPTED_PASSWORD_JSON + slot, gson)!!
+ }
+
+ fun setEncryptedMasterKey(slot: String, value: EncryptedData) {
+ setJsonSerialized(PREFKEY_ENCRYPTED_MASTER_KEY_JSON + slot, value, gson)
+ }
+
+ fun getEncryptedMasterKey(slot: String): EncryptedData {
+ return getJsonSerialized(PREFKEY_ENCRYPTED_MASTER_KEY_JSON + slot, gson)!!
+ }
+
+ fun hasEncryptedMasterKey(slot: String): Boolean {
+ return getString(PREFKEY_ENCRYPTED_MASTER_KEY_JSON + slot) != null
+ }
+
+ fun setLegacyEncryptedPasswordCheck(slot: String, value: EncryptedData) {
+ setJsonSerialized(PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON + slot, value, gson)
+ }
+
+ fun getLegacyEncryptedPasswordCheck(slot: String): EncryptedData? {
+ return getJsonSerialized(
+ PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON + slot,
+ gson
+ )
+ }
+
+ fun setLegacyPasswordCheck(slot: String, value: String) {
+ setString(PREFKEY_LEGACY_PASSWORD_CHECK + slot, value)
+ }
+
+ fun getLegacyPasswordCheck(slot: String): String? {
+ return getString(PREFKEY_LEGACY_PASSWORD_CHECK + slot)
+ }
+
+ fun getCurrentAuthSlot(): String {
+ return getString(PREFKEY_CURRENT_AUTH_SLOT, "default_key")
+ }
+
+ fun setCurrentAuthSlot(slot: String) {
+ return setString(PREFKEY_CURRENT_AUTH_SLOT, slot)
+ }
+
+ fun setHasCompletedInitialSetup(value: Boolean) {
+ setBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, value)
+ }
+
+ fun getHasCompletedInitialSetup(): Boolean {
+ return getBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, false)
+ }
+
+ private companion object {
+ const val PREFKEY_USE_PASSCODE = "PREFKEY_USE_PASSCODE"
+ const val PREFKEY_USE_BIOMETRICS = "PREFKEY_USE_BIOMETRICS"
+ const val PREFKEY_PASSWORD_KEY_SALT_HEX = "PREFKEY_PASSWORD_KEY_SALT"
+ const val PREFKEY_ENCRYPTED_PASSWORD_JSON = "PREFKEY_ENCRYPTED_PASSWORD_JSON"
+ const val PREFKEY_CURRENT_AUTH_SLOT = "PREFKEY_CURRENT_AUTH_SLOT"
+ const val PREFKEY_ENCRYPTED_MASTER_KEY_JSON = "PREFKEY_ENCRYPTED_MASTER_KEY_JSON"
+ const val PREFKEY_HAS_COMPLETED_INITIAL_SETUP = "PREFKEY_HAS_COMPLETED_INITIAL_SETUP"
+ const val PREFKEY_LEGACY_PASSWORD_CHECK = "PREFKEY_LEGACY_PASSWORD_CHECK"
+ const val PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON =
+ "PREFKEY_LEGACY_ENCRYPTED_PASSWORD_CHECK_JSON"
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/TrackingPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/AppTrackingPreferences.kt
similarity index 78%
rename from app/src/main/java/com/concordium/wallet/data/preferences/TrackingPreferences.kt
rename to app/src/main/java/com/concordium/wallet/data/preferences/AppTrackingPreferences.kt
index 6492e170..c965e2bd 100644
--- a/app/src/main/java/com/concordium/wallet/data/preferences/TrackingPreferences.kt
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/AppTrackingPreferences.kt
@@ -2,8 +2,9 @@ package com.concordium.wallet.data.preferences
import android.content.Context
-class TrackingPreferences(context: Context) :
- Preferences(context, SharedPreferencesKeys.PREF_TRACKING.key, Context.MODE_PRIVATE) {
+class AppTrackingPreferences(
+ context: Context,
+) : Preferences(context, SharedPreferenceFiles.APP_TRACKING.key) {
var isTrackingEnabled: Boolean
by BooleanPreference(PREFKEY_TRACKING_ENABLED, false)
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/AuthPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/AuthPreferences.kt
deleted file mode 100644
index 5824bfbf..00000000
--- a/app/src/main/java/com/concordium/wallet/data/preferences/AuthPreferences.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-package com.concordium.wallet.data.preferences
-
-import android.content.Context
-import cash.z.ecc.android.bip39.Mnemonics
-import cash.z.ecc.android.bip39.toSeed
-import com.concordium.wallet.App
-import com.concordium.wallet.util.toHex
-import com.reown.util.hexToBytes
-import javax.crypto.SecretKey
-
-class AuthPreferences(val context: Context) :
- Preferences(context, SharedPreferencesKeys.PREF_FILE_AUTH.key, Context.MODE_PRIVATE) {
-
- companion object {
- const val PREFKEY_HAS_SETUP_USER = "PREFKEY_HAS_SETUP_USER"
- const val PREFKEY_HAS_COMPLETED_INITIAL_SETUP = "PREFKEY_HAS_COMPLETED_INITIAL_SETUP"
- const val PREFKEY_HAS_COMPLETED_ONBOARDING = "PREFKEY_HAS_COMPLETED_ONBOARDING"
- const val PREFKEY_USE_PASSCODE = "PREFKEY_USE_PASSCODE"
- const val PREFKEY_USE_BIOMETRICS = "PREFKEY_USE_BIOMETRICS"
- const val PREFKEY_PASSWORD_CHECK = "PREFKEY_PASSWORD_CHECK"
- const val PREFKEY_PASSWORD_CHECK_ENCRYPTED = "PREFKEY_PASSWORD_CHECK_ENCRYPTED"
- const val PREFKEY_PASSWORD_ENCRYPTION_SALT = "PREFKEY_PASSWORD_ENCRYPTION_SALT"
- const val PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR = "PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR"
- const val PREFKEY_ENCRYPTED_PASSWORD = "PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY"
- const val PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR =
- "PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR"
- const val PREFKEY_BIOMETRIC_KEY = "PREFKEY_BIOMETRIC_KEY"
- const val PREFKEY_ACCOUNTS_BACKED_UP = "PREFKEY_ACCOUNTS_BACKED_UP"
- const val PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX =
- "PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX"
- const val PREFKEY_LEGACY_SEED_HEX_ENCRYPTED = "SEED_PHRASE_ENCRYPTED"
- const val PREFKEY_HAS_SHOWED_INITIAL_ANIMATION = "PREFKEY_HAS_SHOWED_INITIAL_ANIMATION"
- }
-
- fun setHasSetupUser(value: Boolean) {
- setBoolean(PREFKEY_HAS_SETUP_USER, value)
- }
-
- fun getHasSetupUser(): Boolean {
- return getBoolean(PREFKEY_HAS_SETUP_USER)
- }
-
- fun setHasCompletedInitialSetup(value: Boolean) {
- setBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, value)
- }
-
- fun getHasCompletedInitialSetup(): Boolean {
- // Default value is true for backward compatibility.
- return getBoolean(PREFKEY_HAS_COMPLETED_INITIAL_SETUP, true)
- }
-
- fun setHasCompletedOnboarding(value: Boolean) {
- setBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, value)
- }
-
- fun getHasCompletedOnboarding(): Boolean {
- return getBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, false)
- }
-
- fun setHasShowedInitialAnimation(value: Boolean) {
- setBoolean(PREFKEY_HAS_SHOWED_INITIAL_ANIMATION, value)
- }
-
- fun getShowedInitialAnimation(): Boolean {
- return getBoolean(PREFKEY_HAS_SHOWED_INITIAL_ANIMATION, false)
- }
-
- fun setUsePasscode(appendix: String, value: Boolean) {
- setBoolean(PREFKEY_USE_PASSCODE + appendix, value)
- }
-
- fun getUsePasscode(appendix: String): Boolean {
- return getBoolean(PREFKEY_USE_PASSCODE + appendix)
- }
-
- fun setUseBiometrics(appendix: String, value: Boolean) {
- setBoolean(PREFKEY_USE_BIOMETRICS + appendix, value)
- }
-
- fun getUseBiometrics(appendix: String): Boolean {
- return getBoolean(PREFKEY_USE_BIOMETRICS + appendix)
- }
-
- fun setPasswordCheck(appendix: String, value: String) {
- setString(PREFKEY_PASSWORD_CHECK + appendix, value)
- }
-
- fun getPasswordCheck(appendix: String): String? {
- return getString(PREFKEY_PASSWORD_CHECK + appendix)
- }
-
- fun setPasswordCheckEncrypted(appendix: String, value: String) {
- setString(PREFKEY_PASSWORD_CHECK_ENCRYPTED + appendix, value)
- }
-
- fun getPasswordCheckEncrypted(appendix: String): String {
- return getString(PREFKEY_PASSWORD_CHECK_ENCRYPTED + appendix, "")
- }
-
- fun setPasswordEncryptionSalt(appendix: String, value: String) {
- setString(PREFKEY_PASSWORD_ENCRYPTION_SALT + appendix, value)
- }
-
- fun getPasswordEncryptionSalt(appendix: String): String {
- return getString(PREFKEY_PASSWORD_ENCRYPTION_SALT + appendix, "")
- }
-
- fun setPasswordEncryptionInitVector(appendix: String, value: String) {
- setString(PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR + appendix, value)
- }
-
- fun getPasswordEncryptionInitVector(appendix: String): String {
- return getString(PREFKEY_PASSWORD_ENCRYPTION_INITVECTOR + appendix, "")
- }
-
- fun setEncryptedPassword(appendix: String, value: String) {
- setString(PREFKEY_ENCRYPTED_PASSWORD + appendix, value)
- }
-
- fun getEncryptedPassword(appendix: String): String {
- return getString(PREFKEY_ENCRYPTED_PASSWORD + appendix, "")
- }
-
- fun setEncryptedPasswordDerivedKeyInitVector(appendix: String, value: String) {
- setString(PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR + appendix, value)
- }
-
- fun getBiometricsKeyEncryptionInitVector(appendix: String): String {
- return getString(PREFKEY_ENCRYPTED_PASSWORD_DERIVED_KEY_INITVECTOR + appendix, "")
- }
-
- fun getAuthKeyName(): String {
- return getString(PREFKEY_BIOMETRIC_KEY, "default_key")
- }
-
- fun setAuthKeyName(key: String) {
- return setString(PREFKEY_BIOMETRIC_KEY, key)
- }
-
- fun isAccountsBackedUp(): Boolean {
- return getBoolean(PREFKEY_ACCOUNTS_BACKED_UP, true)
- }
-
- fun setAccountsBackedUp(value: Boolean) {
- return setBoolean(PREFKEY_ACCOUNTS_BACKED_UP, value)
- }
-
- fun addAccountsBackedUpListener(listener: Listener) {
- addListener(PREFKEY_ACCOUNTS_BACKED_UP, listener)
- }
-
- /**
- * Saves the seed phrase as its entropy, so it is possible to later get
- * both the seed hex and the seed phrase.
- *
- * @see getSeedHex
- * @see getSeedPhrase
- */
- suspend fun tryToSetEncryptedSeedPhrase(seedPhraseString: String, password: String): Boolean {
- val entropyHex = Mnemonics.MnemonicCode(seedPhraseString).toEntropy().toHex()
- val encryptedEntropyHex = App.appCore.getCurrentAuthenticationManager()
- .encryptInBackground(password, entropyHex)
- ?: return false
- return setStringWithResult(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX, encryptedEntropyHex)
- }
-
- /**
- * Saves the seed in HEX. This method is **only** to be used in case of
- * recovering a wallet from a seed (Wallet private key).
- * In other situation, like creation of a new wallet or recovering it from a seed phrase,
- * [tryToSetEncryptedSeedPhrase] must be used instead.
- *
- * @see getSeedHex
- */
- suspend fun tryToSetEncryptedSeedHex(seedHex: String, password: String): Boolean {
- val encryptedSeedHex = App.appCore.getCurrentAuthenticationManager()
- .encryptInBackground(password, seedHex)
- ?: return false
- return setStringWithResult(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED, encryptedSeedHex)
- }
-
- suspend fun getSeedHex(password: String): String {
- val authenticationManager = App.appCore.getOriginalAuthenticationManager()
-
- // Try the encrypted entropy hex.
- getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX)
- ?.let { authenticationManager.decryptInBackground(password, it) }
- ?.let { Mnemonics.MnemonicCode(it.hexToBytes()).toSeed().toHex() }
- ?.let { return it }
-
- // Try the legacy encrypted seed hex.
- getString(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED)
- ?.let { authenticationManager.decryptInBackground(password, it) }
- ?.let { return it }
-
- error("Failed to get the seed")
- }
-
- suspend fun getSeedHex(decryptKey: SecretKey): String {
- val authenticationManager = App.appCore.getOriginalAuthenticationManager()
-
- // Try the encrypted entropy hex.
- getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX)
- ?.let { authenticationManager.decryptInBackground(decryptKey, it) }
- ?.let { Mnemonics.MnemonicCode(it.hexToBytes()).toSeed().toHex() }
- ?.let { return it }
-
- // Try the legacy encrypted seed hex.
- getString(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED)
- ?.let { authenticationManager.decryptInBackground(decryptKey, it) }
- ?.let { return it }
-
- error("Failed to get the seed")
- }
-
- /**
- * @see hasEncryptedSeedPhrase
- */
- suspend fun getSeedPhrase(password: String): String =
- getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX)
- ?.let {
- App.appCore.getOriginalAuthenticationManager().decryptInBackground(password, it)
- }
- ?.let {
- Mnemonics.MnemonicCode(it.hexToBytes()).words.joinToString(
- separator = " ",
- transform = CharArray::concatToString
- )
- }
- ?: error("Failed to get the seed phrase")
-
- /**
- * @see hasEncryptedSeedPhrase
- */
- suspend fun getSeedPhrase(decryptKey: SecretKey): String =
- getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX)
- ?.let {
- App.appCore.getOriginalAuthenticationManager().decryptInBackground(decryptKey, it)
- }
- ?.let {
- Mnemonics.MnemonicCode(it.hexToBytes()).words.joinToString(
- separator = " ",
- transform = CharArray::concatToString
- )
- }
- ?: error("Failed to get the seed phrase")
-
- fun updateEncryptedSeedHex(encryptedSeedHex: String): Boolean {
- return setStringWithResult(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED, encryptedSeedHex)
- }
-
- fun updateEncryptedSeedEntropyHex(encryptedSeedEntropyHex: String): Boolean {
- return setStringWithResult(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX, encryptedSeedEntropyHex)
- }
-
- fun hasEncryptedSeed(): Boolean =
- getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) != null
- || getString(PREFKEY_LEGACY_SEED_HEX_ENCRYPTED) != null
-
- /**
- * The seed phrase can be restored only if it has been saved as an entropy.
- *
- * @see getSeedPhrase
- */
- fun hasEncryptedSeedPhrase(): Boolean =
- getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX) != null
-}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/FilterPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/FilterPreferences.kt
deleted file mode 100644
index ba561293..00000000
--- a/app/src/main/java/com/concordium/wallet/data/preferences/FilterPreferences.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.concordium.wallet.data.preferences
-
-import android.content.Context
-
-class FilterPreferences(val context: Context) :
- Preferences(context, SharedPreferencesKeys.PREF_FILE_FILTER.key, Context.MODE_PRIVATE) {
-
- companion object {
- val PREFKEY_FILTER_SHOW_REWARDS = "PREFKEY_FILTER_SHOW_REWARDS"
- val PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS = "PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS"
- }
-
- fun setHasShowRewards(id: Int, value: Boolean) {
- setBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, value)
- }
-
- fun getHasShowRewards(id: Int): Boolean {
- return getBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, true)
- }
-
- fun setHasShowFinalizationRewards(id: Int, value: Boolean) {
- setBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, value)
- }
-
- fun getHasShowFinalizationRewards(id: Int): Boolean {
- return getBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, true)
- }
-}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt
index 71bcd655..85b46a0a 100644
--- a/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/Preferences.kt
@@ -2,19 +2,20 @@ package com.concordium.wallet.data.preferences
import android.content.Context
import android.content.SharedPreferences
+import com.concordium.wallet.App
+import com.google.gson.Gson
import kotlin.reflect.KProperty
-open class Preferences {
+open class Preferences(context: Context, preferenceName: String) {
- protected lateinit var sharedPreferences: SharedPreferences
+ private val sharedPreferences: SharedPreferences =
+ context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE)
- protected val editor: android.content.SharedPreferences.Editor
+ private val editor: SharedPreferences.Editor
get() = sharedPreferences.edit()
private val changeListeners = HashMap()
- constructor() {}
-
interface Listener {
fun onChange()
}
@@ -35,10 +36,6 @@ open class Preferences {
) = setBoolean(key, arg)
}
- constructor(context: Context, preferenceName: String, preferenceMode: Int) {
- sharedPreferences = context.getSharedPreferences(preferenceName, preferenceMode)
- }
-
fun triggerChangeEvent(key: String) {
for ((listener, value) in changeListeners) {
if (value == key) {
@@ -53,11 +50,11 @@ open class Preferences {
editor.commit()
}
- public fun addListener(key: String, listener: Listener) {
+ fun addListener(key: String, listener: Listener) {
changeListeners.put(listener, key)
}
- public fun removeListener(listener: Listener) {
+ fun removeListener(listener: Listener) {
changeListeners.remove(listener)
}
@@ -139,4 +136,17 @@ open class Preferences {
protected fun getLong(key: String): Long {
return sharedPreferences.getLong(key, 0)
}
+
+ protected fun setJsonSerialized(
+ key: String,
+ value: T?,
+ gson: Gson = App.appCore.gson,
+ ) = setString(key, value?.let(gson::toJson))
+
+ protected inline fun getJsonSerialized(
+ key: String,
+ gson: Gson = App.appCore.gson,
+ ): T? = runCatching {
+ getString(key)?.let { gson.fromJson(it, T::class.java) }
+ }.getOrNull()
}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferenceFiles.kt b/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferenceFiles.kt
new file mode 100644
index 00000000..bf5530b8
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferenceFiles.kt
@@ -0,0 +1,14 @@
+package com.concordium.wallet.data.preferences
+
+enum class SharedPreferenceFiles(val key: String) {
+ APP_SETUP("PREF_FILE_APP_SETUP"),
+ APP_TRACKING("PREF_TRACKING"),
+
+ WALLET_SETUP("PREF_FILE_WALLET_SETUP"),
+ WALLET_FILTER("PREF_FILE_FILTER"),
+ WALLET_PROVIDER("PREF_FILE_PROVIDER"),
+ WALLET_ID_CREATION_DATA("KEY_IDENTITY_CREATION_DATA"),
+ WALLET_NOTIFICATIONS("PREF_NOTIFICATION"),
+ WALLET_SEND_FUNDS("PREF_SEND_FUNDS"),
+ ;
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferencesKeys.kt b/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferencesKeys.kt
deleted file mode 100644
index e8a6ae52..00000000
--- a/app/src/main/java/com/concordium/wallet/data/preferences/SharedPreferencesKeys.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.concordium.wallet.data.preferences
-
-enum class SharedPreferencesKeys(val key: String) {
- PREF_FILE_AUTH("PREF_FILE_AUTH"),
- PREF_FILE_FILTER("PREF_FILE_FILTER"),
- PREF_FILE_PROVIDER("PREF_FILE_PROVIDER"),
- KEY_IDENTITY_CREATION_DATA("KEY_IDENTITY_CREATION_DATA"),
- PREF_NOTIFICATION("PREF_NOTIFICATION"),
- PREF_TRACKING("PREF_TRACKING"),
- ;
-}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletFilterPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletFilterPreferences.kt
new file mode 100644
index 00000000..c1d44ed5
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletFilterPreferences.kt
@@ -0,0 +1,38 @@
+package com.concordium.wallet.data.preferences
+
+import android.content.Context
+
+class WalletFilterPreferences
+@Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.filterPreferences",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+)
+constructor(
+ val context: Context,
+ fileNameSuffix: String = "",
+) : Preferences(context, SharedPreferenceFiles.WALLET_FILTER.key + fileNameSuffix) {
+
+ private companion object {
+ const val PREFKEY_FILTER_SHOW_REWARDS = "PREFKEY_FILTER_SHOW_REWARDS"
+ const val PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS = "PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS"
+ }
+
+ fun setHasShowRewards(id: Int, value: Boolean) {
+ setBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, value)
+ }
+
+ fun getHasShowRewards(id: Int): Boolean {
+ return getBoolean(PREFKEY_FILTER_SHOW_REWARDS + id, true)
+ }
+
+ fun setHasShowFinalizationRewards(id: Int, value: Boolean) {
+ setBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, value)
+ }
+
+ fun getHasShowFinalizationRewards(id: Int): Boolean {
+ return getBoolean(PREFKEY_FILTER_SHOW_FINALIZATION_REWARDS + id, true)
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletIdentityCreationDataPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletIdentityCreationDataPreferences.kt
new file mode 100644
index 00000000..be1e5f8b
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletIdentityCreationDataPreferences.kt
@@ -0,0 +1,37 @@
+package com.concordium.wallet.data.preferences
+
+import android.content.Context
+import com.concordium.wallet.data.model.IdentityCreationData
+
+class WalletIdentityCreationDataPreferences
+@Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.identityCreationDataPreferences",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+)
+constructor(
+ context: Context,
+ fileNameSuffix: String = "",
+) : Preferences(context, SharedPreferenceFiles.WALLET_ID_CREATION_DATA.key + fileNameSuffix) {
+
+ fun getIdentityCreationData(): IdentityCreationData? =
+ getJsonSerialized(PREFKEY_IDENTITY_CREATION_DATA)
+
+ fun setIdentityCreationData(data: IdentityCreationData?) =
+ setJsonSerialized(PREFKEY_IDENTITY_CREATION_DATA, data)
+
+ fun getShowForFirstIdentityFromCallback(): Boolean {
+ return getBoolean(PREFKEY_SHOW_FOR_FIRST_IDENTITY, false)
+ }
+
+ fun setShowForFirstIdentityFromCallback(isFirst: Boolean) {
+ setBoolean(PREFKEY_SHOW_FOR_FIRST_IDENTITY, isFirst)
+ }
+
+ private companion object {
+ const val PREFKEY_IDENTITY_CREATION_DATA = "KEY_IDENTITY_CREATION_DATA"
+ const val PREFKEY_SHOW_FOR_FIRST_IDENTITY = "SHOW_FOR_FIRST_IDENTITY"
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/NotificationsPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletNotificationsPreferences.kt
similarity index 70%
rename from app/src/main/java/com/concordium/wallet/data/preferences/NotificationsPreferences.kt
rename to app/src/main/java/com/concordium/wallet/data/preferences/WalletNotificationsPreferences.kt
index e3fa0be7..688a8a32 100644
--- a/app/src/main/java/com/concordium/wallet/data/preferences/NotificationsPreferences.kt
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletNotificationsPreferences.kt
@@ -2,8 +2,18 @@ package com.concordium.wallet.data.preferences
import android.content.Context
-class NotificationsPreferences(context: Context) :
- Preferences(context, SharedPreferencesKeys.PREF_NOTIFICATION.key, Context.MODE_PRIVATE) {
+class WalletNotificationsPreferences
+@Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.notificationsPreferences",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+)
+constructor(
+ context: Context,
+ fileNameSuffix: String = "",
+) : Preferences(context, SharedPreferenceFiles.WALLET_NOTIFICATIONS.key + fileNameSuffix) {
var hasEverShownPermissionDialog: Boolean
by BooleanPreference(PREFKEY_HAS_EVER_SHOWN_PERMISSION_DIALOG, false)
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/ProviderPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletProviderPreferences.kt
similarity index 73%
rename from app/src/main/java/com/concordium/wallet/data/preferences/ProviderPreferences.kt
rename to app/src/main/java/com/concordium/wallet/data/preferences/WalletProviderPreferences.kt
index d2524541..763d5457 100644
--- a/app/src/main/java/com/concordium/wallet/data/preferences/ProviderPreferences.kt
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletProviderPreferences.kt
@@ -4,7 +4,18 @@ import android.content.Context
import com.concordium.wallet.ui.tokens.provider.ProviderMeta
import com.google.gson.Gson
-class ProviderPreferences(val context: Context) : Preferences(context, SharedPreferencesKeys.PREF_FILE_PROVIDER.key, Context.MODE_PRIVATE) {
+class WalletProviderPreferences
+@Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.providerPreferences",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+)
+constructor(
+ val context: Context,
+ fileNameSuffix: String = "",
+) : Preferences(context, SharedPreferenceFiles.WALLET_PROVIDER.key + fileNameSuffix) {
private val gson by lazy {
Gson()
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletSendFundsPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSendFundsPreferences.kt
new file mode 100644
index 00000000..d799bb66
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSendFundsPreferences.kt
@@ -0,0 +1,29 @@
+package com.concordium.wallet.data.preferences
+
+import android.content.Context
+
+class WalletSendFundsPreferences
+@Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.sendFundsPreferences",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+)
+constructor(
+ context: Context,
+ fileNameSuffix: String = "",
+) : Preferences(context, SharedPreferenceFiles.WALLET_SEND_FUNDS.key + fileNameSuffix) {
+
+ fun shouldShowMemoWarning(): Boolean {
+ return getBoolean(KEY_SHOW_MEMO_WARNING, true)
+ }
+
+ fun disableShowMemoWarning() {
+ setBoolean(KEY_SHOW_MEMO_WARNING, false)
+ }
+
+ private companion object {
+ const val KEY_SHOW_MEMO_WARNING = "KEY_SHOW_MEMO_WARNING_V2"
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/preferences/WalletSetupPreferences.kt b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSetupPreferences.kt
new file mode 100644
index 00000000..36483adc
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/preferences/WalletSetupPreferences.kt
@@ -0,0 +1,156 @@
+package com.concordium.wallet.data.preferences
+
+import android.content.Context
+import cash.z.ecc.android.bip39.Mnemonics
+import cash.z.ecc.android.bip39.toSeed
+import com.concordium.wallet.App
+import com.concordium.wallet.data.model.EncryptedData
+import com.concordium.wallet.util.toHex
+import com.google.gson.Gson
+import okio.ByteString.Companion.decodeHex
+
+class WalletSetupPreferences
+@Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.setupPreferences",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+)
+constructor(
+ context: Context,
+ fileNameSuffix: String = "",
+ private val gson: Gson = App.appCore.gson,
+) : Preferences(context, SharedPreferenceFiles.WALLET_SETUP.key + fileNameSuffix) {
+
+ fun areAccountsBackedUp(): Boolean {
+ return getBoolean(PREFKEY_ACCOUNTS_BACKED_UP, true)
+ }
+
+ fun setAccountsBackedUp(value: Boolean) {
+ return setBoolean(PREFKEY_ACCOUNTS_BACKED_UP, value)
+ }
+
+ fun addAccountsBackedUpListener(listener: Listener) {
+ addListener(PREFKEY_ACCOUNTS_BACKED_UP, listener)
+ }
+
+ /**
+ * Saves the seed phrase as its entropy, so it is possible to later get
+ * both the seed hex and the seed phrase.
+ *
+ * @see getSeedHex
+ * @see getSeedPhrase
+ */
+ suspend fun tryToSetEncryptedSeedPhrase(seedPhraseString: String, password: String): Boolean {
+ val entropy = Mnemonics.MnemonicCode(seedPhraseString).toEntropy()
+ val encryptedEntropy = App.appCore.auth.encrypt(
+ password = password,
+ // The entropy is encoded to hex to keep backward compatibility
+ // with the data encrypted before the Two wallets feature.
+ data = entropy.toHex().toByteArray(),
+ ) ?: return false
+ return tryToSetEncryptedSeedPhrase(encryptedEntropy)
+ }
+
+ fun tryToSetEncryptedSeedPhrase(encryptedSeedEntropy: EncryptedData): Boolean =
+ setStringWithResult(
+ PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON,
+ gson.toJson(encryptedSeedEntropy)
+ )
+
+ /**
+ * Saves the seed in HEX. This method is **only** to be used in case of
+ * recovering a wallet from a seed (Wallet private key).
+ * In other situation, like creation of a new wallet or recovering it from a seed phrase,
+ * [tryToSetEncryptedSeedPhrase] must be used instead.
+ *
+ * @see getSeedHex
+ */
+ suspend fun tryToSetEncryptedSeedHex(seedHex: String, password: String): Boolean {
+ val encryptedSeed = App.appCore.auth.encrypt(
+ password = password,
+ // The seed is not decoded from hex to keep backward compatibility
+ // with the data encrypted before the Two wallets feature.
+ data = seedHex.toByteArray(),
+ ) ?: return false
+ return tryToSetEncryptedSeedHex(encryptedSeed)
+ }
+
+ fun tryToSetEncryptedSeedHex(encryptedSeedHex: EncryptedData): Boolean =
+ setStringWithResult(
+ PREFKEY_ENCRYPTED_SEED_HEX_JSON,
+ gson.toJson(encryptedSeedHex)
+ )
+
+ suspend fun getSeedHex(password: String): String {
+ val authenticationManager = App.appCore.auth
+
+ // Try the encrypted entropy.
+ getJsonSerialized(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON, gson)
+ ?.let { authenticationManager.decrypt(password, it) }
+ ?.let { String(it).decodeHex().toByteArray() }
+ ?.let { Mnemonics.MnemonicCode(it).toSeed().toHex() }
+ ?.also { return it }
+
+ // Try the encrypted seed.
+ getJsonSerialized(PREFKEY_ENCRYPTED_SEED_HEX_JSON, gson)
+ ?.let { authenticationManager.decrypt(password, it) }
+ ?.let(::String)
+ ?.also { return it }
+
+ error("Failed to get the seed")
+ }
+
+ /**
+ * @see hasEncryptedSeedPhrase
+ */
+ suspend fun getSeedPhrase(password: String): String =
+ getJsonSerialized(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON, gson)
+ ?.let { App.appCore.auth.decrypt(password, it) }
+ ?.let { String(it).decodeHex().toByteArray() }
+ ?.let {
+ Mnemonics.MnemonicCode(it).words.joinToString(
+ separator = " ",
+ transform = CharArray::concatToString
+ )
+ }
+ ?: error("Failed to get the seed phrase")
+
+ fun hasEncryptedSeed(): Boolean =
+ getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON) != null
+ || getString(PREFKEY_ENCRYPTED_SEED_HEX_JSON) != null
+
+ /**
+ * The seed phrase can be restored only if it has been saved as an entropy.
+ *
+ * @see getSeedPhrase
+ */
+ fun hasEncryptedSeedPhrase(): Boolean =
+ getString(PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON) != null
+
+ fun setHasCompletedOnboarding(value: Boolean) {
+ setBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, value)
+ }
+
+ fun getHasCompletedOnboarding(): Boolean {
+ return getBoolean(PREFKEY_HAS_COMPLETED_ONBOARDING, false)
+ }
+
+ fun setHasShownInitialAnimation(value: Boolean) {
+ setBoolean(PREFKEY_HAS_SHOWN_INITIAL_ANIMATION, value)
+ }
+
+ fun getHasShownInitialAnimation(): Boolean {
+ return getBoolean(PREFKEY_HAS_SHOWN_INITIAL_ANIMATION, false)
+ }
+
+ private companion object {
+ const val PREFKEY_ACCOUNTS_BACKED_UP = "PREFKEY_ACCOUNTS_BACKED_UP"
+ const val PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON =
+ "PREFKEY_ENCRYPTED_SEED_ENTROPY_HEX_JSON"
+ const val PREFKEY_ENCRYPTED_SEED_HEX_JSON = "PREFKEY_ENCRYPTED_SEED_HEX_JSON"
+ const val PREFKEY_HAS_COMPLETED_ONBOARDING = "PREFKEY_HAS_COMPLETED_ONBOARDING"
+ const val PREFKEY_HAS_SHOWN_INITIAL_ANIMATION = "PREFKEY_HAS_SHOWN_INITIAL_ANIMATION"
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/room/Account.kt b/app/src/main/java/com/concordium/wallet/data/room/Account.kt
index 08411ad2..2d602458 100644
--- a/app/src/main/java/com/concordium/wallet/data/room/Account.kt
+++ b/app/src/main/java/com/concordium/wallet/data/room/Account.kt
@@ -12,6 +12,7 @@ import com.concordium.wallet.data.model.AccountDelegation
import com.concordium.wallet.data.model.AccountEncryptedAmount
import com.concordium.wallet.data.model.AccountReleaseSchedule
import com.concordium.wallet.data.model.CredentialWrapper
+import com.concordium.wallet.data.model.EncryptedData
import com.concordium.wallet.data.model.IdentityAttribute
import com.concordium.wallet.data.model.ShieldedAccountEncryptionStatus
import com.concordium.wallet.data.model.TransactionStatus
@@ -42,7 +43,7 @@ data class Account(
var transactionStatus: TransactionStatus,
@ColumnInfo(name = "encrypted_account_data")
- var encryptedAccountData: String,
+ var encryptedAccountData: EncryptedData?,
@ColumnInfo("credential")
var credential: CredentialWrapper?,
diff --git a/app/src/main/java/com/concordium/wallet/data/room/Identity.kt b/app/src/main/java/com/concordium/wallet/data/room/Identity.kt
index d1239a33..4620e7f6 100644
--- a/app/src/main/java/com/concordium/wallet/data/room/Identity.kt
+++ b/app/src/main/java/com/concordium/wallet/data/room/Identity.kt
@@ -1,6 +1,11 @@
package com.concordium.wallet.data.room
-import androidx.room.*
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverters
+import com.concordium.wallet.data.model.EncryptedData
import com.concordium.wallet.data.model.IdentityObject
import com.concordium.wallet.data.model.IdentityProvider
import com.concordium.wallet.data.room.typeconverter.IdentityTypeConverters
@@ -26,7 +31,7 @@ data class Identity(
@ColumnInfo(name = "identity_object")
var identityObject: IdentityObject?,
@ColumnInfo(name = "private_id_object_data_encrypted")
- var privateIdObjectDataEncrypted: String, // Used for V0 key creation.
+ var privateIdObjectDataEncrypted: EncryptedData?, // Used for V0 key creation.
@ColumnInfo(name = "identity_provider_id")
var identityProviderId: Int,
@ColumnInfo(name = "identity_index")
diff --git a/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt b/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt
index a3a7a877..c64d4fce 100644
--- a/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt
+++ b/app/src/main/java/com/concordium/wallet/data/room/WalletDatabase.kt
@@ -1,17 +1,20 @@
package com.concordium.wallet.data.room
import android.content.Context
+import androidx.collection.LruCache
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
+import com.concordium.wallet.core.AppCore
import com.concordium.wallet.data.room.WalletDatabase.Companion.VERSION_NUMBER
import com.concordium.wallet.data.room.migrations.MIGRATION_3_4
import com.concordium.wallet.data.room.migrations.MIGRATION_4_5
import com.concordium.wallet.data.room.migrations.MIGRATION_5_6
import com.concordium.wallet.data.room.migrations.MIGRATION_7_8
import com.concordium.wallet.data.room.migrations.MIGRATION_8_9
+import com.concordium.wallet.data.room.migrations.MIGRATION_9_10
import com.concordium.wallet.data.room.typeconverter.GlobalTypeConverters
@Database(
@@ -30,7 +33,7 @@ import com.concordium.wallet.data.room.typeconverter.GlobalTypeConverters
],
)
@TypeConverters(GlobalTypeConverters::class)
-public abstract class WalletDatabase : RoomDatabase() {
+abstract class WalletDatabase : RoomDatabase() {
abstract fun identityDao(): IdentityDao
abstract fun accountDao(): AccountDao
@@ -40,23 +43,36 @@ public abstract class WalletDatabase : RoomDatabase() {
abstract fun contractTokenDao(): ContractTokenDao
companion object {
+ const val VERSION_NUMBER = 10
+ private val instances = object : LruCache(2) {
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: WalletDatabase,
+ newValue: WalletDatabase?,
+ ) {
+ oldValue.close()
+ }
+ }
- const val VERSION_NUMBER = 9
-
- // Singleton prevents multiple instances of database opening at the same time.
- @Volatile
- private var INSTANCE: WalletDatabase? = null
+ @Deprecated(
+ message = "Do not construct instances on your own",
+ replaceWith = ReplaceWith(
+ expression = "App.appCore.session.walletStorage.database",
+ imports = arrayOf("com.concordium.wallet.App"),
+ )
+ )
+ fun getDatabase(
+ context: Context,
+ fileNameSuffix: String = "",
+ ): WalletDatabase = synchronized(this) {
+ val name = "wallet_database$fileNameSuffix"
- fun getDatabase(context: Context): WalletDatabase {
- val tempInstance = INSTANCE
- if (tempInstance != null) {
- return tempInstance
- }
- synchronized(this) {
- val instance = Room.databaseBuilder(
+ instances[name]
+ ?: Room.databaseBuilder(
context.applicationContext,
WalletDatabase::class.java,
- "wallet_database"
+ "wallet_database$fileNameSuffix"
)
.fallbackToDestructiveMigration()
// See auto migrations in the @Database declaration.
@@ -66,11 +82,13 @@ public abstract class WalletDatabase : RoomDatabase() {
MIGRATION_5_6,
MIGRATION_7_8,
MIGRATION_8_9,
+ MIGRATION_9_10(
+ context = context,
+ gson = AppCore.getGson(),
+ ),
)
.build()
- INSTANCE = instance
- return instance
- }
+ .also { instances.put(name, it) }
}
}
}
diff --git a/app/src/main/java/com/concordium/wallet/data/room/app/AppDatabase.kt b/app/src/main/java/com/concordium/wallet/data/room/app/AppDatabase.kt
new file mode 100644
index 00000000..6882f658
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/room/app/AppDatabase.kt
@@ -0,0 +1,34 @@
+package com.concordium.wallet.data.room.app
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+@Database(
+ entities = [
+ AppWalletEntity::class,
+ ],
+ version = 1,
+ exportSchema = true,
+)
+abstract class AppDatabase : RoomDatabase() {
+
+ abstract fun appWalletDao(): AppWalletDao
+
+ companion object {
+ private const val FILE_NAME = "app_database"
+ private var instance: AppDatabase? = null
+
+ fun getDatabase(context: Context): AppDatabase = synchronized(this) {
+ instance
+ ?: Room.databaseBuilder(
+ context.applicationContext,
+ AppDatabase::class.java,
+ FILE_NAME,
+ )
+ .build()
+ .also(::instance::set)
+ }
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletDao.kt b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletDao.kt
new file mode 100644
index 00000000..6724dd54
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletDao.kt
@@ -0,0 +1,49 @@
+package com.concordium.wallet.data.room.app
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+abstract class AppWalletDao {
+ @Query("SELECT * FROM wallets ORDER BY created_at ASC")
+ abstract fun getAll(): Flow>
+
+ @Query("SELECT COUNT(*) FROM wallets")
+ abstract suspend fun getCount(): Int
+
+ @Query("SELECT * FROM WALLETS WHERE is_active=1")
+ abstract suspend fun getActive(): AppWalletEntity
+
+ @Query("UPDATE wallets SET is_active = CASE WHEN id=:walletId THEN 1 ELSE 0 END")
+ abstract suspend fun activate(walletId: String)
+
+ @Insert
+ protected abstract suspend fun insert(wallet: AppWalletEntity): Long
+
+ @Transaction
+ open suspend fun insertAndActivate(wallet: AppWalletEntity) {
+ insert(wallet)
+ activate(wallet.id)
+ }
+
+ @Query("UPDATE wallets SET type=:newType WHERE id=:walletId")
+ abstract suspend fun switchType(
+ walletId: String,
+ newType: String,
+ )
+
+ @Query("DELETE FROM wallets WHERE id=:walletId")
+ protected abstract suspend fun delete(walletId: String)
+
+ @Transaction
+ open suspend fun deleteAndActivateAnother(
+ walletToDeleteId: String,
+ walletToActivateId: String,
+ ) {
+ delete(walletToDeleteId)
+ activate(walletToActivateId)
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletEntity.kt b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletEntity.kt
new file mode 100644
index 00000000..29db78b8
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/room/app/AppWalletEntity.kt
@@ -0,0 +1,36 @@
+package com.concordium.wallet.data.room.app
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import com.concordium.wallet.core.multiwallet.AppWallet
+
+@Entity(
+ tableName = "wallets",
+ indices = [
+ Index("created_at"),
+ Index("is_active"),
+ ]
+)
+class AppWalletEntity(
+ @PrimaryKey
+ @ColumnInfo(name = "id")
+ val id: String,
+ @ColumnInfo(name = "type")
+ val type: String,
+ @ColumnInfo(name = "created_at")
+ val createdAt: Long,
+ @ColumnInfo(name = "is_active")
+ val isActive: Boolean,
+) {
+ constructor(
+ wallet: AppWallet,
+ isActive: Boolean = false,
+ ) : this(
+ id = wallet.id,
+ type = wallet.type.name,
+ createdAt = wallet.createdAt.time,
+ isActive = isActive,
+ )
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/room/migrations/MIGRATION_9_10.kt b/app/src/main/java/com/concordium/wallet/data/room/migrations/MIGRATION_9_10.kt
new file mode 100644
index 00000000..5fbf7276
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/data/room/migrations/MIGRATION_9_10.kt
@@ -0,0 +1,94 @@
+package com.concordium.wallet.data.room.migrations
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.concordium.wallet.core.migration.TwoWalletsMigration
+import com.concordium.wallet.data.model.EncryptedData
+import com.concordium.wallet.data.room.typeconverter.GlobalTypeConverters
+import com.google.gson.Gson
+
+fun MIGRATION_9_10(
+ context: Context,
+ gson: Gson,
+) = object : Migration(9, 10) {
+ private val globalTypeConverters = GlobalTypeConverters()
+ private val twoWalletsMigration = TwoWalletsMigration(
+ context = context,
+ gson = gson,
+ )
+
+ override fun migrate(database: SupportSQLiteDatabase) = with(database) {
+ execSQL("CREATE TABLE IF NOT EXISTS `_new_identity_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `status` TEXT NOT NULL, `detail` TEXT, `code_uri` TEXT NOT NULL, `next_account_number` INTEGER NOT NULL, `identity_provider` TEXT NOT NULL, `identity_object` TEXT, `private_id_object_data_encrypted` TEXT, `identity_provider_id` INTEGER NOT NULL, `identity_index` INTEGER NOT NULL)")
+ execSQL("INSERT INTO `_new_identity_table` (`id`,`name`,`status`,`detail`,`code_uri`,`next_account_number`,`identity_provider`,`identity_object`,`private_id_object_data_encrypted`,`identity_provider_id`,`identity_index`) SELECT `id`,`name`,`status`,`detail`,`code_uri`,`next_account_number`,`identity_provider`,`identity_object`,`private_id_object_data_encrypted`,`identity_provider_id`,`identity_index` FROM `identity_table`")
+ execSQL("DROP TABLE `identity_table`")
+ execSQL("ALTER TABLE `_new_identity_table` RENAME TO `identity_table`")
+ execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_identity_table_identity_provider_id_identity_index` ON `identity_table` (`identity_provider_id`, `identity_index`)")
+
+ // Migrate identity encrypted data:
+ // wrap the existing data into EncryptedData, or overwrite with null if missing.
+ database.query("SELECT `id`, `private_id_object_data_encrypted` FROM `identity_table`")
+ .use { identitiesCursor ->
+ while (identitiesCursor.moveToNext()) {
+ val oldPrivateIdObjectDataEncrypted = identitiesCursor.getString(1)
+ ?.takeIf(String::isNotEmpty)
+ val identityId = identitiesCursor.getInt(0)
+ val migratedPrivateIdObjectDataEncrypted: EncryptedData? =
+ oldPrivateIdObjectDataEncrypted
+ ?.let(twoWalletsMigration::migrateOldEncryptedData)
+
+ update(
+ "identity_table",
+ SQLiteDatabase.CONFLICT_FAIL,
+ ContentValues(1).apply {
+ put(
+ "private_id_object_data_encrypted",
+ globalTypeConverters.encryptedDataToJson(
+ migratedPrivateIdObjectDataEncrypted
+ )
+ )
+ },
+ "`id`=?",
+ arrayOf(identityId)
+ )
+ }
+ }
+
+ execSQL("CREATE TABLE IF NOT EXISTS `_new_account_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `identity_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `submission_id` TEXT NOT NULL, `transaction_status` INTEGER NOT NULL, `encrypted_account_data` TEXT, `credential` TEXT, `cred_number` INTEGER NOT NULL, `revealed_attributes` TEXT NOT NULL, `finalized_balance` TEXT NOT NULL, `balance_at_disposal` TEXT NOT NULL, `total_shielded_balance` TEXT NOT NULL, `finalized_encrypted_balance` TEXT, `current_balance_status` INTEGER NOT NULL, `read_only` INTEGER NOT NULL, `finalized_account_release_schedule` TEXT, `cooldowns` TEXT NOT NULL, `account_delegation` TEXT, `account_baker` TEXT, `accountIndex` INTEGER)")
+ execSQL("INSERT INTO `_new_account_table` (`id`,`identity_id`,`name`,`address`,`submission_id`,`transaction_status`,`encrypted_account_data`,`credential`,`cred_number`,`revealed_attributes`,`finalized_balance`,`balance_at_disposal`,`total_shielded_balance`,`finalized_encrypted_balance`,`current_balance_status`,`read_only`,`finalized_account_release_schedule`,`cooldowns`,`account_delegation`,`account_baker`,`accountIndex`) SELECT `id`,`identity_id`,`name`,`address`,`submission_id`,`transaction_status`,`encrypted_account_data`,`credential`,`cred_number`,`revealed_attributes`,`finalized_balance`,`balance_at_disposal`,`total_shielded_balance`,`finalized_encrypted_balance`,`current_balance_status`,`read_only`,`finalized_account_release_schedule`,`cooldowns`,`account_delegation`,`account_baker`,`accountIndex` FROM `account_table`")
+ execSQL("DROP TABLE `account_table`")
+ execSQL("ALTER TABLE `_new_account_table` RENAME TO `account_table`")
+ execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_account_table_address` ON `account_table` (`address`)")
+
+ // Migrate account encrypted data:
+ // wrap the existing data into EncryptedData, or overwrite with null if missing.
+ database.query("SELECT `id`, `encrypted_account_data` FROM `account_table`")
+ .use { accountsCursor ->
+ while (accountsCursor.moveToNext()) {
+ val oldEncryptedAccountData = accountsCursor.getString(1)
+ ?.takeIf(String::isNotEmpty)
+ val accountId = accountsCursor.getInt(0)
+ val migratedEncryptedAccountData: EncryptedData? =
+ oldEncryptedAccountData
+ ?.let(twoWalletsMigration::migrateOldEncryptedData)
+
+ update(
+ "account_table",
+ SQLiteDatabase.CONFLICT_FAIL,
+ ContentValues(1).apply {
+ put(
+ "encrypted_account_data",
+ globalTypeConverters.encryptedDataToJson(
+ migratedEncryptedAccountData
+ )
+ )
+ },
+ "`id`=?",
+ arrayOf(accountId)
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt b/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt
index cc079715..e8d0819a 100644
--- a/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt
+++ b/app/src/main/java/com/concordium/wallet/data/room/typeconverter/GlobalTypeConverters.kt
@@ -1,14 +1,16 @@
package com.concordium.wallet.data.room.typeconverter
import androidx.room.TypeConverter
+import com.concordium.wallet.core.AppCore
+import com.concordium.wallet.data.model.EncryptedData
import com.concordium.wallet.data.model.ShieldedAccountEncryptionStatus
-import com.concordium.wallet.data.model.TransactionOriginType
import com.concordium.wallet.data.model.TransactionOutcome
import com.concordium.wallet.data.model.TransactionStatus
import com.concordium.wallet.util.toBigInteger
import java.math.BigInteger
class GlobalTypeConverters {
+ private val gson = AppCore.getGson()
@TypeConverter
fun intToTransactionStatus(value: Int): TransactionStatus {
@@ -41,22 +43,6 @@ class GlobalTypeConverters {
return transactionOutcome.code
}
- @TypeConverter
- fun intToTransactionOriginType(value: Int): TransactionOriginType {
- return when (value) {
- 0 -> TransactionOriginType.Self
- 1 -> TransactionOriginType.Account
- 2 -> TransactionOriginType.Reward
- 3 -> TransactionOriginType.None
- else -> TransactionOriginType.UNKNOWN
- }
- }
-
- @TypeConverter
- fun transactionOriginTypeToInt(transactionOriginType: TransactionOriginType): Int {
- return transactionOriginType.code
- }
-
@TypeConverter
fun intToShieldedAccountEncryptionStatus(value: Int): ShieldedAccountEncryptionStatus {
return when (value) {
@@ -81,4 +67,16 @@ class GlobalTypeConverters {
fun stringToBigInteger(value: String?): BigInteger? {
return value?.toBigInteger()
}
-}
\ No newline at end of file
+
+ @TypeConverter
+ fun encryptedDataToJson(value: EncryptedData?): String? {
+ return value?.let(gson::toJson)
+ }
+
+ @TypeConverter
+ fun jsonToEncryptedData(value: String?): EncryptedData? {
+ return value?.takeIf(String::isNotEmpty)?.let {
+ gson.fromJson(it, EncryptedData::class.java)
+ }
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt b/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt
index 005e27f5..ba197141 100644
--- a/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt
+++ b/app/src/main/java/com/concordium/wallet/data/util/ExportEncryptionHelper.kt
@@ -1,10 +1,12 @@
package com.concordium.wallet.data.util
+import android.security.keystore.KeyProperties
import android.util.Base64
import com.concordium.wallet.core.security.EncryptionException
import com.concordium.wallet.core.security.EncryptionHelper
import com.concordium.wallet.data.export.EncryptedExportData
import com.concordium.wallet.data.export.ExportEncryptionMetaData
+import com.concordium.wallet.data.model.EncryptedData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -14,39 +16,71 @@ object ExportEncryptionHelper {
* @exception EncryptionException
*/
@Throws(EncryptionException::class)
- suspend fun encryptExportData(password: String, toBeEncrypted: String): EncryptedExportData = withContext(Dispatchers.Default) {
- val iterations = 100000
- val (salt, iv) = EncryptionHelper.createEncryptionData()
- val saltEncoded = Base64.encodeToString(salt, Base64.NO_WRAP)
- val ivEncoded = Base64.encodeToString(iv, Base64.NO_WRAP)
- val encryptionMetaData =
- ExportEncryptionMetaData(
- "AES-256",
- "PBKDF2WithHmacSHA256",
- iterations,
- saltEncoded,
- ivEncoded
- )
- val key = EncryptionHelper.generateKey(password, salt, iterations)
- // Encrypt (using NO_WRAP to avoid line break in the end)
- val cipherText = EncryptionHelper.encrypt(key, iv, toBeEncrypted, Base64.NO_WRAP)
- val encryptedExportData = EncryptedExportData(encryptionMetaData, cipherText)
- return@withContext encryptedExportData
+ suspend fun encryptExportData(
+ password: String,
+ toBeEncrypted: String,
+ ): EncryptedExportData = withContext(Dispatchers.Default) {
+ // This method must remain backward-compatible with the Concordium file export,
+ // therefore it uses own encryption params and Base64 encoding.
+
+ val salt = EncryptionHelper.generatePasswordKeySalt()
+ val key = EncryptionHelper.generatePasswordKey(
+ password = password.toCharArray(),
+ salt = salt,
+ sizeBits = ENCRYPTION_KEY_SIZE_BITS,
+ kdf = KDF,
+ kdfIterationCount = KDF_ITERATION_COUNT,
+ )
+ val encryptedData = EncryptionHelper.encrypt(
+ key = key,
+ data = toBeEncrypted.toByteArray(),
+ cipherTransformation = CIPHER_TRANSFORMATION,
+ )
+
+ EncryptedExportData(
+ metadata = ExportEncryptionMetaData(
+ encryptionMethod = ENCRYPTION_METHOD,
+ keyDerivationMethod = KDF,
+ iterations = KDF_ITERATION_COUNT,
+ salt = Base64.encodeToString(salt, Base64.NO_WRAP),
+ initializationVector = Base64.encodeToString(
+ encryptedData.decodeIv(),
+ Base64.NO_WRAP
+ ),
+ ),
+ cipherText = Base64.encodeToString(encryptedData.decodeCiphertext(), Base64.NO_WRAP),
+ )
}
@Throws(EncryptionException::class)
- suspend fun decryptExportData(password: String, encryptedExportData: EncryptedExportData): String = withContext(Dispatchers.Default) {
- val toBeEncryptedEncoded = encryptedExportData.cipherText
- val toBeEncrypted = Base64.decode(toBeEncryptedEncoded, Base64.DEFAULT)
- val iterations = encryptedExportData.metadata.iterations
- val saltEncoded = encryptedExportData.metadata.salt
- val ivEncoded = encryptedExportData.metadata.initializationVector
- val salt = Base64.decode(saltEncoded, Base64.DEFAULT)
- val iv = Base64.decode(ivEncoded, Base64.DEFAULT)
- // Assume encryptionMethod: AES-256 and keyDerivationMethod: PBKDF2WithHmacSHA256
- // Because that is the only thing we support
- val key = EncryptionHelper.generateKey(password, salt, iterations)
- val decrypted = EncryptionHelper.decrypt(key, iv, toBeEncrypted)
- return@withContext decrypted
+ suspend fun decryptExportData(
+ password: String,
+ encryptedExportData: EncryptedExportData,
+ ): String = withContext(Dispatchers.Default) {
+ val key = EncryptionHelper.generatePasswordKey(
+ password = password.toCharArray(),
+ salt = Base64.decode(encryptedExportData.metadata.salt, Base64.DEFAULT),
+ kdf = encryptedExportData.metadata.keyDerivationMethod,
+ kdfIterationCount = encryptedExportData.metadata.iterations,
+ sizeBits = ENCRYPTION_KEY_SIZE_BITS,
+ )
+ val wrappedEncryptedExportData = EncryptedData(
+ ciphertext = Base64.decode(encryptedExportData.cipherText, Base64.DEFAULT),
+ iv = Base64.decode(encryptedExportData.metadata.initializationVector, Base64.DEFAULT),
+ transformation = CIPHER_TRANSFORMATION,
+ )
+
+ EncryptionHelper.decrypt(
+ key = key,
+ encryptedData = wrappedEncryptedExportData,
+ ).let(::String)
}
-}
\ No newline at end of file
+
+ private const val KDF_ITERATION_COUNT = 100000
+ private const val KDF = "PBKDF2WithHmacSHA256"
+ private const val ENCRYPTION_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
+ private const val ENCRYPTION_KEY_SIZE_BITS = 256
+ private const val ENCRYPTION_METHOD = "$ENCRYPTION_KEY_ALGORITHM-$ENCRYPTION_KEY_SIZE_BITS"
+ private const val CIPHER_TRANSFORMATION =
+ "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/PKCS7Padding"
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt b/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt
index 3fa80fd5..1a2a320c 100644
--- a/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/MainActivity.kt
@@ -7,11 +7,13 @@ import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
import androidx.lifecycle.lifecycleScope
import com.concordium.wallet.App
import com.concordium.wallet.BuildConfig
import com.concordium.wallet.R
import com.concordium.wallet.databinding.ActivityMainBinding
+import com.concordium.wallet.extension.collectWhenStarted
import com.concordium.wallet.ui.account.accountsoverview.AccountsOverviewFragment
import com.concordium.wallet.ui.auth.login.AuthLoginActivity
import com.concordium.wallet.ui.base.BaseActivity
@@ -21,12 +23,14 @@ import com.concordium.wallet.ui.common.delegates.IdentityStatusDelegate
import com.concordium.wallet.ui.common.delegates.IdentityStatusDelegateImpl
import com.concordium.wallet.ui.more.import.ImportActivity
import com.concordium.wallet.ui.more.moreoverview.MoreOverviewFragment
+import com.concordium.wallet.ui.multiwallet.WalletSwitchViewModel
import com.concordium.wallet.ui.news.NewsOverviewFragment
import com.concordium.wallet.ui.onboarding.OnboardingSharedViewModel
import com.concordium.wallet.ui.tokens.provider.ProvidersOverviewFragment
import com.concordium.wallet.ui.walletconnect.WalletConnectView
import com.concordium.wallet.ui.walletconnect.WalletConnectViewModel
import com.concordium.wallet.ui.welcome.WelcomeActivity
+import com.concordium.wallet.ui.welcome.WelcomeRecoverWalletActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
@@ -35,6 +39,8 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
IdentityStatusDelegate by IdentityStatusDelegateImpl() {
companion object {
+ const val EXTRA_IMPORT_FROM_FILE = "EXTRA_IMPORT_FROM_FILE"
+ const val EXTRA_IMPORT_FROM_SEED = "EXTRA_IMPORT_FROM_SEED"
const val EXTRA_WALLET_CONNECT_URI = "wc_uri"
}
@@ -44,6 +50,7 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
private lateinit var viewModel: MainViewModel
private lateinit var onboardingViewModel: OnboardingSharedViewModel
private lateinit var walletConnectViewModel: WalletConnectViewModel
+ private lateinit var walletSwitchViewModel: WalletSwitchViewModel
private var hasHandledPossibleImportFile = false
//region Lifecycle
@@ -73,6 +80,12 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
}
handlePossibleWalletConnectUri(intent)
+
+ if (intent.getBooleanExtra(EXTRA_IMPORT_FROM_FILE, false)) {
+ goToImportFromFile()
+ } else if (intent.getBooleanExtra(EXTRA_IMPORT_FROM_SEED, false)) {
+ goToImportFromSeed()
+ }
}
override fun onResume() {
@@ -146,11 +159,12 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
// ************************************************************
private fun initializeViewModel() {
- viewModel = ViewModelProvider(
+ val viewModelProvider = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[MainViewModel::class.java]
+ )
+ viewModel = viewModelProvider.get()
viewModel.titleLiveData.observe(this, this::setActionBarTitle)
viewModel.stateLiveData.observe(this) { state ->
checkNotNull(state)
@@ -161,15 +175,15 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
replaceFragment(state)
}
- onboardingViewModel = ViewModelProvider(
- this,
- ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[OnboardingSharedViewModel::class.java]
+ walletConnectViewModel = viewModelProvider.get()
+ onboardingViewModel = viewModelProvider.get()
- walletConnectViewModel = ViewModelProvider(
- this,
- ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[WalletConnectViewModel::class.java]
+ walletSwitchViewModel = viewModelProvider.get()
+ walletSwitchViewModel.switchesFlow.collectWhenStarted(this) {
+ // Force restart the activity with recreation of view models.
+ finishAffinity()
+ startActivity(Intent(this, MainActivity::class.java))
+ }
}
private fun initializeViews() {
@@ -187,6 +201,8 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
authDelegate = this,
viewModel = walletConnectViewModel,
).init()
+
+ binding.walletSwitchView.bind(walletSwitchViewModel)
}
//endregion
@@ -276,5 +292,18 @@ class MainActivity : BaseActivity(R.layout.activity_main, R.string.accounts_over
walletConnectViewModel.handleWcUri(walletConnectUri)
}
}
+
+ private fun goToImportFromFile() {
+ val intent = Intent(this, ImportActivity::class.java)
+ startActivity(intent)
+ }
+
+ private fun goToImportFromSeed() {
+ val intent = Intent(this, WelcomeRecoverWalletActivity::class.java)
+ intent.putExtras(WelcomeRecoverWalletActivity.getBundle(
+ showFileOptions = false,
+ ))
+ startActivity(intent)
+ }
//endregion
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt
index 3d06bf9e..95dbb8ef 100644
--- a/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/MainViewModel.kt
@@ -6,11 +6,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.concordium.wallet.App
-import com.concordium.wallet.core.authentication.Session
-import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.Identity
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.common.identity.IdentityUpdater
import com.concordium.wallet.util.Log
import kotlinx.coroutines.launch
@@ -25,9 +22,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
;
}
- private val identityRepository: IdentityRepository
private val identityUpdater = IdentityUpdater(application, viewModelScope)
- private val session: Session = App.appCore.session
var databaseVersionAllowed = true
@@ -42,15 +37,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val canAcceptImportFiles: Boolean
get() = App.appCore.session.isAccountsBackupPossible()
- init {
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- }
-
fun initialize() {
try {
val dbVersion =
- WalletDatabase.getDatabase(getApplication()).openHelper.readableDatabase.version.toString()
+ App.appCore.session.walletStorage.database.openHelper.readableDatabase.version.toString()
} catch (e: Exception) {
Log.e("Database init failed. Missing migrations?")
e.printStackTrace()
@@ -78,19 +68,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
fun shouldShowAuthentication(): Boolean {
- session.isLoggedIn.value.let { return it == null || it == false }
+ App.appCore.session.isLoggedIn.value.let { return it == null || it == false }
}
fun shouldShowPasswordSetup(): Boolean {
- return !session.hasSetupPassword
+ return !App.appCore.setup.isAuthSetupCompleted
}
fun shouldShowInitialSetup(): Boolean {
- return session.hasSetupPassword && !session.hasCompletedInitialSetup
+ return !App.appCore.setup.isInitialSetupCompleted
}
fun hasCompletedOnboarding(): Boolean {
- return session.hasCompleteOnboarding
+ return App.appCore.session.walletStorage.setupPreferences.getHasCompletedOnboarding()
}
fun startIdentityUpdate() {
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt
index 6eaa2d1a..efe79403 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountDetailsViewModel.kt
@@ -9,7 +9,7 @@ import androidx.lifecycle.viewModelScope
import com.concordium.wallet.App
import com.concordium.wallet.BuildConfig
import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
+import com.concordium.wallet.core.Session
import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.RecipientRepository
@@ -24,7 +24,6 @@ import com.concordium.wallet.data.model.TransactionType
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.Identity
import com.concordium.wallet.data.room.Transfer
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.data.util.toTransaction
import com.concordium.wallet.ui.account.accountdetails.transfers.AdapterItem
import com.concordium.wallet.ui.account.accountdetails.transfers.HeaderItem
@@ -40,25 +39,17 @@ import java.math.BigInteger
import java.util.Date
class AccountDetailsViewModel(application: Application) : AndroidViewModel(application) {
- private fun MutableLiveData.forceRefresh() {
- this.value = this.value
- }
-
private val session: Session = App.appCore.session
- var hasTransactionsToDecrypt: Boolean = false
-
lateinit var account: Account
var hasPendingDelegationTransactions: Boolean = false
var hasPendingBakingTransactions: Boolean = false
private val proxyRepository = ProxyRepository()
- private val accountRepository: AccountRepository
- private val transferRepository: TransferRepository
- private val identityRepository: IdentityRepository
- private val recipientRepository: RecipientRepository
-
- private val gson = App.appCore.gson
+ private val accountRepository = AccountRepository(session.walletStorage.database.accountDao())
+ private val transferRepository = TransferRepository(session.walletStorage.database.transferDao())
+ private val identityRepository = IdentityRepository(session.walletStorage.database.identityDao())
+ private val recipientRepository = RecipientRepository(session.walletStorage.database.recipientDao())
private lateinit var transactionMappingHelper: TransactionMappingHelper
private val accountUpdater = AccountUpdater(application, viewModelScope)
@@ -111,14 +102,6 @@ class AccountDetailsViewModel(application: Application) : AndroidViewModel(appli
get() = _accountUpdatedLiveData
init {
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
- val transferDao = WalletDatabase.getDatabase(application).transferDao()
- transferRepository = TransferRepository(transferDao)
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- val recipientDao = WalletDatabase.getDatabase(application).recipientDao()
- recipientRepository = RecipientRepository(recipientDao)
initializeAccountUpdater()
_transferListLiveData.observeForever {
@@ -366,7 +349,7 @@ class AccountDetailsViewModel(application: Application) : AndroidViewModel(appli
}
private fun addToTransactionList(newTransactions: List) {
- val transferList = _transferListLiveData.value ?: ArrayList()
+ val transferList = _transferListLiveData.value ?: ArrayList()
val adapterList = transferList.toMutableList()
for (ta in newTransactions) {
val isAfterHeader = checkToAddHeaderItem(adapterList, ta)
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt
index ae0399f8..08b06d4f 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountSettingsViewModel.kt
@@ -4,10 +4,10 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
+import com.concordium.wallet.App
import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.RecipientRepository
import com.concordium.wallet.data.room.Account
-import com.concordium.wallet.data.room.WalletDatabase
import kotlinx.coroutines.launch
class AccountSettingsViewModel(application: Application) : AndroidViewModel(application) {
@@ -21,9 +21,9 @@ class AccountSettingsViewModel(application: Application) : AndroidViewModel(appl
fun changeAccountName(name: String) {
val accountRepository =
- AccountRepository(WalletDatabase.getDatabase(getApplication()).accountDao())
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
val recipientRepository =
- RecipientRepository(WalletDatabase.getDatabase(getApplication()).recipientDao())
+ RecipientRepository(App.appCore.session.walletStorage.database.recipientDao())
viewModelScope.launch {
account.name = name
accountRepository.update(account)
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt
index a7956926..4fede94e 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/accountdetails/AccountTransactionsFiltersActivity.kt
@@ -3,7 +3,7 @@ package com.concordium.wallet.ui.account.accountdetails
import android.os.Bundle
import com.concordium.wallet.App
import com.concordium.wallet.R
-import com.concordium.wallet.core.authentication.Session
+import com.concordium.wallet.core.Session
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.databinding.ActivityAccountTransactionFiltersBinding
import com.concordium.wallet.ui.base.BaseActivity
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt
index 679cc20d..cfb35b68 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewFragment.kt
@@ -20,7 +20,6 @@ import com.concordium.wallet.App
import com.concordium.wallet.R
import com.concordium.wallet.core.arch.EventObserver
import com.concordium.wallet.data.model.Token
-import com.concordium.wallet.data.preferences.AuthPreferences
import com.concordium.wallet.data.preferences.Preferences
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.util.CurrencyUtil
@@ -36,11 +35,12 @@ import com.concordium.wallet.ui.base.BaseActivity
import com.concordium.wallet.ui.base.BaseFragment
import com.concordium.wallet.ui.cis2.SendTokenActivity
import com.concordium.wallet.ui.more.export.ExportActivity
+import com.concordium.wallet.ui.multiwallet.FileWalletCreationLimitationDialog
+import com.concordium.wallet.ui.multiwallet.WalletsActivity
import com.concordium.wallet.ui.onboarding.OnboardingFragment
import com.concordium.wallet.ui.onboarding.OnboardingSharedViewModel
import com.concordium.wallet.ui.onboarding.OnboardingState
import com.concordium.wallet.ui.onramp.CcdOnrampSitesActivity
-import com.concordium.wallet.util.KeyCreationVersion
import com.concordium.wallet.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -58,7 +58,6 @@ class AccountsOverviewFragment : BaseFragment() {
private lateinit var viewModel: AccountsOverviewViewModel
private lateinit var mainViewModel: MainViewModel
private lateinit var onboardingViewModel: OnboardingSharedViewModel
- private lateinit var keyCreationVersion: KeyCreationVersion
private lateinit var onboardingStatusCard: OnboardingFragment
//region Lifecycle
@@ -68,7 +67,6 @@ class AccountsOverviewFragment : BaseFragment() {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
- keyCreationVersion = KeyCreationVersion(AuthPreferences(requireContext()))
}
override fun onCreateView(
@@ -91,8 +89,13 @@ class AccountsOverviewFragment : BaseFragment() {
val baseActivity = (activity as BaseActivity)
- baseActivity.hideLeftPlus(isVisible = true) {
- if (mainViewModel.hasCompletedOnboarding()) {
+ baseActivity.hideLeftPlus(
+ isVisible = true,
+ hasNotice = viewModel.isCreationLimitedForFileWallet,
+ ) {
+ if (viewModel.isCreationLimitedForFileWallet) {
+ showFileWalletCreationLimitation()
+ } else if (mainViewModel.hasCompletedOnboarding()) {
gotoCreateAccount()
} else {
baseActivity.showUnlockFeatureDialog()
@@ -198,7 +201,7 @@ class AccountsOverviewFragment : BaseFragment() {
}
viewModel.onrampActive.collectWhenStarted(viewLifecycleOwner) { active ->
if (active) {
- if (!viewModel.hasShowedInitialAnimation()) {
+ if (!viewModel.hasShownInitialAnimation()) {
binding.confettiAnimation.visibility = View.VISIBLE
binding.confettiAnimation.playAnimation()
}
@@ -225,6 +228,11 @@ class AccountsOverviewFragment : BaseFragment() {
onboardingViewModel.setIdentity(identity)
}
+ viewModel.fileWalletMigrationVisible.collectWhenStarted(
+ viewLifecycleOwner,
+ binding.fileWalletMigrationDisclaimerLayout::isVisible::set
+ )
+
onboardingViewModel.identityFlow.collectWhenStarted(viewLifecycleOwner) { identity ->
onboardingStatusCard.updateViewsByIdentityStatus(identity)
}
@@ -264,6 +272,10 @@ class AccountsOverviewFragment : BaseFragment() {
App.appCore.session.addAccountsBackedUpListener(it)
}
+ binding.fileWalletMigrationDisclaimerLayout.setOnClickListener {
+ startActivity(Intent(requireActivity(), WalletsActivity::class.java))
+ }
+
initializeAnimation()
initializeList()
updateMissingBackup()
@@ -271,7 +283,7 @@ class AccountsOverviewFragment : BaseFragment() {
private fun updateMissingBackup() = viewModel.viewModelScope.launch(Dispatchers.Main) {
binding.missingBackup.isVisible = App.appCore.session.run {
- isAccountsBackupPossible() && !isAccountsBackedUp()
+ isAccountsBackupPossible() && !areAccountsBackedUp()
}
}
@@ -389,6 +401,13 @@ class AccountsOverviewFragment : BaseFragment() {
startActivity(intent)
}
+ private fun showFileWalletCreationLimitation() {
+ FileWalletCreationLimitationDialog().showSingle(
+ childFragmentManager,
+ FileWalletCreationLimitationDialog.TAG
+ )
+ }
+
private fun showWaiting(waiting: Boolean) {
if (waiting) {
binding.progress.progressLayout.visibility = View.VISIBLE
@@ -439,7 +458,7 @@ class AccountsOverviewFragment : BaseFragment() {
}
private fun cancelAnimation() {
- viewModel.setHasShowedInitialAnimation()
+ viewModel.setHasShownInitialAnimation()
binding.confettiAnimation.visibility = View.GONE
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt
index 124889af..6e2dfb5b 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/AccountsOverviewViewModel.kt
@@ -10,21 +10,18 @@ import androidx.lifecycle.viewModelScope
import com.concordium.wallet.App
import com.concordium.wallet.BuildConfig
import com.concordium.wallet.core.arch.Event
+import com.concordium.wallet.core.multiwallet.AppWallet
import com.concordium.wallet.core.notifications.UpdateNotificationsSubscriptionUseCase
import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.model.TransactionStatus
-import com.concordium.wallet.data.preferences.AuthPreferences
-import com.concordium.wallet.data.preferences.NotificationsPreferences
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.AccountWithIdentity
import com.concordium.wallet.data.room.Identity
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.account.common.accountupdater.AccountUpdater
import com.concordium.wallet.ui.account.common.accountupdater.TotalBalancesData
import com.concordium.wallet.ui.onboarding.OnboardingState
import com.concordium.wallet.ui.onramp.CcdOnrampSiteRepository
-import com.concordium.wallet.util.KeyCreationVersion
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -52,6 +49,9 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
private val _onrampActive = MutableStateFlow(false)
val onrampActive = _onrampActive.asStateFlow()
+ private val _fileWalletMigrationVisible = MutableStateFlow(false)
+ val fileWalletMigrationVisible = _fileWalletMigrationVisible.asSharedFlow()
+
private val zeroAccountBalances = TotalBalancesData(
BigInteger.ZERO,
BigInteger.ZERO,
@@ -70,29 +70,24 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
private val _listItemsLiveData = MutableLiveData>()
val listItemsLiveData: LiveData> = _listItemsLiveData
- private val identityRepository: IdentityRepository
- private val accountRepository: AccountRepository
+ private val identityRepository =
+ IdentityRepository(App.appCore.session.walletStorage.database.identityDao())
+ private val accountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
private val accountUpdater = AccountUpdater(application, viewModelScope)
- private val keyCreationVersion: KeyCreationVersion
private val ccdOnrampSiteRepository: CcdOnrampSiteRepository
private val accountsObserver: Observer>
- private val notificationsPreferences: NotificationsPreferences
+ private val updateNotificationsSubscriptionUseCase by lazy(::UpdateNotificationsSubscriptionUseCase)
+ val isCreationLimitedForFileWallet: Boolean
+ get() = App.appCore.session.activeWallet.type == AppWallet.Type.FILE
private var updater: CountDownTimer? = null
- private val updateNotificationsSubscriptionUseCase by lazy {
- UpdateNotificationsSubscriptionUseCase(application)
- }
-
enum class DialogToShow {
UNSHIELDING,
;
}
init {
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
accountUpdater.setUpdateListener(object : AccountUpdater.UpdateListener {
override fun onDone(totalBalances: TotalBalancesData) {
_waitingLiveData.postValue(false)
@@ -115,13 +110,12 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
_errorLiveData.postValue(Event(stringRes))
}
})
- keyCreationVersion = KeyCreationVersion(AuthPreferences(application))
ccdOnrampSiteRepository = CcdOnrampSiteRepository()
accountsObserver = Observer { accountsWithIdentity ->
postListItems(accountsWithIdentity)
}
accountRepository.allAccountsWithIdentity.observeForever(accountsObserver)
- notificationsPreferences = NotificationsPreferences(application)
+ _fileWalletMigrationVisible.tryEmit(App.appCore.session.activeWallet.type == AppWallet.Type.FILE)
updateNotificationSubscription()
}
@@ -131,31 +125,36 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
accountUpdater.dispose()
}
- fun updateState(notifyWaitingLiveData: Boolean = true) {
+ fun updateState(notifyWaitingLiveData: Boolean = true) = viewModelScope.launch(Dispatchers.IO) {
// Decide what state to show (visible buttons based on if there is any identities and accounts)
// Also update all accounts (and set the overall balance) if any exists.
- viewModelScope.launch(Dispatchers.IO) {
- if (!keyCreationVersion.useV1) {
- checkFileWallet(notifyWaitingLiveData)
- } else {
- val identityCount = identityRepository.getCount()
- if (identityCount == 0) {
- postState(
- OnboardingState.VERIFY_IDENTITY,
- zeroAccountBalances,
- notifyWaitingLiveData
- )
- } else {
- updateIdentityStatus(notifyWaitingLiveData)
- }
- }
+
+ if (App.appCore.session.activeWallet.type == AppWallet.Type.FILE) {
+ postState(OnboardingState.DONE, notifyWaitingLiveData = notifyWaitingLiveData)
+ updateSubmissionStatesAndBalances()
+ return@launch
+ }
+
+ when {
+ !App.appCore.session.walletStorage.setupPreferences.hasEncryptedSeed() ->
+ postState(OnboardingState.SAVE_PHRASE, zeroAccountBalances, notifyWaitingLiveData)
+
+ identityRepository.getCount() == 0 ->
+ postState(
+ OnboardingState.VERIFY_IDENTITY,
+ zeroAccountBalances,
+ notifyWaitingLiveData
+ )
+
+ else ->
+ updateIdentityStatus(notifyWaitingLiveData)
}
}
private suspend fun postState(
state: OnboardingState,
balances: TotalBalancesData = zeroAccountBalances,
- notifyWaitingLiveData: Boolean = true
+ notifyWaitingLiveData: Boolean = true,
) {
_stateFlow.emit(state)
_totalBalanceLiveData.postValue(balances)
@@ -193,22 +192,10 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
}
}
- private suspend fun checkFileWallet(notifyWaitingLiveData: Boolean) {
- val doneCount = identityRepository.getAllDone().size
- if (doneCount > 0) {
- App.appCore.session.hasCompletedOnboarding()
- postState(OnboardingState.DONE, notifyWaitingLiveData = notifyWaitingLiveData)
- showSingleDialogIfNeeded()
- updateSubmissionStatesAndBalances()
- } else {
- postState(OnboardingState.SAVE_PHRASE, zeroAccountBalances, notifyWaitingLiveData)
- }
- }
-
private suspend fun handleAccountCreation(notifyWaitingLiveData: Boolean) {
val allAccounts = accountRepository.getAll()
if (allAccounts.any { it.transactionStatus == TransactionStatus.FINALIZED }) {
- App.appCore.session.hasCompletedOnboarding()
+ App.appCore.session.walletStorage.setupPreferences.setHasCompletedOnboarding(true)
postState(OnboardingState.DONE, notifyWaitingLiveData = notifyWaitingLiveData)
showSingleDialogIfNeeded()
} else {
@@ -237,12 +224,12 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
updater = null
}
- fun hasShowedInitialAnimation(): Boolean {
- return App.appCore.session.getHasShowedInitialAnimation()
+ fun hasShownInitialAnimation(): Boolean {
+ return App.appCore.session.walletStorage.setupPreferences.getHasShownInitialAnimation()
}
- fun setHasShowedInitialAnimation() {
- App.appCore.session.setHasShowedInitialAnimation()
+ fun setHasShownInitialAnimation() {
+ return App.appCore.session.walletStorage.setupPreferences.setHasShownInitialAnimation(true)
}
private fun startUpdater(countdownInterval: Long = BuildConfig.ACCOUNT_UPDATE_FREQUENCY_SEC) {
@@ -278,11 +265,6 @@ class AccountsOverviewViewModel(application: Application) : AndroidViewModel(app
return false
}
- fun checkUsingV1KeyCreation() =
- check(keyCreationVersion.useV1) {
- "Key creation V1 (seed-based) must be used to perform this action"
- }
-
private fun showSingleDialogIfNeeded() = viewModelScope.launch(Dispatchers.IO) {
val dialogsToShow = linkedSetOf()
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt
index c9b580b8..87d1d6ee 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/accountsoverview/UnshieldingNoticeDialog.kt
@@ -40,7 +40,7 @@ class UnshieldingNoticeDialog : AppCompatDialogFragment() {
// Track showing the notice once it is visible to the user.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
delay(500)
- App.appCore.session.unshieldingNoticeShown()
+ App.appCore.session.setUnshieldingNoticeShown()
}
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt
index a430aef6..6df25a03 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/common/NewAccountViewModel.kt
@@ -13,31 +13,24 @@ import com.concordium.wallet.core.backend.BackendError
import com.concordium.wallet.core.backend.BackendErrorException
import com.concordium.wallet.core.backend.BackendRequest
import com.concordium.wallet.data.AccountRepository
-import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.RecipientRepository
import com.concordium.wallet.data.backend.repository.IdentityProviderRepository
import com.concordium.wallet.data.backend.repository.ProxyRepository
-import com.concordium.wallet.data.cryptolib.CreateCredentialInput
import com.concordium.wallet.data.cryptolib.CreateCredentialInputV1
import com.concordium.wallet.data.cryptolib.CreateCredentialOutput
-import com.concordium.wallet.data.cryptolib.GenerateAccountsInput
import com.concordium.wallet.data.cryptolib.StorageAccountData
import com.concordium.wallet.data.model.AccountSubmissionStatus
import com.concordium.wallet.data.model.CredentialWrapper
+import com.concordium.wallet.data.model.EncryptedData
import com.concordium.wallet.data.model.GlobalParams
import com.concordium.wallet.data.model.GlobalParamsWrapper
-import com.concordium.wallet.data.model.PossibleAccount
-import com.concordium.wallet.data.model.RawJson
import com.concordium.wallet.data.model.SubmissionData
import com.concordium.wallet.data.model.TransactionStatus
-import com.concordium.wallet.data.preferences.AuthPreferences
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.Identity
import com.concordium.wallet.data.room.Recipient
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.common.BackendErrorHandler
import com.concordium.wallet.util.DateTimeUtil
-import com.concordium.wallet.util.KeyCreationVersion
import com.concordium.wallet.util.Log
import com.google.gson.JsonArray
import kotlinx.coroutines.Dispatchers
@@ -48,11 +41,11 @@ open class NewAccountViewModel(application: Application) :
private val identityProviderRepository = IdentityProviderRepository()
private val proxyRepository = ProxyRepository()
- private val identityRepository: IdentityRepository
- private val accountRepository: AccountRepository
- private val recipientRepository: RecipientRepository
+ private val accountRepository: AccountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
+ private val recipientRepository: RecipientRepository =
+ RecipientRepository(App.appCore.session.walletStorage.database.recipientDao())
private val gson = App.appCore.gson
- private val keyCreationVersion = KeyCreationVersion(AuthPreferences(App.appContext))
private var globalParamsRequest: BackendRequest? = null
private var submitCredentialRequest: BackendRequest? = null
@@ -91,20 +84,11 @@ open class NewAccountViewModel(application: Application) :
var globalParams: GlobalParams? = null
var submissionId: String? = null
var accountAddress: String? = null
- var encryptedAccountData: String? = null
+ var encryptedAccountData: EncryptedData? = null
var credential: CredentialWrapper? = null
var nextCredNumber: Int? = null
}
- init {
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
- val recipientDao = WalletDatabase.getDatabase(application).recipientDao()
- recipientRepository = RecipientRepository(recipientDao)
- }
-
override fun onCleared() {
super.onCleared()
globalParamsRequest?.dispose()
@@ -131,6 +115,7 @@ open class NewAccountViewModel(application: Application) :
globalParamsRequest?.dispose()
globalParamsRequest = identityProviderRepository.getIGlobalInfo(
{
+ App.appCore.session.walletStorage.setupPreferences.setHasCompletedOnboarding(true)
tempData.globalParams = it.value
_showAuthenticationLiveData.postValue(Event(true))
_waitingLiveData.postValue(false)
@@ -149,38 +134,19 @@ open class NewAccountViewModel(application: Application) :
private suspend fun decryptAndContinue(password: String) {
val globalParams = tempData.globalParams
- if (keyCreationVersion.useV1) {
- // Proceed to credentials creation as there is nothing to decrypt.
- if (globalParams == null) {
- _errorLiveData.value = Event(R.string.app_error_general)
- _waitingLiveData.value = false
- return
- }
- createCredentials(password, null, globalParams)
- } else {
- // Decrypt the private data.
- val privateIdObjectDataEncrypted = identity.privateIdObjectDataEncrypted
- if (globalParams == null) {
- _errorLiveData.postValue(Event(R.string.app_error_general))
- _waitingLiveData.postValue(false)
- return
- }
- val decryptedJson =
- App.appCore.getCurrentAuthenticationManager()
- .decryptInBackground(password, privateIdObjectDataEncrypted)
- if (decryptedJson != null) {
- val privateIdObjectData = gson.fromJson(decryptedJson, RawJson::class.java)
- createCredentials(password, privateIdObjectData, globalParams)
- } else {
- _errorLiveData.postValue(Event(R.string.app_error_encryption))
- _waitingLiveData.postValue(false)
- }
+
+ // Proceed to credentials creation as there is nothing to decrypt.
+ if (globalParams == null) {
+ _errorLiveData.value = Event(R.string.app_error_general)
+ _waitingLiveData.value = false
+ return
}
+
+ createCredentials(password, globalParams)
}
private suspend fun createCredentials(
password: String,
- privateIdObjectData: RawJson?,
globalParams: GlobalParams
) {
val identityProvider = identity.identityProvider
@@ -197,49 +163,24 @@ open class NewAccountViewModel(application: Application) :
tempData.nextCredNumber = accountRepository.nextCredNumber(identity.id)
- val output: CreateCredentialOutput? =
- if (keyCreationVersion.useV1) {
- val net = AppConfig.net
- val seed = AuthPreferences(getApplication()).getSeedHex(password)
-
- val credentialInput = CreateCredentialInputV1(
- ipInfo = idProviderInfo,
- arsInfos = arsInfos,
- global = globalParams,
- identityObject = identityObject,
- revealedAttributes = JsonArray(),
- seed = seed,
- net = net,
- identityIndex = identity.identityIndex,
- accountNumber = tempData.nextCredNumber ?: 0,
- expiry = (DateTimeUtil.nowPlusMinutes(5).time) / 1000,
- )
-
- App.appCore.cryptoLibrary.createCredentialV1(credentialInput)
- } else {
- requireNotNull(privateIdObjectData) {
- "Private ID data must be set when using V0 creation"
- }
-
- val generateAccountsInput =
- GenerateAccountsInput(globalParams, identityObject, privateIdObjectData)
- val nextAccountNumber = findNextAccountNumber(generateAccountsInput)
- ?: // Error has been handled
- return
-
- val credentialInput = CreateCredentialInput(
- ipInfo = idProviderInfo,
- arsInfos = arsInfos,
- global = globalParams,
- identityObject = identityObject,
- privateIdObjectData = privateIdObjectData,
- revealedAttributes = JsonArray(),
- accountNumber = nextAccountNumber,
- expiry = (DateTimeUtil.nowPlusMinutes(5).time) / 1000,
- )
+ val net = AppConfig.net
+ val seed = App.appCore.session.walletStorage.setupPreferences.getSeedHex(password)
+
+ val credentialInput = CreateCredentialInputV1(
+ ipInfo = idProviderInfo,
+ arsInfos = arsInfos,
+ global = globalParams,
+ identityObject = identityObject,
+ revealedAttributes = JsonArray(),
+ seed = seed,
+ net = net,
+ identityIndex = identity.identityIndex,
+ accountNumber = tempData.nextCredNumber ?: 0,
+ expiry = (DateTimeUtil.nowPlusMinutes(5).time) / 1000,
+ )
- App.appCore.cryptoLibrary.createCredential(credentialInput)
- }
+ val output: CreateCredentialOutput? =
+ App.appCore.cryptoLibrary.createCredentialV1(credentialInput)
if (output == null) {
_errorLiveData.postValue(Event(R.string.app_error_lib))
@@ -255,8 +196,11 @@ open class NewAccountViewModel(application: Application) :
)
)
- val storageAccountDataEncrypted = App.appCore.getCurrentAuthenticationManager()
- .encryptInBackground(password, jsonToBeEncrypted)
+ val storageAccountDataEncrypted = App.appCore.auth
+ .encrypt(
+ password = password,
+ data = jsonToBeEncrypted.toByteArray()
+ )
if (storageAccountDataEncrypted != null) {
tempData.encryptedAccountData = storageAccountDataEncrypted
tempData.credential = output.credential
@@ -268,77 +212,6 @@ open class NewAccountViewModel(application: Application) :
}
}
- private suspend fun findNextAccountNumber(generateAccountsInput: GenerateAccountsInput): Int? {
- var nextAccountNumber = identity.nextAccountNumber
- Log.d("nextAccountNumber: $nextAccountNumber")
- val maxAccounts = identity.identityObject!!.attributeList.maxAccounts
-
- val possibleAccountList = App.appCore.cryptoLibrary.generateAccounts(generateAccountsInput)
- if (possibleAccountList == null) {
- Log.e("Could not generate accounts, so do not allow account creation")
- _errorLiveData.postValue(Event(R.string.app_error_lib))
- _waitingLiveData.postValue(false)
- return null
- } else {
- Log.d("Generated account info for ${possibleAccountList.size} accounts")
- val next = checkExistingAccounts(possibleAccountList, nextAccountNumber)
- if (next == null) {
- _errorLiveData.postValue(Event(R.string.app_error_backend_unknown))
- _waitingLiveData.postValue(false)
- return null
- } else {
- nextAccountNumber = next
- }
- }
-
- Log.d("nextAccountNumber used: $nextAccountNumber")
- // Next account number starts from 0
- if (nextAccountNumber >= maxAccounts) {
- _errorLiveData.postValue(Event(R.string.new_account_identity_attributes_error_max_accounts_alt))
- _waitingLiveData.postValue(false)
- return null
- }
- identity.nextAccountNumber = nextAccountNumber + 1
- viewModelScope.launch(Dispatchers.IO) {
- identityRepository.update(identity)
- }
- return nextAccountNumber
- }
-
- /**
- * Returns the index for the first unused account
- */
- private suspend fun checkExistingAccounts(
- possibleAccountList: List,
- startIndex: Int
- ): Int? {
- var index = startIndex
- while (index < possibleAccountList.size) {
- try {
- Log.d("Get account balance for index $index")
- val accountBalance =
- proxyRepository.getAccountBalanceSuspended(possibleAccountList[index].accountAddress)
- Log.d("AccountBalance: $accountBalance")
- if (!accountBalance.accountExists()) {
- Log.d("Unused account address")
- return index
- }
- index++
- } catch (e: Exception) {
- val ex = BackendErrorHandler.getCoroutineBackendException(e)
- return if (ex != null && ex is BackendErrorException) {
- Log.d("Backend error - unused account address", ex)
- index
- } else {
- // Other exceptions like connection problems should not let the account be created
- Log.e("Unexpected exception when getting account balance", e)
- null
- }
- }
- }
- return null
- }
-
private fun submitCredential(credentialWrapper: CredentialWrapper) {
_waitingLiveData.postValue(true)
submitCredentialRequest?.dispose()
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt b/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt
index f058f71c..200b5465 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/common/accountupdater/AccountUpdater.kt
@@ -25,7 +25,6 @@ import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.EncryptedAmount
import com.concordium.wallet.data.room.Recipient
import com.concordium.wallet.data.room.Transfer
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.cis2.defaults.DefaultFungibleTokensManager
import com.concordium.wallet.ui.cis2.defaults.DefaultTokensManagerFactory
import com.concordium.wallet.ui.common.BackendErrorHandler
@@ -60,10 +59,14 @@ class AccountUpdater(val application: Application, private val viewModelScope: C
)
private val proxyRepository = ProxyRepository()
- private val accountRepository: AccountRepository
- private val encryptedAmountRepository: EncryptedAmountRepository
- private val transferRepository: TransferRepository
- private val recipientRepository: RecipientRepository
+ private val accountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
+ private val encryptedAmountRepository =
+ EncryptedAmountRepository(App.appCore.session.walletStorage.database.encryptedAmountDao())
+ private val transferRepository =
+ TransferRepository(App.appCore.session.walletStorage.database.transferDao())
+ private val recipientRepository =
+ RecipientRepository(App.appCore.session.walletStorage.database.recipientDao())
private val defaultFungibleTokensManager: DefaultFungibleTokensManager
private var accountSubmissionStatusRequestList: MutableList =
@@ -79,23 +82,12 @@ class AccountUpdater(val application: Application, private val viewModelScope: C
private var transferList: MutableList = ArrayList()
private var transfersToDeleteList: MutableList = ArrayList()
- private val updateNotificationsSubscriptionUseCase by lazy {
- UpdateNotificationsSubscriptionUseCase(application)
- }
+ private val updateNotificationsSubscriptionUseCase by lazy(::UpdateNotificationsSubscriptionUseCase)
init {
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
- val transferDao = WalletDatabase.getDatabase(application).transferDao()
- transferRepository = TransferRepository(transferDao)
- val encryptedAmountDao = WalletDatabase.getDatabase(application).encryptedAmountDao()
- encryptedAmountRepository = EncryptedAmountRepository(encryptedAmountDao)
- val recipientDao = WalletDatabase.getDatabase(application).recipientDao()
- recipientRepository = RecipientRepository(recipientDao)
-
- val contractTokenDao = WalletDatabase.getDatabase(application).contractTokenDao()
+
val defaultTokensManagerFactory = DefaultTokensManagerFactory(
- contractTokensRepository = ContractTokensRepository(contractTokenDao),
+ contractTokensRepository = ContractTokensRepository(App.appCore.session.walletStorage.database.contractTokenDao()),
)
defaultFungibleTokensManager = defaultTokensManagerFactory.getDefaultFungibleTokensManager()
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt
index 2c61b42d..fff88b7d 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/newaccountconfirmed/NewAccountConfirmedViewModel.kt
@@ -1,18 +1,20 @@
package com.concordium.wallet.ui.account.newaccountconfirmed
import android.app.Application
-import androidx.lifecycle.*
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.concordium.wallet.App
import com.concordium.wallet.data.AccountRepository
-import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.AccountWithIdentity
-import com.concordium.wallet.data.room.WalletDatabase
import kotlinx.coroutines.launch
class NewAccountConfirmedViewModel(application: Application) : AndroidViewModel(application) {
- private val identityRepository: IdentityRepository
- private val accountRepository: AccountRepository
+ private val accountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
private val _waitingLiveData = MutableLiveData()
val waitingLiveData: LiveData
@@ -22,13 +24,6 @@ class NewAccountConfirmedViewModel(application: Application) : AndroidViewModel(
lateinit var accountWithIdentityLiveData: LiveData
- init {
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
- }
-
fun initialize(account: Account) {
this.account = account
accountWithIdentityLiveData = accountRepository.getByIdWithIdentityAsLiveData(account.id)
@@ -40,4 +35,4 @@ class NewAccountConfirmedViewModel(application: Application) : AndroidViewModel(
_waitingLiveData.value = false
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt
index 1001c6d7..3f2f679e 100644
--- a/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/account/newaccountidentity/NewAccountIdentityViewModel.kt
@@ -4,19 +4,19 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import com.concordium.wallet.App
import com.concordium.wallet.R
import com.concordium.wallet.core.arch.Event
import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.room.Identity
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.util.Log
class NewAccountIdentityViewModel(application: Application) : AndroidViewModel(application) {
lateinit var accountName: String
- private val identityRepository: IdentityRepository
- val identityListLiveData: LiveData>
+ private val identityRepository = IdentityRepository(App.appCore.session.walletStorage.database.identityDao())
+ val identityListLiveData = identityRepository.allDoneIdentities
private val _errorLiveData = MutableLiveData>()
val errorLiveData: LiveData>
@@ -26,12 +26,6 @@ class NewAccountIdentityViewModel(application: Application) : AndroidViewModel(a
val errorDialogLiveData: LiveData>
get() = _errorDialogLiveData
- init {
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- identityListLiveData = identityRepository.allDoneIdentities
- }
-
fun initialize(accountName: String) {
this.accountName = accountName
}
@@ -47,4 +41,4 @@ class NewAccountIdentityViewModel(application: Application) : AndroidViewModel(a
}
return true
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt
index bb89bbc6..c8c1b4dd 100644
--- a/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/airdrop/AirDropViewModel.kt
@@ -5,12 +5,12 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
+import com.concordium.wallet.App
import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.backend.airdrop.AirDropRepository
import com.concordium.wallet.data.backend.airdrop.RegistrationRequest
import com.concordium.wallet.data.backend.airdrop.RegistrationResponse
import com.concordium.wallet.data.room.Account
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.extension.Event
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -36,12 +36,8 @@ class AirDropViewModel(application: Application) : AndroidViewModel(application)
private val _doRegistrationResponseLiveData: MutableLiveData> = MutableLiveData()
fun doRegistrationResponse(): LiveData> = _doRegistrationResponseLiveData
- private var accountRepository: AccountRepository? = null
-
- init {
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
- }
+ private val accountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
fun processAction(action: AirDropAction) {
when (action) {
@@ -79,4 +75,4 @@ class AirDropViewModel(application: Application) : AndroidViewModel(application)
_walletsLiveData.postValue(Event(wallets))
_viewState.emit(AirDropState.SelectWallet)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt
index d1fead62..fa224870 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/login/AuthLoginViewModel.kt
@@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.concordium.wallet.App
import com.concordium.wallet.R
+import com.concordium.wallet.core.Session
import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
import com.concordium.wallet.core.security.KeystoreEncryptionException
import kotlinx.coroutines.launch
import javax.crypto.Cipher
@@ -37,12 +37,13 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio
}
fun shouldShowBiometrics(): Boolean {
- return App.appCore.getCurrentAuthenticationManager().useBiometrics()
+ return App.appCore.auth.isBiometricsUsed()
}
fun getCipherForBiometrics(): Cipher? {
try {
- val cipher = App.appCore.getCurrentAuthenticationManager().initBiometricsCipherForDecryption()
+ val cipher =
+ App.appCore.auth.getBiometricsCipherForDecryption()
if (cipher == null) {
_errorLiveData.value = Event(R.string.app_error_keystore_key_invalidated)
}
@@ -53,36 +54,33 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- fun checkLogin(password: String) = viewModelScope.launch {
+ fun checkLogin(password: String?) = viewModelScope.launch {
_waitingLiveData.value = true
- val res = App.appCore.getCurrentAuthenticationManager().checkPasswordInBackground(password)
- if (res) {
+ if (password != null && App.appCore.auth.checkPassword(password)) {
loginSuccess()
} else {
_passwordErrorLiveData.value =
- Event(if (App.appCore.getCurrentAuthenticationManager().usePasscode()) R.string.auth_login_passcode_error else R.string.auth_login_password_error)
+ Event(
+ if (App.appCore.auth
+ .isPasscodeUsed()
+ ) R.string.auth_login_passcode_error else R.string.auth_login_password_error
+ )
_waitingLiveData.value = false
}
}
fun checkLogin(cipher: Cipher) = viewModelScope.launch {
- _waitingLiveData.value = true
- val password = App.appCore.getCurrentAuthenticationManager().checkPasswordInBackground(cipher)
- if (password != null) {
- loginSuccess()
- } else {
- _passwordErrorLiveData.value =
- Event(if (App.appCore.getCurrentAuthenticationManager().usePasscode()) R.string.auth_login_passcode_error else R.string.auth_login_password_error)
- _waitingLiveData.value = false
- }
+ checkLogin(
+ password = App.appCore.auth.decryptPasswordWithBiometricsCipher(cipher),
+ )
}
private fun loginSuccess() {
- session.hasLoggedInUser()
+ session.setUserLoggedIn()
_finishScreenLiveData.value = Event(true)
}
fun usePasscode(): Boolean {
- return App.appCore.getCurrentAuthenticationManager().usePasscode()
+ return App.appCore.auth.isPasscodeUsed()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupActivity.kt
deleted file mode 100644
index b547ea9d..00000000
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupActivity.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package com.concordium.wallet.ui.auth.setup
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.view.isVisible
-import androidx.lifecycle.ViewModelProvider
-import com.concordium.wallet.R
-import com.concordium.wallet.core.arch.EventObserver
-import com.concordium.wallet.databinding.ActivityAuthSetupBinding
-import com.concordium.wallet.ui.auth.setupbiometrics.AuthSetupBiometricsActivity
-import com.concordium.wallet.ui.auth.setuppassword.AuthSetupPasswordActivity
-import com.concordium.wallet.ui.auth.setuprepeat.AuthSetupRepeatActivity
-import com.concordium.wallet.ui.base.BaseActivity
-import com.concordium.wallet.uicore.view.PasscodeView
-import com.concordium.wallet.util.KeyboardUtil
-
-class AuthSetupActivity : BaseActivity(
- R.layout.activity_auth_setup,
- R.string.auth_setup_title
-) {
-
- private var continueFlow: Boolean = true
-
- companion object {
- const val CONTINUE_INITIAL_SETUP = "CONTINUE_INITIAL_SETUP"
- }
-
- private lateinit var binding: ActivityAuthSetupBinding
- private lateinit var viewModel: AuthSetupViewModel
-
- //region Lifecycle
- //************************************************************
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityAuthSetupBinding.bind(findViewById(R.id.root_layout))
-
- continueFlow = intent.getBooleanExtra(CONTINUE_INITIAL_SETUP, true)
-
- initializeViewModel()
- viewModel.initialize()
- initializeViews()
-
- hideActionBarBack(isVisible = false)
- }
-
- override fun onBackPressed() {
- // Ignore back press
- }
-
- private fun finishSuccess() {
- setResult(Activity.RESULT_OK)
- finish()
- }
-
- //endregion
-
- //region Initialize
- //************************************************************
-
- private fun initializeViewModel() {
- viewModel = ViewModelProvider(
- this,
- ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[AuthSetupViewModel::class.java]
- viewModel.errorLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Boolean) {
- if (value) {
- showPasswordError()
- }
- }
- })
- viewModel.finishScreenLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Boolean) {
- if (value) {
- finishSuccess()
- }
- }
- })
- viewModel.gotoBiometricsSetupLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Boolean) {
- if (value) {
- gotoAuthSetupBiometrics()
- }
- }
- })
- }
-
- private fun initializeViews() {
- binding.passcodeView.passcodeListener = object : PasscodeView.PasscodeListener {
- override fun onInputChanged() {
- binding.errorTextview.text = ""
- binding.errorTextview.isVisible = false
- }
-
- override fun onDone() {
- onConfirmClicked()
- }
- }
- binding.fullPasswordButton.setOnClickListener {
- gotoAuthSetupPassword()
- }
- binding.passcodeView.requestFocus()
- }
-
- //endregion
-
- //region Control/UI
- //************************************************************
-
- private fun onConfirmClicked() {
- if (viewModel.checkPasswordRequirements(binding.passcodeView.getPasscode())) {
- viewModel.startSetupPassword(binding.passcodeView.getPasscode())
- gotoAuthSetupPasscodeRepeat()
- } else {
- binding.passcodeView.clearPasscode()
- binding.errorTextview.setText(R.string.auth_error_passcode_not_valid)
- binding.errorTextview.isVisible = true
- }
- }
-
- private val getResultAuthSetupBiometrics =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == Activity.RESULT_OK) {
- if (continueFlow) {
- viewModel.hasFinishedSetupPassword()
- }
- finishSuccess()
- }
- }
-
- private fun gotoAuthSetupBiometrics() {
- val intent = Intent(this, AuthSetupBiometricsActivity::class.java)
- getResultAuthSetupBiometrics.launch(intent)
- }
-
- private val getResultAuthSetupPasscodeRepeat =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- val result = it.data
- if (it.resultCode == Activity.RESULT_OK && result != null) {
- if (AuthSetupRepeatActivity.useFullPassword(result)) {
- gotoAuthSetupPassword()
- } else if (AuthSetupRepeatActivity.doesMatch(result)) {
- viewModel.setupPassword(binding.passcodeView.getPasscode(), continueFlow)
- } else {
- binding.passcodeView.clearPasscode()
- binding.errorTextview.setText(R.string.auth_error_passcodes_different)
- binding.errorTextview.isVisible = true
- }
- }
- }
-
- private fun gotoAuthSetupPasscodeRepeat() {
- val intent = Intent(this, AuthSetupRepeatActivity::class.java)
- getResultAuthSetupPasscodeRepeat.launch(intent)
- }
-
- private val getResultAuthSetupFullPassword =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (it.resultCode == Activity.RESULT_OK) {
- if (continueFlow) {
- viewModel.hasFinishedSetupPassword()
- }
- finishSuccess()
- }
- binding.passcodeView.clearPasscode()
- }
-
- private fun gotoAuthSetupPassword() {
- val intent = Intent(this, AuthSetupPasswordActivity::class.java)
- getResultAuthSetupFullPassword.launch(intent)
- }
-
- private fun showPasswordError() {
- binding.passcodeView.clearPasscode()
- KeyboardUtil.hideKeyboard(this)
- popup.showSnackbar(binding.rootLayout, R.string.auth_error_password_setup)
- }
-
- override fun loggedOut() {
- }
-
- //endregion
-}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupBiometricsDialog.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsDialog.kt
similarity index 86%
rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupBiometricsDialog.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsDialog.kt
index 7b222118..c61a9955 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupBiometricsDialog.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsDialog.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.passcode
+package com.concordium.wallet.ui.auth.setup
import android.content.DialogInterface
import android.os.Bundle
@@ -12,15 +12,14 @@ import androidx.lifecycle.ViewModelProvider
import com.concordium.wallet.App
import com.concordium.wallet.R
import com.concordium.wallet.core.security.BiometricPromptCallback
-import com.concordium.wallet.databinding.DialogPasscodeSetupBiometricsBinding
-import com.concordium.wallet.ui.auth.setupbiometrics.AuthSetupBiometricsViewModel
+import com.concordium.wallet.databinding.DialogAuthSetupBiometricsBinding
import javax.crypto.Cipher
-class PasscodeSetupBiometricsDialog : AppCompatDialogFragment() {
+class AuthSetupBiometricsDialog : AppCompatDialogFragment() {
override fun getTheme(): Int =
R.style.CCX_Dialog
- private lateinit var binding: DialogPasscodeSetupBiometricsBinding
+ private lateinit var binding: DialogAuthSetupBiometricsBinding
private lateinit var viewModel: AuthSetupBiometricsViewModel
private lateinit var biometricPrompt: BiometricPrompt
@@ -29,7 +28,7 @@ class PasscodeSetupBiometricsDialog : AppCompatDialogFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- binding = DialogPasscodeSetupBiometricsBinding.inflate(inflater, container, false)
+ binding = DialogAuthSetupBiometricsBinding.inflate(inflater, container, false)
return binding.root
}
@@ -81,7 +80,7 @@ class PasscodeSetupBiometricsDialog : AppCompatDialogFragment() {
val callback = object : BiometricPromptCallback() {
override fun onAuthenticationSucceeded(cipher: Cipher) {
- viewModel.setupBiometricWithPassword(cipher)
+ viewModel.proceedWithSetup(cipher)
}
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsViewModel.kt
similarity index 58%
rename from app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsViewModel.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsViewModel.kt
index 96220478..4701a3e3 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupBiometricsViewModel.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.setupbiometrics
+package com.concordium.wallet.ui.auth.setup
import android.app.Application
import androidx.lifecycle.AndroidViewModel
@@ -7,15 +7,11 @@ import androidx.lifecycle.MutableLiveData
import com.concordium.wallet.App
import com.concordium.wallet.R
import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
import com.concordium.wallet.core.security.KeystoreEncryptionException
-import com.concordium.wallet.util.Log
import javax.crypto.Cipher
class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel(application) {
- private val session: Session = App.appCore.session
-
private val _errorLiveData = MutableLiveData>()
val errorLiveData: LiveData>
get() = _errorLiveData
@@ -24,8 +20,7 @@ class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel(
get() = _finishScreenLiveData
fun initialize() {
-
- val generated = App.appCore.getCurrentAuthenticationManager().generateBiometricsSecretKey()
+ val generated = App.appCore.auth.generateBiometricsSecretKey()
if (!generated) {
_errorLiveData.value = Event(R.string.app_error_keystore)
}
@@ -33,7 +28,7 @@ class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel(
fun getCipherForBiometrics(): Cipher? {
try {
- val cipher = App.appCore.getCurrentAuthenticationManager().initBiometricsCipherForEncryption()
+ val cipher = App.appCore.auth.getBiometricsCipherForEncryption()
if (cipher == null) {
_errorLiveData.value = Event(R.string.app_error_keystore_key_invalidated)
}
@@ -44,17 +39,15 @@ class AuthSetupBiometricsViewModel(application: Application) : AndroidViewModel(
}
}
- fun setupBiometricWithPassword(cipher: Cipher) {
- val password = session.tempPassword
- if (password != null) {
- val setupDone = App.appCore.getCurrentAuthenticationManager().setupBiometrics(password, cipher)
- if (setupDone) {
- _finishScreenLiveData.value = Event(true)
- } else {
- _errorLiveData.value = Event(R.string.app_error_keystore)
- }
+ fun proceedWithSetup(cipher: Cipher) {
+ val password = App.appCore.setup.authSetupPassword
+ ?: error("Setting up biometrics when there is no password to set up")
+
+ val setupDone = App.appCore.auth.initBiometricAuth(password, cipher)
+ if (setupDone) {
+ _finishScreenLiveData.value = Event(true)
} else {
- Log.e("Temp password has been removed before biometrics was setup")
+ _errorLiveData.value = Event(R.string.app_error_keystore)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeActivity.kt
similarity index 70%
rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupActivity.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeActivity.kt
index a2d43aaf..a41fb341 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeActivity.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.passcode
+package com.concordium.wallet.ui.auth.setup
import android.app.Activity
import android.content.DialogInterface
@@ -10,24 +10,24 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isInvisible
import androidx.lifecycle.ViewModelProvider
import com.concordium.wallet.R
-import com.concordium.wallet.databinding.ActivityPasscodeSetupBinding
+import com.concordium.wallet.databinding.ActivityAuthSetupPasscodeBinding
import com.concordium.wallet.extension.collect
import com.concordium.wallet.extension.collectWhenStarted
-import com.concordium.wallet.ui.auth.setuppassword.AuthSetupPasswordActivity
+import com.concordium.wallet.ui.auth.setup.password.AuthSetupPasswordActivity
import com.concordium.wallet.ui.base.BaseActivity
-class PasscodeSetupActivity :
- BaseActivity(R.layout.activity_passcode_setup),
+class AuthSetupPasscodeActivity :
+ BaseActivity(R.layout.activity_auth_setup_passcode),
OnDismissListener {
- private val binding: ActivityPasscodeSetupBinding by lazy {
- ActivityPasscodeSetupBinding.bind(findViewById(R.id.toastLayoutTopError))
+ private val binding: ActivityAuthSetupPasscodeBinding by lazy {
+ ActivityAuthSetupPasscodeBinding.bind(findViewById(R.id.toastLayoutTopError))
}
- private val viewModel: PasscodeSetupViewModel by lazy {
+ private val viewModel: AuthSetupPasscodeViewModel by lazy {
ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[PasscodeSetupViewModel::class.java]
+ )[AuthSetupPasscodeViewModel::class.java]
}
private val getResultAuthSetupFullPassword =
@@ -50,7 +50,7 @@ class PasscodeSetupActivity :
length = viewModel.passcodeLength
biometricsButton.isInvisible = true
- mutableInput.observe(this@PasscodeSetupActivity) { inputValue ->
+ mutableInput.observe(this@AuthSetupPasscodeActivity) { inputValue ->
if (inputValue.length == viewModel.passcodeLength) {
viewModel.onPasscodeEntered(inputValue)
}
@@ -68,31 +68,31 @@ class PasscodeSetupActivity :
private fun subscribeToState(
) = viewModel.stateFlow.collectWhenStarted(this) { state ->
binding.titleTextView.text = when (state) {
- is PasscodeSetupViewModel.State.Create -> getString(R.string.passcode_create_title)
- PasscodeSetupViewModel.State.Repeat -> getString(R.string.passcode_repeat_title)
+ is AuthSetupPasscodeViewModel.State.Create -> getString(R.string.passcode_create_title)
+ AuthSetupPasscodeViewModel.State.Repeat -> getString(R.string.passcode_repeat_title)
}
binding.detailsTextView.text = when (state) {
- is PasscodeSetupViewModel.State.Create -> getString(
+ is AuthSetupPasscodeViewModel.State.Create -> getString(
R.string.template_passcode_create_details,
viewModel.passcodeLength
)
- PasscodeSetupViewModel.State.Repeat -> getString(
+ AuthSetupPasscodeViewModel.State.Repeat -> getString(
R.string.template_passcode_repeat_details,
viewModel.passcodeLength
)
}
when (state) {
- is PasscodeSetupViewModel.State.Create -> {
+ is AuthSetupPasscodeViewModel.State.Create -> {
binding.passcodeInputView.reset()
if (state.hasError) {
binding.passcodeInputView.animateError()
}
}
- PasscodeSetupViewModel.State.Repeat -> {
+ AuthSetupPasscodeViewModel.State.Repeat -> {
binding.passcodeInputView.reset()
}
}
@@ -101,23 +101,23 @@ class PasscodeSetupActivity :
private fun subscribeToEvents(
) = viewModel.eventsFlow.collect(this) { event ->
when (event) {
- PasscodeSetupViewModel.Event.FinishWithSuccess -> {
+ AuthSetupPasscodeViewModel.Event.FinishWithSuccess -> {
setResult(Activity.RESULT_OK)
finish()
}
- PasscodeSetupViewModel.Event.ShowFatalError -> {
+ AuthSetupPasscodeViewModel.Event.ShowFatalError -> {
showError(R.string.passcode_setup_failed)
}
- PasscodeSetupViewModel.Event.SuggestBiometricsSetup -> {
- PasscodeSetupBiometricsDialog().show(
+ AuthSetupPasscodeViewModel.Event.SuggestBiometricsSetup -> {
+ AuthSetupBiometricsDialog().show(
supportFragmentManager,
- PasscodeSetupBiometricsDialog.TAG
+ AuthSetupBiometricsDialog.TAG
)
}
- PasscodeSetupViewModel.Event.OpenFullPasswordSetUp ->
+ AuthSetupPasscodeViewModel.Event.OpenFullPasswordSetUp ->
goToAuthSetupPassword()
}
}
@@ -129,7 +129,6 @@ class PasscodeSetupActivity :
private fun goToAuthSetupPassword() {
val intent = Intent(this, AuthSetupPasswordActivity::class.java)
- intent.putExtra(AuthSetupPasswordActivity.SKIP_BIOMETRICS, true)
getResultAuthSetupFullPassword.launch(intent)
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeViewModel.kt
similarity index 67%
rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupViewModel.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeViewModel.kt
index a860e6e5..47c10117 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/PasscodeSetupViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupPasscodeViewModel.kt
@@ -1,17 +1,16 @@
-package com.concordium.wallet.ui.auth.passcode
+package com.concordium.wallet.ui.auth.setup
import android.app.Application
import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
import com.concordium.wallet.App
-import com.concordium.wallet.core.authentication.Session
import com.concordium.wallet.util.BiometricsUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
-class PasscodeSetupViewModel(application: Application) : AndroidViewModel(application) {
- private val session: Session = App.appCore.session
-
+class AuthSetupPasscodeViewModel(application: Application) : AndroidViewModel(application) {
private val mutableStateFlow = MutableStateFlow(
State.Create(hasError = false)
)
@@ -29,7 +28,7 @@ class PasscodeSetupViewModel(application: Application) : AndroidViewModel(applic
App.appCore.tracker.welcomePasscodeScreen()
}
- fun onPasscodeEntered(passcode: String) {
+ fun onPasscodeEntered(passcode: String) = viewModelScope.launch {
require(passcode.length == passcodeLength) {
"The entered passcode doesn't have the required length"
}
@@ -54,39 +53,48 @@ class PasscodeSetupViewModel(application: Application) : AndroidViewModel(applic
}
}
- private fun proceedWithConfirmedCreatedPasscode() {
+ private suspend fun proceedWithConfirmedCreatedPasscode() {
val confirmedPasscode = checkNotNull(createdPasscode) {
"The passcode must be created at this point"
}
- session.startPasswordSetup(confirmedPasscode)
-
- val isSetUpSuccessfully = App.appCore
- .getCurrentAuthenticationManager()
- .createPasswordCheck(confirmedPasscode)
+ App.appCore.setup.beginAuthSetup(confirmedPasscode)
+
+ val isSetUpSuccessfully = runCatching {
+ val authResetMasterKey = App.appCore.setup.authResetMasterKey
+ if (authResetMasterKey != null) {
+ App.appCore.auth.initPasswordAuth(
+ password = confirmedPasscode,
+ isPasscode = true,
+ masterKey = authResetMasterKey,
+ )
+ } else {
+ App.appCore.auth.initPasswordAuth(
+ password = confirmedPasscode,
+ isPasscode = true,
+ )
+ }
+ }.isSuccess
if (isSetUpSuccessfully) {
- onSetUpSuccessfully(usedPasscode = true)
+ onSetUpSuccessfully()
} else {
- session.hasFinishedSetupPassword()
+ App.appCore.setup.finishAuthSetup()
mutableEventsFlow.tryEmit(Event.ShowFatalError)
}
}
- private fun onSetUpSuccessfully(usedPasscode: Boolean) {
- // Setting up password is done, so login screen should be shown next time app is opened
- session.hasSetupPassword(usedPasscode)
-
+ private fun onSetUpSuccessfully() {
if (BiometricsUtil.isBiometricsAvailable()) {
mutableEventsFlow.tryEmit(Event.SuggestBiometricsSetup)
} else {
- session.hasFinishedSetupPassword()
+ App.appCore.setup.finishAuthSetup()
mutableEventsFlow.tryEmit(Event.FinishWithSuccess)
}
}
fun onBiometricsSuggestionReviewed() {
- session.hasFinishedSetupPassword()
+ App.appCore.setup.finishAuthSetup()
mutableEventsFlow.tryEmit(Event.FinishWithSuccess)
}
@@ -100,7 +108,10 @@ class PasscodeSetupViewModel(application: Application) : AndroidViewModel(applic
)
if (isSetUpSuccessfully) {
- onSetUpSuccessfully(usedPasscode = false)
+ onSetUpSuccessfully()
+ } else {
+ App.appCore.setup.finishAuthSetup()
+ mutableEventsFlow.tryEmit(Event.ShowFatalError)
}
}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupViewModel.kt
deleted file mode 100644
index 9119f54c..00000000
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setup/AuthSetupViewModel.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.concordium.wallet.ui.auth.setup
-
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import com.concordium.wallet.App
-import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
-import com.concordium.wallet.util.BiometricsUtil
-
-class AuthSetupViewModel(application: Application) : AndroidViewModel(application) {
-
- private val session: Session = App.appCore.session
-
- private val _errorLiveData = MutableLiveData>()
- val errorLiveData: LiveData>
- get() = _errorLiveData
- private val _finishScreenLiveData = MutableLiveData>()
- val finishScreenLiveData: LiveData>
- get() = _finishScreenLiveData
- private val _gotoBiometricsSetupLiveData = MutableLiveData>()
- val gotoBiometricsSetupLiveData: LiveData>
- get() = _gotoBiometricsSetupLiveData
-
- fun initialize() {
- }
-
- fun hasFinishedSetupPassword() {
- session.hasFinishedSetupPassword()
- }
-
- fun startSetupPassword(password: String) {
- // Keep password for re-enter check and biometrics activation
- session.startPasswordSetup(password)
- }
-
- fun checkPasswordRequirements(password: String): Boolean {
- return (password.length == 6)
- }
-
- fun setupPassword(password: String, continueFlow: Boolean) {
- val res = App.appCore.getCurrentAuthenticationManager().createPasswordCheck(password)
- if (res) {
- // Setting up password is done, so login screen should be shown next time app is opened
- session.hasSetupPassword(true)
- if (BiometricsUtil.isBiometricsAvailable()) {
- _gotoBiometricsSetupLiveData.value = Event(true)
- } else {
- session.hasFinishedSetupPassword()
- if (continueFlow) { // if we continue flow (setting up account and identity) we are at the end of the line and we clear stored password
- session.hasFinishedSetupPassword()
- }
- _finishScreenLiveData.value = Event(true)
- }
- } else {
- _errorLiveData.value = Event(true)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/CcxPasscodeInputView.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/CcxPasscodeInputView.kt
similarity index 98%
rename from app/src/main/java/com/concordium/wallet/ui/auth/passcode/CcxPasscodeInputView.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/CcxPasscodeInputView.kt
index bc97afc8..9307a2a0 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/passcode/CcxPasscodeInputView.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/CcxPasscodeInputView.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.passcode
+package com.concordium.wallet.ui.auth.setup
import android.content.Context
import android.util.AttributeSet
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordActivity.kt
similarity index 86%
rename from app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordActivity.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordActivity.kt
index 04682e54..d54edf9f 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordActivity.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.setuppassword
+package com.concordium.wallet.ui.auth.setup.password
import android.app.Activity
import android.content.Intent
@@ -9,12 +9,14 @@ import androidx.lifecycle.ViewModelProvider
import com.concordium.wallet.R
import com.concordium.wallet.core.arch.EventObserver
import com.concordium.wallet.databinding.ActivityAuthSetupPasswordBinding
-import com.concordium.wallet.ui.auth.setupbiometrics.AuthSetupBiometricsActivity
-import com.concordium.wallet.ui.auth.setuppasswordrepeat.AuthSetupPasswordRepeatActivity
import com.concordium.wallet.ui.base.BaseActivity
import com.concordium.wallet.uicore.afterTextChanged
import com.concordium.wallet.util.KeyboardUtil
+/**
+ * This screen should only be called from the passcode setup.
+ * It does not suggest biometrics and does not finalize session password setup.
+ */
class AuthSetupPasswordActivity : BaseActivity(
R.layout.activity_auth_setup_password,
R.string.auth_setup_password_title
@@ -24,14 +26,9 @@ class AuthSetupPasswordActivity : BaseActivity(
ActivityAuthSetupPasswordBinding.bind(findViewById(R.id.root_layout))
}
- private val skipBiometrics: Boolean by lazy {
- intent.getBooleanExtra(SKIP_BIOMETRICS, false)
- }
-
companion object {
private const val REQUEST_CODE_AUTH_SETUP_BIOMETRICS = 2000
private const val REQUEST_CODE_AUTH_SETUP_PASSWORD_REPEAT = 2001
- const val SKIP_BIOMETRICS = "skip_biometrics"
}
//region Lifecycle
@@ -41,7 +38,7 @@ class AuthSetupPasswordActivity : BaseActivity(
super.onCreate(savedInstanceState)
initializeViewModel()
- viewModel.initialize(skipBiometrics)
+ viewModel.initialize()
initializeViews()
hideActionBarBack(isVisible = true)
@@ -99,13 +96,6 @@ class AuthSetupPasswordActivity : BaseActivity(
}
}
})
- viewModel.gotoBiometricsSetupLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Boolean) {
- if (value) {
- gotoAuthSetupBiometrics()
- }
- }
- })
}
private fun initializeViews() {
@@ -157,11 +147,6 @@ class AuthSetupPasswordActivity : BaseActivity(
}
}
- private fun gotoAuthSetupBiometrics() {
- val intent = Intent(this, AuthSetupBiometricsActivity::class.java)
- startActivityForResult(intent, REQUEST_CODE_AUTH_SETUP_BIOMETRICS)
- }
-
private fun gotoAuthSetupPasswordRepeat() {
val intent = Intent(this, AuthSetupPasswordRepeatActivity::class.java)
startActivityForResult(intent, REQUEST_CODE_AUTH_SETUP_PASSWORD_REPEAT)
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatActivity.kt
similarity index 98%
rename from app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatActivity.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatActivity.kt
index 4d3a7f61..c50fc642 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatActivity.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.setuppasswordrepeat
+package com.concordium.wallet.ui.auth.setup.password
import android.app.Activity
import android.content.Intent
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatViewModel.kt
similarity index 69%
rename from app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatViewModel.kt
rename to app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatViewModel.kt
index 3dd69a59..c807bd08 100644
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppasswordrepeat/AuthSetupPasswordRepeatViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordRepeatViewModel.kt
@@ -1,4 +1,4 @@
-package com.concordium.wallet.ui.auth.setuppasswordrepeat
+package com.concordium.wallet.ui.auth.setup.password
import android.app.Application
import androidx.lifecycle.AndroidViewModel
@@ -6,13 +6,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.concordium.wallet.App
import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
class AuthSetupPasswordRepeatViewModel(application: Application) : AndroidViewModel(application) {
- // App.appCore.getCurrentAuthenticationManager()
- private val session: Session = App.appCore.session
-
private val _finishScreenLiveData = MutableLiveData>()
val finishScreenLiveData: LiveData>
get() = _finishScreenLiveData
@@ -21,7 +17,7 @@ class AuthSetupPasswordRepeatViewModel(application: Application) : AndroidViewMo
}
fun checkPassword(password: String) {
- val isEqual = session.checkPassword(password)
+ val isEqual = App.appCore.setup.authSetupPassword == password
_finishScreenLiveData.value = Event(isEqual)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordViewModel.kt
new file mode 100644
index 00000000..060a25fb
--- /dev/null
+++ b/app/src/main/java/com/concordium/wallet/ui/auth/setup/password/AuthSetupPasswordViewModel.kt
@@ -0,0 +1,56 @@
+package com.concordium.wallet.ui.auth.setup.password
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.concordium.wallet.App
+import com.concordium.wallet.core.arch.Event
+import kotlinx.coroutines.launch
+
+class AuthSetupPasswordViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val _errorLiveData = MutableLiveData>()
+ val errorLiveData: LiveData>
+ get() = _errorLiveData
+ private val _finishScreenLiveData = MutableLiveData>()
+ val finishScreenLiveData: LiveData>
+ get() = _finishScreenLiveData
+
+ fun initialize() {
+ }
+
+ fun startSetupPassword(password: String) {
+ // Keep password for re-enter check and biometrics activation (outside this screen).
+ App.appCore.setup.beginAuthSetup(password)
+ }
+
+ fun checkPasswordRequirements(password: String): Boolean {
+ return (password.length >= 6)
+ }
+
+ fun setupPassword(password: String) = viewModelScope.launch {
+ val isSetUpSuccessfully = runCatching {
+ val authResetMasterKey = App.appCore.setup.authResetMasterKey
+ if (authResetMasterKey != null) {
+ App.appCore.auth.initPasswordAuth(
+ password = password,
+ isPasscode = false,
+ masterKey = authResetMasterKey,
+ )
+ } else {
+ App.appCore.auth.initPasswordAuth(
+ password = password,
+ isPasscode = false,
+ )
+ }
+ }.isSuccess
+
+ if (isSetUpSuccessfully) {
+ _finishScreenLiveData.value = Event(true)
+ } else {
+ _errorLiveData.value = Event(true)
+ }
+ }
+}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsActivity.kt
deleted file mode 100644
index fb1e1585..00000000
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setupbiometrics/AuthSetupBiometricsActivity.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package com.concordium.wallet.ui.auth.setupbiometrics
-
-import android.app.Activity
-import android.os.Bundle
-import androidx.biometric.BiometricPrompt
-import androidx.core.content.ContextCompat
-import androidx.lifecycle.ViewModelProvider
-import com.concordium.wallet.R
-import com.concordium.wallet.core.arch.EventObserver
-import com.concordium.wallet.core.security.BiometricPromptCallback
-import com.concordium.wallet.databinding.ActivityAuthSetupBiometricsBinding
-import com.concordium.wallet.ui.base.BaseActivity
-import com.concordium.wallet.ui.common.delegates.AuthDelegate
-import com.concordium.wallet.ui.common.delegates.AuthDelegateImpl
-import javax.crypto.Cipher
-
-class AuthSetupBiometricsActivity : BaseActivity(
- R.layout.activity_auth_setup_biometrics,
- R.string.auth_setup_biometrics_title
-), AuthDelegate by AuthDelegateImpl() {
- private lateinit var viewModel: AuthSetupBiometricsViewModel
- private val binding by lazy {
- ActivityAuthSetupBiometricsBinding.bind(findViewById(R.id.root_layout))
- }
- private lateinit var biometricPrompt: BiometricPrompt
-
- //region Lifecycle
- // ************************************************************
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- initializeViewModel()
- viewModel.initialize()
- initializeViews()
-
- hideActionBarBack(isVisible = false)
-
- biometricPrompt = createBiometricPrompt()
- }
-
- override fun onBackPressed() {
- // Ignore back press
- }
-
- //endregion
-
- //region Initialize
- // ************************************************************
-
- private fun initializeViewModel() {
- viewModel = ViewModelProvider(
- this,
- ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[AuthSetupBiometricsViewModel::class.java]
-
- viewModel.errorLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Int) {
- showError(value)
- }
- })
- viewModel.finishScreenLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Boolean) {
- if (value) {
- setResult(Activity.RESULT_OK)
- finish()
- }
- }
- })
- }
-
- private fun initializeViews() {
- hideActionBarBack(false)
- binding.enableBiometricsButton.setOnClickListener {
- onEnableBiometricsClicked()
- }
- binding.cancelButton.setOnClickListener {
- onCancelClicked()
- }
- }
-
- //endregion
-
- //region Control/UI
- // ************************************************************
-
- private fun onEnableBiometricsClicked() {
- val promptInfo = createPromptInfo()
-
- val cipher = viewModel.getCipherForBiometrics()
- if (cipher != null) {
- biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
- }
- }
-
- private fun onCancelClicked() {
- setResult(Activity.RESULT_OK)
- finish()
- }
-
- //endregion
-
- //region Biometrics
- // ************************************************************
-
- private fun createBiometricPrompt(): BiometricPrompt {
- val executor = ContextCompat.getMainExecutor(this)
-
- val callback = object : BiometricPromptCallback() {
- override fun onAuthenticationSucceeded(cipher: Cipher) {
- viewModel.setupBiometricWithPassword(cipher)
- }
- }
-
- return BiometricPrompt(this, executor, callback)
- }
-
- private fun createPromptInfo(): BiometricPrompt.PromptInfo {
- return BiometricPrompt.PromptInfo.Builder()
- .setTitle(getString(R.string.auth_setup_biometrics_dialog_title))
- .setSubtitle(getString(R.string.auth_setup_biometrics_dialog_subtitle))
- .setConfirmationRequired(false)
- .setNegativeButtonText(getString(R.string.auth_setup_biometrics_dialog_cancel))
- .build()
- }
-
- //endregion
-
-
- override fun loggedOut() {
- // No need to show auth, as it is anyway requested further.
- }
-}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordViewModel.kt
deleted file mode 100644
index 6ccaa21c..00000000
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setuppassword/AuthSetupPasswordViewModel.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.concordium.wallet.ui.auth.setuppassword
-
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import com.concordium.wallet.App
-import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
-import com.concordium.wallet.util.BiometricsUtil
-
-class AuthSetupPasswordViewModel(application: Application) : AndroidViewModel(application) {
-
- private val session: Session = App.appCore.session
-
- private val _errorLiveData = MutableLiveData>()
- val errorLiveData: LiveData>
- get() = _errorLiveData
- private val _finishScreenLiveData = MutableLiveData>()
- val finishScreenLiveData: LiveData>
- get() = _finishScreenLiveData
- private val _gotoBiometricsSetupLiveData = MutableLiveData>()
- val gotoBiometricsSetupLiveData: LiveData>
- get() = _gotoBiometricsSetupLiveData
- private var skipBiometrics = true
-
- fun initialize(skipBiometrics: Boolean) {
- this.skipBiometrics = skipBiometrics
- }
-
- fun startSetupPassword(password: String) {
- // Keep password for re-enter check and biometrics activation
- session.startPasswordSetup(password)
- }
-
- fun checkPasswordRequirements(password: String): Boolean {
- return (password.length >= 6)
- }
-
- fun setupPassword(password: String) {
- val res = App.appCore.getCurrentAuthenticationManager().createPasswordCheck(password)
- if (res) {
- // Setting up password is done, so login screen should be shown next time app is opened
- session.hasSetupPassword()
- if (BiometricsUtil.isBiometricsAvailable()) {
- if (skipBiometrics) {
- _finishScreenLiveData.value = Event(true)
- } else {
- _gotoBiometricsSetupLiveData.value = Event(true)
- }
- } else {
- session.hasFinishedSetupPassword()
- _finishScreenLiveData.value = Event(true)
- }
- } else {
- _errorLiveData.value = Event(true)
- }
- }
-}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatActivity.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatActivity.kt
deleted file mode 100644
index 5a8b96ca..00000000
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatActivity.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.concordium.wallet.ui.auth.setuprepeat
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import androidx.lifecycle.ViewModelProvider
-import com.concordium.wallet.R
-import com.concordium.wallet.core.arch.EventObserver
-import com.concordium.wallet.databinding.ActivityAuthSetupBinding
-import com.concordium.wallet.ui.base.BaseActivity
-import com.concordium.wallet.uicore.view.PasscodeView
-
-class AuthSetupRepeatActivity : BaseActivity(
- R.layout.activity_auth_setup,
- R.string.auth_setup_repeat_title
-) {
- private lateinit var viewModel: AuthSetupRepeatViewModel
- private val binding by lazy {
- ActivityAuthSetupBinding.bind(findViewById(R.id.root_layout))
- }
-
- //region Lifecycle
- // ************************************************************
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- initializeViewModel()
- viewModel.initialize()
- initializeViews()
-
- hideActionBarBack(isVisible = false)
- }
-
- //endregion
-
- //region Initialize
- // ************************************************************
-
- private fun initializeViewModel() {
- viewModel = ViewModelProvider(
- this,
- ViewModelProvider.AndroidViewModelFactory.getInstance(application)
- )[AuthSetupRepeatViewModel::class.java]
-
- viewModel.finishScreenLiveData.observe(this, object : EventObserver() {
- override fun onUnhandledEvent(value: Boolean) {
- setResult(
- Activity.RESULT_OK,
- createResult(
- doesMatch = value,
- useFullPassword = false
- )
- )
- finish()
- }
- })
- }
-
- private fun initializeViews() {
- binding.instructionTextview.setText(R.string.auth_setup_repeat_info)
- binding.passcodeView.passcodeListener = object : PasscodeView.PasscodeListener {
- override fun onInputChanged() {
- }
-
- override fun onDone() {
- onConfirmClicked()
- }
- }
- binding.fullPasswordButton.setOnClickListener {
- setResult(
- Activity.RESULT_OK,
- createResult(
- doesMatch = false,
- useFullPassword = true
- )
- )
- finish()
- }
- binding.passcodeView.requestFocus()
- }
-
- //endregion
-
- //region Control/UI
- // ************************************************************
-
- private fun onConfirmClicked() {
- viewModel.checkPassword(binding.passcodeView.getPasscode())
- }
-
- //endregion
-
- override fun loggedOut() {
- // No need to show auth, as it is anyway requested further.
- }
-
- companion object {
- private const val DOES_MATCH_EXTRA = "does_match"
- private const val USE_FULL_PASSWORD_EXTRA = "use_full_password"
-
- private fun createResult(
- doesMatch: Boolean,
- useFullPassword: Boolean,
- ) = Intent().apply {
- putExtra(DOES_MATCH_EXTRA, doesMatch)
- putExtra(USE_FULL_PASSWORD_EXTRA, useFullPassword)
- }
-
- fun doesMatch(result: Intent): Boolean =
- result.getBooleanExtra(DOES_MATCH_EXTRA, false)
-
- fun useFullPassword(result: Intent): Boolean =
- result.getBooleanExtra(USE_FULL_PASSWORD_EXTRA, false)
- }
-}
diff --git a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatViewModel.kt
deleted file mode 100644
index 57f5aa0d..00000000
--- a/app/src/main/java/com/concordium/wallet/ui/auth/setuprepeat/AuthSetupRepeatViewModel.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.concordium.wallet.ui.auth.setuprepeat
-
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import com.concordium.wallet.App
-import com.concordium.wallet.core.arch.Event
-import com.concordium.wallet.core.authentication.Session
-
-class AuthSetupRepeatViewModel(application: Application) : AndroidViewModel(application) {
-
-// App.appCore.getCurrentAuthenticationManager()
- private val session: Session = App.appCore.session
-
- private val _finishScreenLiveData = MutableLiveData>()
- val finishScreenLiveData: LiveData>
- get() = _finishScreenLiveData
-
- fun initialize() {
- }
-
- fun checkPassword(password: String) {
- val isEqual = session.checkPassword(password)
- _finishScreenLiveData.value = Event(isEqual)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt
index b80f8bdf..e6c1d141 100644
--- a/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/bakerdelegation/common/DelegationBakerViewModel.kt
@@ -2,7 +2,6 @@ package com.concordium.wallet.ui.bakerdelegation.common
import android.app.Application
import android.net.Uri
-import android.text.TextUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -41,7 +40,6 @@ import com.concordium.wallet.data.model.TransactionStatus
import com.concordium.wallet.data.model.TransactionType
import com.concordium.wallet.data.model.TransferSubmissionStatus
import com.concordium.wallet.data.room.Transfer
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.data.util.FileUtil
import com.concordium.wallet.ui.common.BackendErrorHandler
import com.concordium.wallet.util.DateTimeUtil
@@ -58,7 +56,8 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl
lateinit var bakerDelegationData: BakerDelegationData
private val proxyRepository = ProxyRepository()
- private val transferRepository: TransferRepository
+ private val transferRepository =
+ TransferRepository(App.appCore.session.walletStorage.database.transferDao())
private var bakerPoolRequest: BackendRequest? = null
private var accountNonceRequest: BackendRequest? = null
@@ -116,11 +115,6 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl
val bakerPoolStatusLiveData: MutableLiveData
get() = _bakerPoolStatusLiveData
- init {
- val transferDao = WalletDatabase.getDatabase(application).transferDao()
- transferRepository = TransferRepository(transferDao)
- }
-
fun initialize(bakerDelegationData: BakerDelegationData) {
this.bakerDelegationData = bakerDelegationData
}
@@ -462,13 +456,17 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl
Log.d("decryptAndContinue")
bakerDelegationData.account?.let { account ->
val storageAccountDataEncrypted = account.encryptedAccountData
- if (TextUtils.isEmpty(storageAccountDataEncrypted)) {
+ if (storageAccountDataEncrypted == null) {
_errorLiveData.value = Event(R.string.app_error_general)
_waitingLiveData.value = false
return
}
- val decryptedJson = App.appCore.getCurrentAuthenticationManager()
- .decryptInBackground(password, storageAccountDataEncrypted)
+ val decryptedJson = App.appCore.auth
+ .decrypt(
+ password = password,
+ encryptedData = storageAccountDataEncrypted,
+ )
+ ?.let(::String)
if (decryptedJson != null) {
val credentialsOutput =
@@ -759,7 +757,7 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl
}
}
- fun bakerKeysJson(): String? {
+ private fun bakerKeysJson(): String? {
_bakerKeysLiveData.value?.let { bakerKeys ->
bakerKeys.bakerId = bakerDelegationData.account?.index
return if (bakerKeys.toString()
diff --git a/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt b/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt
index 1e766d92..8c620f75 100644
--- a/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/base/BaseActivity.kt
@@ -9,6 +9,7 @@ import android.os.storage.StorageManager
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
@@ -19,7 +20,6 @@ import androidx.appcompat.widget.Toolbar
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
-import androidx.lifecycle.Observer
import com.concordium.wallet.App
import com.concordium.wallet.Constants
import com.concordium.wallet.Constants.Extras.EXTRA_ADD_CONTACT
@@ -52,9 +52,12 @@ abstract class BaseActivity(
var isActive = false
private var backBtn: ImageView? = null
- private var plusLeftBtn: ImageView? = null
- private var plusRightBtn: ImageView? = null
+ private var plusLeftBtn: FrameLayout? = null
+ private var plusLeftBtnNotice: View? = null
+ private var plusRightBtn: FrameLayout? = null
+ private var plusRightBtnNotice: View? = null
private var qrScanBtn: ImageView? = null
+ private var infoBtn: ImageView? = null
protected var closeBtn: ImageView? = null
protected var deleteBtn: ImageView? = null
private var settingsBtn: ImageView? = null
@@ -81,8 +84,11 @@ abstract class BaseActivity(
backBtn = toolbar?.findViewById(R.id.toolbar_back_btn)
plusLeftBtn = toolbar?.findViewById(R.id.toolbar_plus_btn)
+ plusLeftBtnNotice = toolbar?.findViewById(R.id.toolbar_plus_btn_notice)
plusRightBtn = toolbar?.findViewById(R.id.toolbar_plus_btn_add_contact)
+ plusRightBtnNotice = toolbar?.findViewById(R.id.toolbar_plus_btn_add_contact_notice)
qrScanBtn = toolbar?.findViewById(R.id.toolbar_qr_btn)
+ infoBtn = toolbar?.findViewById(R.id.toolbar_info_btn)
closeBtn = toolbar?.findViewById(R.id.toolbar_close_btn)
deleteBtn = toolbar?.findViewById(R.id.toolbar_delete_btn)
settingsBtn = toolbar?.findViewById(R.id.toolbar_settings_btn)
@@ -96,15 +102,15 @@ abstract class BaseActivity(
popup = Popup()
dialogs = Dialogs()
- App.appCore.session.isLoggedIn.observe(this, Observer { loggedin ->
- if (App.appCore.session.hasSetupPassword) {
- if (loggedin) {
+ App.appCore.session.isLoggedIn.observe(this) { loggedIn ->
+ if (App.appCore.setup.isAuthSetupCompleted) {
+ if (loggedIn) {
loggedIn()
} else {
loggedOut()
}
}
- })
+ }
}
override fun onResume() {
@@ -202,14 +208,29 @@ abstract class BaseActivity(
}
}
- fun hideRightPlus(isVisible: Boolean, listener: View.OnClickListener? = null) {
+ fun hideInfo(isVisible: Boolean, listener: View.OnClickListener? = null) {
+ infoBtn?.isVisible = isVisible
+ infoBtn?.setOnClickListener(listener)
+ }
+
+ fun hideRightPlus(
+ isVisible: Boolean,
+ hasNotice: Boolean = false,
+ listener: View.OnClickListener? = null
+ ) {
plusRightBtn?.isVisible = isVisible
plusRightBtn?.setOnClickListener(listener)
+ plusRightBtnNotice?.isVisible = hasNotice
}
- fun hideLeftPlus(isVisible: Boolean, listener: View.OnClickListener? = null) {
+ fun hideLeftPlus(
+ isVisible: Boolean,
+ hasNotice: Boolean = false,
+ listener: View.OnClickListener? = null
+ ) {
plusLeftBtn?.isVisible = isVisible
plusLeftBtn?.setOnClickListener(listener)
+ plusLeftBtnNotice?.isVisible = hasNotice
}
fun hideSettings(isVisible: Boolean, listener: View.OnClickListener? = null) {
@@ -271,8 +292,8 @@ abstract class BaseActivity(
}
fun showAuthentication(text: String = authenticateText(), callback: AuthenticationCallback) {
- val useBiometrics = App.appCore.getCurrentAuthenticationManager().useBiometrics()
- val usePasscode = App.appCore.getCurrentAuthenticationManager().usePasscode()
+ val useBiometrics = App.appCore.auth.isBiometricsUsed()
+ val usePasscode = App.appCore.auth.isPasscodeUsed()
if (useBiometrics) {
showBiometrics(text, usePasscode, callback)
} else {
@@ -314,7 +335,10 @@ abstract class BaseActivity(
}
}
- private fun createBiometricPrompt(text: String?, callback: AuthenticationCallback): BiometricPrompt {
+ private fun createBiometricPrompt(
+ text: String?,
+ callback: AuthenticationCallback
+ ): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(this)
val callback = object : BiometricPromptCallback() {
@@ -345,8 +369,8 @@ abstract class BaseActivity(
}
fun authenticateText(): String {
- val useBiometrics = App.appCore.getCurrentAuthenticationManager().useBiometrics()
- val usePasscode = App.appCore.getCurrentAuthenticationManager().usePasscode()
+ val useBiometrics = App.appCore.auth.isBiometricsUsed()
+ val usePasscode = App.appCore.auth.isPasscodeUsed()
return when {
useBiometrics -> getString(R.string.auth_login_biometrics_dialog_subtitle)
usePasscode -> getString(R.string.auth_login_biometrics_dialog_cancel_passcode)
diff --git a/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt
index 7673f39f..b815d96f 100644
--- a/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/cis2/SendTokenViewModel.kt
@@ -1,7 +1,6 @@
package com.concordium.wallet.ui.cis2
import android.app.Application
-import android.text.TextUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
@@ -13,6 +12,7 @@ import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.ContractTokensRepository
import com.concordium.wallet.data.TransferRepository
import com.concordium.wallet.data.backend.repository.ProxyRepository
+import com.concordium.wallet.data.cryptolib.ContractAddress
import com.concordium.wallet.data.cryptolib.CreateAccountTransactionInput
import com.concordium.wallet.data.cryptolib.CreateTransferInput
import com.concordium.wallet.data.cryptolib.CreateTransferOutput
@@ -29,14 +29,12 @@ import com.concordium.wallet.data.model.Token
import com.concordium.wallet.data.model.TransactionOutcome
import com.concordium.wallet.data.model.TransactionStatus
import com.concordium.wallet.data.model.TransactionType
+import com.concordium.wallet.data.preferences.WalletSendFundsPreferences
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.Transfer
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.data.walletconnect.AccountTransactionPayload
-import com.concordium.wallet.data.cryptolib.ContractAddress
import com.concordium.wallet.ui.account.common.accountupdater.AccountUpdater
import com.concordium.wallet.ui.common.BackendErrorHandler
-import com.concordium.wallet.ui.transaction.sendfunds.SendFundsPreferences
import com.concordium.wallet.ui.transaction.sendfunds.SendFundsViewModel
import com.concordium.wallet.util.CBORUtil
import com.concordium.wallet.util.DateTimeUtil
@@ -78,11 +76,11 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
}
private val proxyRepository = ProxyRepository()
- private val transferRepository: TransferRepository
+ private val transferRepository =
+ TransferRepository(App.appCore.session.walletStorage.database.transferDao())
private val accountUpdater = AccountUpdater(application, viewModelScope)
- private val sendFundsPreferences by lazy {
- SendFundsPreferences(getApplication())
- }
+ private val sendFundsPreferences: WalletSendFundsPreferences =
+ App.appCore.session.walletStorage.sendFundsPreferences
private var accountNonceRequest: BackendRequest? = null
private var globalParamsRequest: BackendRequest? = null
@@ -108,9 +106,6 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
}
init {
- transferRepository =
- TransferRepository(WalletDatabase.getDatabase(application).transferDao())
-
chooseToken.observeForever { token ->
sendTokenData.token = token
sendTokenData.max = if (token.isCcd) null else token.balance
@@ -131,7 +126,7 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
waiting.postValue(true)
CoroutineScope(Dispatchers.IO).launch {
val contractTokensRepository = ContractTokensRepository(
- WalletDatabase.getDatabase(getApplication()).contractTokenDao()
+ App.appCore.session.walletStorage.database.contractTokenDao()
)
val tokensFound = mutableListOf()
tokensFound.add(getCCDDefaultToken(accountAddress))
@@ -201,11 +196,11 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
sendTokenData.memo?.let(CBORUtil.Companion::decodeHexAndCBOR)
fun showMemoWarning(): Boolean {
- return sendFundsPreferences.showMemoWarning()
+ return sendFundsPreferences.shouldShowMemoWarning()
}
fun dontShowMemoWarning() {
- return sendFundsPreferences.dontShowMemoWarning()
+ return sendFundsPreferences.disableShowMemoWarning()
}
fun onReceiverEntered(input: String) {
@@ -331,7 +326,7 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
private suspend fun getCCDDefaultToken(accountAddress: String): Token {
val accountRepository =
- AccountRepository(WalletDatabase.getDatabase(getApplication()).accountDao())
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
val account = accountRepository.findByAddress(accountAddress)
?: error("Account $accountAddress not found")
return Token.ccd(account)
@@ -345,16 +340,21 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
private suspend fun decryptAndContinue(password: String) {
sendTokenData.account?.let { account ->
val storageAccountDataEncrypted = account.encryptedAccountData
- if (TextUtils.isEmpty(storageAccountDataEncrypted)) {
+ if (storageAccountDataEncrypted == null) {
errorInt.postValue(R.string.app_error_general)
waiting.postValue(false)
return
}
- val decryptedJson = App.appCore.getCurrentAuthenticationManager()
- .decryptInBackground(password, storageAccountDataEncrypted)
- val credentialsOutput =
- App.appCore.gson.fromJson(decryptedJson, StorageAccountData::class.java)
+ val decryptedJson = App.appCore.auth
+ .decrypt(
+ password = password,
+ encryptedData = storageAccountDataEncrypted
+ )
+ ?.let(::String)
+
if (decryptedJson != null) {
+ val credentialsOutput =
+ App.appCore.gson.fromJson(decryptedJson, StorageAccountData::class.java)
getAccountEncryptedKey(credentialsOutput)
} else {
errorInt.postValue(R.string.app_error_encryption)
diff --git a/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt b/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt
index d105274c..e5965183 100644
--- a/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/cis2/TokensViewModel.kt
@@ -4,13 +4,13 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
+import com.concordium.wallet.App
import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.ContractTokensRepository
import com.concordium.wallet.data.backend.repository.ProxyRepository
import com.concordium.wallet.data.model.Token
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.ContractToken
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.cis2.retrofit.IncorrectChecksumException
import com.concordium.wallet.ui.cis2.retrofit.MetadataApiInstance
import com.concordium.wallet.ui.common.BackendErrorHandler
@@ -66,7 +66,7 @@ class TokensViewModel(application: Application) : AndroidViewModel(application)
private val proxyRepository = ProxyRepository()
private val contractTokensRepository: ContractTokensRepository by lazy {
ContractTokensRepository(
- WalletDatabase.getDatabase(getApplication()).contractTokenDao()
+ App.appCore.session.walletStorage.database.contractTokenDao()
)
}
@@ -445,7 +445,7 @@ class TokensViewModel(application: Application) : AndroidViewModel(application)
private suspend fun getCCDDefaultToken(accountAddress: String): Token {
val accountRepository =
- AccountRepository(WalletDatabase.getDatabase(getApplication()).accountDao())
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
val account = accountRepository.findByAddress(accountAddress)
?: error("Account $accountAddress not found")
return Token.ccd(account)
diff --git a/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt b/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt
index 6c50e64b..cbdf2fff 100644
--- a/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/common/delegates/AuthDelegate.kt
@@ -27,16 +27,16 @@ class AuthDelegateImpl : AuthDelegate {
) {
activity.showAuthentication(callback = object : BaseActivity.AuthenticationCallback {
override fun getCipherForBiometrics(): Cipher? =
- App.appCore.getCurrentAuthenticationManager()
- .initBiometricsCipherForDecryption()
+ App.appCore.auth
+ .getBiometricsCipherForDecryption()
override fun onCorrectPassword(password: String) =
onAuthenticated(password)
override fun onCipher(cipher: Cipher) {
activity.lifecycleScope.launch(Dispatchers.IO) {
- val password = App.appCore.getCurrentAuthenticationManager()
- .checkPasswordInBackground(cipher)
+ val password = App.appCore.auth
+ .decryptPasswordWithBiometricsCipher(cipher)
if (password != null) {
withContext(Dispatchers.Main) {
onAuthenticated(password)
diff --git a/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt b/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt
index ff528caf..647d6307 100644
--- a/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/common/delegates/IdentityStatusDelegate.kt
@@ -1,13 +1,12 @@
package com.concordium.wallet.ui.common.delegates
+import android.app.Activity
import android.content.Intent
-import androidx.core.app.ComponentActivity
import com.concordium.wallet.App
import com.concordium.wallet.R
import com.concordium.wallet.data.IdentityRepository
import com.concordium.wallet.data.model.IdentityStatus
import com.concordium.wallet.data.room.Identity
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.MainViewModel
import com.concordium.wallet.ui.base.BaseActivity
import com.concordium.wallet.ui.identity.identityconfirmed.IdentityConfirmedActivity
@@ -24,20 +23,20 @@ import kotlin.concurrent.schedule
interface IdentityStatusDelegate {
fun startCheckForPendingIdentity(
- activity: ComponentActivity?,
+ activity: Activity?,
specificIdentityId: Int?,
showForFirstIdentity: Boolean,
statusChanged: (Identity) -> Unit
)
fun identityDone(
- activity: ComponentActivity,
+ activity: Activity,
identity: Identity,
statusChanged: (Identity) -> Unit
)
fun identityError(
- activity: ComponentActivity,
+ activity: Activity,
identity: Identity,
statusChanged: (Identity) -> Unit
)
@@ -50,7 +49,7 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate {
private var showForFirstIdentity = false
override fun startCheckForPendingIdentity(
- activity: ComponentActivity?,
+ activity: Activity?,
specificIdentityId: Int?,
showForFirstIdentity: Boolean,
statusChanged: (Identity) -> Unit
@@ -58,13 +57,13 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate {
this.showForFirstIdentity = showForFirstIdentity
if (activity == null || activity.isFinishing || activity.isDestroyed)
return
- if (App.appCore.newIdentities.isNotEmpty()) {
- for (newIdentity in App.appCore.newIdentities) {
+ if (App.appCore.session.newIdentities.isNotEmpty()) {
+ for (newIdentity in App.appCore.session.newIdentities) {
if (specificIdentityId == null || specificIdentityId == newIdentity.key) {
CoroutineScope(Dispatchers.IO).launch {
job = launch {
val identityRepository = IdentityRepository(
- WalletDatabase.getDatabase(activity).identityDao()
+ App.appCore.session.walletStorage.database.identityDao()
)
val identity = identityRepository.findById(newIdentity.key)
identity?.let {
@@ -114,13 +113,13 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate {
}
override fun identityDone(
- activity: ComponentActivity,
+ activity: Activity,
identity: Identity,
statusChanged: (Identity) -> Unit
) {
- if (App.appCore.newIdentities[identity.id] == null)
+ if (App.appCore.session.newIdentities[identity.id] == null)
return
- App.appCore.newIdentities.remove(identity.id)
+ App.appCore.session.newIdentities.remove(identity.id)
if (showForFirstIdentity) {
statusChanged(identity)
@@ -153,13 +152,13 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate {
}
override fun identityError(
- activity: ComponentActivity,
+ activity: Activity,
identity: Identity,
statusChanged: (Identity) -> Unit
) {
- if (App.appCore.newIdentities[identity.id] == null)
+ if (App.appCore.session.newIdentities[identity.id] == null)
return
- App.appCore.newIdentities.remove(identity.id)
+ App.appCore.session.newIdentities.remove(identity.id)
if (showForFirstIdentity) {
statusChanged(identity)
@@ -176,7 +175,7 @@ class IdentityStatusDelegateImpl : IdentityStatusDelegate {
}
private fun identityErrorNextIdentity(
- activity: ComponentActivity,
+ activity: Activity,
identity: Identity,
builder: MaterialAlertDialogBuilder,
statusChanged: (Identity) -> Unit
diff --git a/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt b/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt
index 18fea960..3edcedb6 100644
--- a/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/common/identity/IdentityUpdater.kt
@@ -14,7 +14,6 @@ import com.concordium.wallet.data.model.TransactionStatus
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.Identity
import com.concordium.wallet.data.room.Recipient
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.cis2.defaults.DefaultFungibleTokensManager
import com.concordium.wallet.ui.cis2.defaults.DefaultTokensManagerFactory
import com.concordium.wallet.util.Log
@@ -30,28 +29,24 @@ import java.net.URL
class IdentityUpdater(val application: Application, private val viewModelScope: CoroutineScope) {
private val gson = App.appCore.gson
- private val identityRepository: IdentityRepository
- private val accountRepository: AccountRepository
- private val recipientRepository: RecipientRepository
+ private val identityRepository =
+ IdentityRepository(App.appCore.session.walletStorage.database.identityDao())
+ private val accountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
+ private val recipientRepository =
+ RecipientRepository(App.appCore.session.walletStorage.database.recipientDao())
private val defaultFungibleTokensManager: DefaultFungibleTokensManager
- private val updateNotificationsSubscriptionUseCase by lazy {
- UpdateNotificationsSubscriptionUseCase(application)
- }
+ private val updateNotificationsSubscriptionUseCase by lazy(::UpdateNotificationsSubscriptionUseCase)
private var updateListener: UpdateListener? = null
private var run = true
init {
- val identityDao = WalletDatabase.getDatabase(application).identityDao()
- identityRepository = IdentityRepository(identityDao)
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
- val recipientDao = WalletDatabase.getDatabase(application).recipientDao()
- recipientRepository = RecipientRepository(recipientDao)
-
- val contractTokenDao = WalletDatabase.getDatabase(application).contractTokenDao()
+
val defaultTokensManagerFactory = DefaultTokensManagerFactory(
- contractTokensRepository = ContractTokensRepository(contractTokenDao),
+ contractTokensRepository = ContractTokensRepository(
+ App.appCore.session.walletStorage.database.contractTokenDao()
+ ),
)
defaultFungibleTokensManager = defaultTokensManagerFactory.getDefaultFungibleTokensManager()
}
@@ -102,9 +97,13 @@ class IdentityUpdater(val application: Application, private val viewModelScope:
val resp = URL(identity.codeUri).readText()
Log.d("Identity poll response: $resp")
- val identityTokenContainer = gson.fromJson(resp, IdentityTokenContainer::class.java)
+ val identityTokenContainer = gson.fromJson(
+ resp,
+ IdentityTokenContainer::class.java
+ )
- val newStatus = if (BuildConfig.FAIL_IDENTITY_CREATION) IdentityStatus.ERROR else identityTokenContainer.status
+ val newStatus =
+ if (BuildConfig.FAIL_IDENTITY_CREATION) IdentityStatus.ERROR else identityTokenContainer.status
if (newStatus != IdentityStatus.PENDING) {
identity.status = identityTokenContainer.status
@@ -117,7 +116,8 @@ class IdentityUpdater(val application: Application, private val viewModelScope:
val accountAddress = token.accountAddress
identity.identityObject = identityContainer.value
identityRepository.update(identity)
- val account = accountRepository.getAllByIdentityId(identity.id).firstOrNull()
+ val account =
+ accountRepository.getAllByIdentityId(identity.id).firstOrNull()
account?.let {
if (it.address == accountAddress) {
// it.credential = CredentialWrapper(RawJson(gson.toJson(CredentialContentWrapper(accountCredentialWrapper.value))), accountCredentialWrapper.v) //Make up for protocol inconsistency
@@ -143,14 +143,15 @@ class IdentityUpdater(val application: Application, private val viewModelScope:
} else if (newStatus == IdentityStatus.ERROR) {
identityRepository.update(identity)
- val account = accountRepository.getAllByIdentityId(identity.id).firstOrNull()
+ val account =
+ accountRepository.getAllByIdentityId(identity.id).firstOrNull()
account?.let { accountRepository.delete(it) }
withContext(Dispatchers.Main) {
updateListener?.onError(identity, account)
}
}
- if (App.appCore.newIdentities[identity.id] == null) {
- App.appCore.newIdentities[identity.id] = identity
+ if (App.appCore.session.newIdentities[identity.id] == null) {
+ App.appCore.session.newIdentities[identity.id] = identity
}
} else {
hasMorePending = true
diff --git a/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt b/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt
index 62dcf92e..87d2b319 100644
--- a/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/connect/ConnectActivity.kt
@@ -14,13 +14,10 @@ import com.concordium.wallet.Constants
import com.concordium.wallet.Constants.Extras.EXTRA_ADD_CONTACT
import com.concordium.wallet.Constants.Extras.EXTRA_CONNECT_URL
import com.concordium.wallet.R
-import com.concordium.wallet.data.AccountRepository
import com.concordium.wallet.data.backend.OfflineMockInterceptor
import com.concordium.wallet.data.backend.ws.WsTransport
import com.concordium.wallet.data.model.WsConnectionInfo
import com.concordium.wallet.data.model.WsMessageResponse
-import com.concordium.wallet.data.room.Account
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.MainActivity
import com.concordium.wallet.ui.base.BaseActivity
import com.concordium.wallet.ui.common.BackendErrorHandler
@@ -43,10 +40,8 @@ class ConnectActivity : BaseActivity(R.layout.activity_connect) {
private lateinit var shopDescription: TextView
private lateinit var shopLogo: ImageView
private var wsTransport: WsTransport? = null
- private var accountRepository: AccountRepository? = null
private var currentSiteInfo: WsConnectionInfo.SiteInfo? = null
private var accountsPool: LinearLayout? = null
- private var selectedAccount: Account? = null
private val gson by lazy {
Gson()
@@ -124,9 +119,6 @@ class ConnectActivity : BaseActivity(R.layout.activity_connect) {
connectWc(connectUrl)
}
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
-
closeBtn?.visibility = View.VISIBLE
closeBtn?.setOnClickListener {
finish()
diff --git a/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt b/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt
index a4e7451c..54df04c2 100644
--- a/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt
+++ b/app/src/main/java/com/concordium/wallet/ui/connect/add_wallet/AddWalletNftActivity.kt
@@ -9,6 +9,7 @@ import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import com.bumptech.glide.Glide
+import com.concordium.wallet.App
import com.concordium.wallet.Constants
import com.concordium.wallet.R
import com.concordium.wallet.data.AccountRepository
@@ -17,7 +18,6 @@ import com.concordium.wallet.data.model.AccountInfo
import com.concordium.wallet.data.model.WsConnectionInfo
import com.concordium.wallet.data.model.WsMessageResponse
import com.concordium.wallet.data.room.Account
-import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.data.util.CurrencyUtil
import com.concordium.wallet.ui.base.BaseActivity
import com.google.gson.Gson
@@ -27,7 +27,8 @@ import kotlinx.coroutines.launch
class AddWalletNftActivity : BaseActivity(R.layout.activity_connect, R.string.title_add_wallet) {
- private var accountRepository: AccountRepository? = null
+ private val accountRepository =
+ AccountRepository(App.appCore.session.walletStorage.database.accountDao())
private var siteInfo: WsConnectionInfo.SiteInfo? = null
private var payload: WsMessageResponse.Payload? = null
@@ -48,9 +49,6 @@ class AddWalletNftActivity : BaseActivity(R.layout.activity_connect, R.string.ti
descTitle.setText(R.string.title_add_wallet)
accountsPool = findViewById(R.id.accountsPool)
-
- val accountDao = WalletDatabase.getDatabase(application).accountDao()
- accountRepository = AccountRepository(accountDao)
}
override fun onResume() {
@@ -69,7 +67,8 @@ class AddWalletNftActivity : BaseActivity(R.layout.activity_connect, R.string.ti
shopName.text = siteInfo?.title
shopDescription.text = siteInfo?.description
- Glide.with(applicationContext).load(siteInfo?.iconLink).placeholder(R.drawable.ic_favicon).error(R.drawable.ic_favicon).into(shopLogo)
+ Glide.with(applicationContext).load(siteInfo?.iconLink).placeholder(R.drawable.ic_favicon)
+ .error(R.drawable.ic_favicon).into(shopLogo)
findViewById