mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Compare commits
10 Commits
release-no
...
v2024.11.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5752c10ed | ||
|
|
ec85e7af61 | ||
|
|
816b9769a1 | ||
|
|
25097cbae1 | ||
|
|
5a4b8d64ab | ||
|
|
5523d99400 | ||
|
|
9f8d21cb95 | ||
|
|
75fc9fe210 | ||
|
|
42671aadfb | ||
|
|
d71389ab02 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -382,7 +382,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish Play Store bundle
|
- name: Publish Play Store bundle
|
||||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
|
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
|
||||||
run: bundle exec fastlane publishBetaToPlayStore
|
run: |
|
||||||
|
bundle exec fastlane publishProdToPlayStore
|
||||||
|
bundle exec fastlane publishBetaToPlayStore
|
||||||
|
|
||||||
publish_fdroid:
|
publish_fdroid:
|
||||||
name: Publish F-Droid artifacts
|
name: Publish F-Droid artifacts
|
||||||
|
|||||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -6,6 +6,10 @@
|
|||||||
# we keep it here.
|
# we keep it here.
|
||||||
-keep class com.bitwarden.** { *; }
|
-keep class com.bitwarden.** { *; }
|
||||||
|
|
||||||
|
# The Android Verifier component must be kept because it looks like dead code. Proguard is unable to
|
||||||
|
# see any JNI usage, so our rules must manually opt into keeping it.
|
||||||
|
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Bitwarden Models
|
# Bitwarden Models
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "f28200334a5c94feed1d9712e04ff01b",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "ciphers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "cipherType",
|
||||||
|
"columnName": "cipher_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "cipherJson",
|
||||||
|
"columnName": "cipher_json",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_ciphers_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "collections",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "organizationId",
|
||||||
|
"columnName": "organization_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shouldHidePasswords",
|
||||||
|
"columnName": "should_hide_passwords",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "externalId",
|
||||||
|
"columnName": "external_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isReadOnly",
|
||||||
|
"columnName": "read_only",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_collections_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "domains",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domainsJson",
|
||||||
|
"columnName": "domains_json",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "revisionDate",
|
||||||
|
"columnName": "revision_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_folders_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sends",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sendType",
|
||||||
|
"columnName": "send_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sendJson",
|
||||||
|
"columnName": "send_json",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_sends_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'f28200334a5c94feed1d9712e04ff01b')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "f28200334a5c94feed1d9712e04ff01b",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "ciphers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "cipherType",
|
||||||
|
"columnName": "cipher_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "cipherJson",
|
||||||
|
"columnName": "cipher_json",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_ciphers_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "collections",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "organizationId",
|
||||||
|
"columnName": "organization_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shouldHidePasswords",
|
||||||
|
"columnName": "should_hide_passwords",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "externalId",
|
||||||
|
"columnName": "external_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isReadOnly",
|
||||||
|
"columnName": "read_only",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_collections_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "domains",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domainsJson",
|
||||||
|
"columnName": "domains_json",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "revisionDate",
|
||||||
|
"columnName": "revision_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_folders_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sends",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sendType",
|
||||||
|
"columnName": "send_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sendJson",
|
||||||
|
"columnName": "send_json",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_sends_user_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'f28200334a5c94feed1d9712e04ff01b')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ class BitwardenAccessibilityService : AccessibilityService() {
|
|||||||
|
|
||||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||||
if (rootInActiveWindow?.packageName != event.packageName) return
|
if (rootInActiveWindow?.packageName != event.packageName) return
|
||||||
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = rootInActiveWindow)
|
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = event.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInterrupt() = Unit
|
override fun onInterrupt() = Unit
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package com.x8bit.bitwarden.data.platform.repository.util
|
package com.x8bit.bitwarden.data.platform.repository.util
|
||||||
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -31,3 +33,34 @@ fun <T, R> MutableStateFlow<T>.observeWhenSubscribedAndLoggedIn(
|
|||||||
.flatMapLatest { activeUserId ->
|
.flatMapLatest { activeUserId ->
|
||||||
activeUserId?.let(observer) ?: flow { awaitCancellation() }
|
activeUserId?.let(observer) ?: flow { awaitCancellation() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the [observer] callback whenever the user is logged in, the active user changes, the
|
||||||
|
* vault for the user changes, and there are subscribers to the [MutableStateFlow]. The flow from
|
||||||
|
* all previous calls to the `observer` is canceled whenever the `observer` is re-invoked, there
|
||||||
|
* is no active user (logged-out), there are no subscribers to the [MutableStateFlow], or the vault
|
||||||
|
* is not unlocked.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T, R> MutableStateFlow<T>.observeWhenSubscribedAndUnlocked(
|
||||||
|
userStateFlow: Flow<UserStateJson?>,
|
||||||
|
vaultUnlockFlow: Flow<List<VaultUnlockData>>,
|
||||||
|
observer: (activeUserId: String) -> Flow<R>,
|
||||||
|
): Flow<R> =
|
||||||
|
combine(
|
||||||
|
this.subscriptionCount.map { it > 0 }.distinctUntilChanged(),
|
||||||
|
userStateFlow.map { it?.activeUserId }.distinctUntilChanged(),
|
||||||
|
userStateFlow.map { it?.activeUserId }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.filterNotNull()
|
||||||
|
.flatMapLatest { activeUserId ->
|
||||||
|
vaultUnlockFlow
|
||||||
|
.map { it.any { it.userId == activeUserId } }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
},
|
||||||
|
) { isSubscribed, activeUserId, isUnlocked ->
|
||||||
|
activeUserId.takeIf { isSubscribed && isUnlocked }
|
||||||
|
}
|
||||||
|
.flatMapLatest { activeUserId ->
|
||||||
|
activeUserId?.let(observer) ?: flow { awaitCancellation() }
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ class GeneratorRepositoryImpl(
|
|||||||
|
|
||||||
override suspend fun generateForwardedServiceUsername(
|
override suspend fun generateForwardedServiceUsername(
|
||||||
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
|
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
|
||||||
): GeneratedForwardedServiceUsernameResult =
|
): GeneratedForwardedServiceUsernameResult = withContext(scope.coroutineContext) {
|
||||||
generatorSdkSource.generateForwardedServiceEmail(forwardedServiceGeneratorRequest)
|
generatorSdkSource.generateForwardedServiceEmail(forwardedServiceGeneratorRequest)
|
||||||
.fold(
|
.fold(
|
||||||
onSuccess = { generatedEmail ->
|
onSuccess = { generatedEmail ->
|
||||||
@@ -200,6 +201,7 @@ class GeneratorRepositoryImpl(
|
|||||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
|
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
|
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
|
||||||
val userId = authDiskSource.userState?.activeUserId
|
val userId = authDiskSource.userState?.activeUserId
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ interface VaultDiskSource {
|
|||||||
/**
|
/**
|
||||||
* Retrieves all domains from the data source for a given [userId].
|
* Retrieves all domains from the data source for a given [userId].
|
||||||
*/
|
*/
|
||||||
fun getDomains(userId: String): Flow<SyncResponseJson.Domains>
|
fun getDomains(userId: String): Flow<SyncResponseJson.Domains?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a folder from the data source for the given [userId] and [folderId].
|
* Deletes a folder from the data source for the given [userId] and [folderId].
|
||||||
|
|||||||
@@ -119,12 +119,12 @@ class VaultDiskSourceImpl(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains> =
|
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains?> =
|
||||||
domainsDao
|
domainsDao
|
||||||
.getDomains(userId)
|
.getDomains(userId)
|
||||||
.map { entity ->
|
.map { entity ->
|
||||||
withContext(dispatcherManager.default) {
|
withContext(dispatcherManager.default) {
|
||||||
json.decodeFromString<SyncResponseJson.Domains>(entity.domainsJson)
|
entity?.domainsJson?.let { json.decodeFromString<SyncResponseJson.Domains>(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ class VaultDiskSourceImpl(
|
|||||||
domainsDao.insertDomains(
|
domainsDao.insertDomains(
|
||||||
domains = DomainsEntity(
|
domains = DomainsEntity(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
domainsJson = json.encodeToString(vault.domains),
|
domainsJson = vault.domains?.let { json.encodeToString(it) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface DomainsDao {
|
|||||||
@Query("SELECT * FROM domains WHERE user_id = :userId")
|
@Query("SELECT * FROM domains WHERE user_id = :userId")
|
||||||
fun getDomains(
|
fun getDomains(
|
||||||
userId: String,
|
userId: String,
|
||||||
): Flow<DomainsEntity>
|
): Flow<DomainsEntity?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts domains into the database.
|
* Inserts domains into the database.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
|
|||||||
FolderEntity::class,
|
FolderEntity::class,
|
||||||
SendEntity::class,
|
SendEntity::class,
|
||||||
],
|
],
|
||||||
version = 3,
|
version = 5,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@TypeConverters(ZonedDateTimeTypeConverter::class)
|
@TypeConverters(ZonedDateTimeTypeConverter::class)
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ data class DomainsEntity(
|
|||||||
val userId: String,
|
val userId: String,
|
||||||
|
|
||||||
@ColumnInfo(name = "domains_json")
|
@ColumnInfo(name = "domains_json")
|
||||||
val domainsJson: String,
|
val domainsJson: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||||
|
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ private const val DEFAULT_FIDO_2_KEY_CURVE = "P-256"
|
|||||||
* @property domains A domains object associated with the vault data.
|
* @property domains A domains object associated with the vault data.
|
||||||
* @property sends A list of send objects associated with the vault data (nullable).
|
* @property sends A list of send objects associated with the vault data (nullable).
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SyncResponseJson(
|
data class SyncResponseJson(
|
||||||
@SerialName("folders")
|
@SerialName("folders")
|
||||||
@@ -30,6 +33,7 @@ data class SyncResponseJson(
|
|||||||
val collections: List<Collection>?,
|
val collections: List<Collection>?,
|
||||||
|
|
||||||
@SerialName("profile")
|
@SerialName("profile")
|
||||||
|
@JsonNames("Profile")
|
||||||
val profile: Profile,
|
val profile: Profile,
|
||||||
|
|
||||||
@SerialName("ciphers")
|
@SerialName("ciphers")
|
||||||
@@ -39,7 +43,8 @@ data class SyncResponseJson(
|
|||||||
val policies: List<Policy>?,
|
val policies: List<Policy>?,
|
||||||
|
|
||||||
@SerialName("domains")
|
@SerialName("domains")
|
||||||
val domains: Domains,
|
@JsonNames("Domains")
|
||||||
|
val domains: Domains?,
|
||||||
|
|
||||||
@SerialName("sends")
|
@SerialName("sends")
|
||||||
val sends: List<Send>?,
|
val sends: List<Send>?,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates
|
|||||||
import com.x8bit.bitwarden.data.platform.repository.util.map
|
import com.x8bit.bitwarden.data.platform.repository.util.map
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.mapNullable
|
import com.x8bit.bitwarden.data.platform.repository.util.mapNullable
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading
|
import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading
|
||||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||||
@@ -222,7 +223,13 @@ class VaultRepositoryImpl(
|
|||||||
// Cancel any ongoing sync request and clear the vault data in memory every time
|
// Cancel any ongoing sync request and clear the vault data in memory every time
|
||||||
// the user switches or the vault is locked for the active user.
|
// the user switches or the vault is locked for the active user.
|
||||||
merge(
|
merge(
|
||||||
authDiskSource.userSwitchingChangesFlow,
|
authDiskSource
|
||||||
|
.userSwitchingChangesFlow
|
||||||
|
.onEach {
|
||||||
|
// DomainState is not part of the locked data but should still be cleared
|
||||||
|
// when the user changes
|
||||||
|
mutableDomainsStateFlow.update { DataState.Loading }
|
||||||
|
},
|
||||||
vaultLockManager
|
vaultLockManager
|
||||||
.vaultUnlockDataStateFlow
|
.vaultUnlockDataStateFlow
|
||||||
.filter { vaultUnlockDataList ->
|
.filter { vaultUnlockDataList ->
|
||||||
@@ -238,7 +245,10 @@ class VaultRepositoryImpl(
|
|||||||
|
|
||||||
// Setup ciphers MutableStateFlow
|
// Setup ciphers MutableStateFlow
|
||||||
mutableCiphersStateFlow
|
mutableCiphersStateFlow
|
||||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
.observeWhenSubscribedAndUnlocked(
|
||||||
|
userStateFlow = authDiskSource.userStateFlow,
|
||||||
|
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||||
|
) { activeUserId ->
|
||||||
observeVaultDiskCiphers(activeUserId)
|
observeVaultDiskCiphers(activeUserId)
|
||||||
}
|
}
|
||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
@@ -250,19 +260,28 @@ class VaultRepositoryImpl(
|
|||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
// Setup folders MutableStateFlow
|
// Setup folders MutableStateFlow
|
||||||
mutableFoldersStateFlow
|
mutableFoldersStateFlow
|
||||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
.observeWhenSubscribedAndUnlocked(
|
||||||
|
userStateFlow = authDiskSource.userStateFlow,
|
||||||
|
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||||
|
) { activeUserId ->
|
||||||
observeVaultDiskFolders(activeUserId)
|
observeVaultDiskFolders(activeUserId)
|
||||||
}
|
}
|
||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
// Setup collections MutableStateFlow
|
// Setup collections MutableStateFlow
|
||||||
mutableCollectionsStateFlow
|
mutableCollectionsStateFlow
|
||||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
.observeWhenSubscribedAndUnlocked(
|
||||||
|
userStateFlow = authDiskSource.userStateFlow,
|
||||||
|
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||||
|
) { activeUserId ->
|
||||||
observeVaultDiskCollections(activeUserId)
|
observeVaultDiskCollections(activeUserId)
|
||||||
}
|
}
|
||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
// Setup sends MutableStateFlow
|
// Setup sends MutableStateFlow
|
||||||
mutableSendDataStateFlow
|
mutableSendDataStateFlow
|
||||||
.observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId ->
|
.observeWhenSubscribedAndUnlocked(
|
||||||
|
userStateFlow = authDiskSource.userStateFlow,
|
||||||
|
vaultUnlockFlow = vaultUnlockDataStateFlow,
|
||||||
|
) { activeUserId ->
|
||||||
observeVaultDiskSends(activeUserId)
|
observeVaultDiskSends(activeUserId)
|
||||||
}
|
}
|
||||||
.launchIn(unconfinedScope)
|
.launchIn(unconfinedScope)
|
||||||
@@ -305,7 +324,6 @@ class VaultRepositoryImpl(
|
|||||||
|
|
||||||
private fun clearUnlockedData() {
|
private fun clearUnlockedData() {
|
||||||
mutableCiphersStateFlow.update { DataState.Loading }
|
mutableCiphersStateFlow.update { DataState.Loading }
|
||||||
mutableDomainsStateFlow.update { DataState.Loading }
|
|
||||||
mutableFoldersStateFlow.update { DataState.Loading }
|
mutableFoldersStateFlow.update { DataState.Loading }
|
||||||
mutableCollectionsStateFlow.update { DataState.Loading }
|
mutableCollectionsStateFlow.update { DataState.Loading }
|
||||||
mutableSendDataStateFlow.update { DataState.Loading }
|
mutableSendDataStateFlow.update { DataState.Loading }
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
|||||||
/**
|
/**
|
||||||
* Map the API [Domains] model to the internal [DomainsData] model.
|
* Map the API [Domains] model to the internal [DomainsData] model.
|
||||||
*/
|
*/
|
||||||
fun Domains.toDomainsData(): DomainsData {
|
fun Domains?.toDomainsData(): DomainsData {
|
||||||
val globalEquivalentDomains = this
|
val globalEquivalentDomains = this
|
||||||
.globalEquivalentDomains
|
?.globalEquivalentDomains
|
||||||
?.map { it.toInternalModel() }
|
?.map { it.toInternalModel() }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
||||||
return DomainsData(
|
return DomainsData(
|
||||||
equivalentDomains = this.equivalentDomains.orEmpty(),
|
equivalentDomains = this?.equivalentDomains.orEmpty(),
|
||||||
globalEquivalentDomains = globalEquivalentDomains,
|
globalEquivalentDomains = globalEquivalentDomains,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
android:accessibilityEventTypes="typeWindowStateChanged"
|
||||||
android:accessibilityFeedbackType="feedbackGeneric"
|
android:accessibilityFeedbackType="feedbackGeneric"
|
||||||
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||||
android:canRetrieveWindowContent="true"
|
android:canRetrieveWindowContent="true"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class LogsManagerImpl(
|
|||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
Timber.plant(nonfatalErrorTree)
|
Timber.plant(nonfatalErrorTree)
|
||||||
} else {
|
} else if (Timber.forest().contains(nonfatalErrorTree)) {
|
||||||
Timber.uproot(nonfatalErrorTree)
|
Timber.uproot(nonfatalErrorTree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository.util
|
|||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -50,6 +51,74 @@ class StateFlowExtensionsTest {
|
|||||||
assertEquals(0, awaitItem())
|
assertEquals(0, awaitItem())
|
||||||
assertEquals(1, awaitItem())
|
assertEquals(1, awaitItem())
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
// Job is canceled, we should have no more subscribers
|
||||||
|
assertEquals(0, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `observeWhenSubscribedAndUnlocked should observe the given flow depending on the state of the source user and vault unlock flow`() =
|
||||||
|
runTest {
|
||||||
|
val userStateFlow = MutableStateFlow<UserStateJson?>(null)
|
||||||
|
val vaultUnlockFlow = MutableStateFlow<List<VaultUnlockData>>(emptyList())
|
||||||
|
val observerStateFlow = MutableStateFlow("")
|
||||||
|
val sourceMutableStateFlow = MutableStateFlow(Unit)
|
||||||
|
|
||||||
|
assertEquals(0, observerStateFlow.subscriptionCount.value)
|
||||||
|
sourceMutableStateFlow
|
||||||
|
.observeWhenSubscribedAndUnlocked(
|
||||||
|
userStateFlow = userStateFlow,
|
||||||
|
vaultUnlockFlow = vaultUnlockFlow,
|
||||||
|
observer = { observerStateFlow },
|
||||||
|
)
|
||||||
|
.launchIn(backgroundScope)
|
||||||
|
|
||||||
|
observerStateFlow.subscriptionCount.test {
|
||||||
|
// No subscriber to start
|
||||||
|
assertEquals(0, awaitItem())
|
||||||
|
|
||||||
|
userStateFlow.value = mockk<UserStateJson> {
|
||||||
|
every { activeUserId } returns "user_id_1234"
|
||||||
|
}
|
||||||
|
// Still none, since no one has subscribed to the testMutableStateFlow
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
vaultUnlockFlow.value = listOf(
|
||||||
|
VaultUnlockData(
|
||||||
|
userId = "user_id_1234",
|
||||||
|
status = VaultUnlockData.Status.UNLOCKED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Still none, since no one has subscribed to the testMutableStateFlow
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
val job = sourceMutableStateFlow.launchIn(backgroundScope)
|
||||||
|
// Now we subscribe to the observer flow since have a active user and a listener
|
||||||
|
assertEquals(1, awaitItem())
|
||||||
|
|
||||||
|
userStateFlow.value = mockk<UserStateJson> {
|
||||||
|
every { activeUserId } returns "user_id_4321"
|
||||||
|
}
|
||||||
|
// The user changed, so we clear the previous observer but then resubscribe
|
||||||
|
// with the new user ID
|
||||||
|
assertEquals(0, awaitItem())
|
||||||
|
assertEquals(1, awaitItem())
|
||||||
|
|
||||||
|
vaultUnlockFlow.value = listOf(
|
||||||
|
VaultUnlockData(
|
||||||
|
userId = "user_id_4321",
|
||||||
|
status = VaultUnlockData.Status.UNLOCKED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// The VaultUnlockData changed, so we clear the previous observer but then resubscribe
|
||||||
|
// with the new data
|
||||||
|
assertEquals(0, awaitItem())
|
||||||
|
assertEquals(1, awaitItem())
|
||||||
|
|
||||||
job.cancel()
|
job.cancel()
|
||||||
// Job is canceled, we should have no more subscribers
|
// Job is canceled, we should have no more subscribers
|
||||||
assertEquals(0, awaitItem())
|
assertEquals(0, awaitItem())
|
||||||
|
|||||||
@@ -248,10 +248,13 @@ class VaultDiskSourceTest {
|
|||||||
// We cannot compare the JSON strings directly because of formatting differences
|
// We cannot compare the JSON strings directly because of formatting differences
|
||||||
// So we split that off into its own assertion.
|
// So we split that off into its own assertion.
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DOMAINS_ENTITY.copy(domainsJson = ""),
|
DOMAINS_ENTITY.copy(domainsJson = null),
|
||||||
storedDomainsEntity.copy(domainsJson = ""),
|
storedDomainsEntity.copy(domainsJson = null),
|
||||||
|
)
|
||||||
|
assertJsonEquals(
|
||||||
|
requireNotNull(DOMAINS_ENTITY.domainsJson),
|
||||||
|
requireNotNull(storedDomainsEntity.domainsJson),
|
||||||
)
|
)
|
||||||
assertJsonEquals(DOMAINS_ENTITY.domainsJson, storedDomainsEntity.domainsJson)
|
|
||||||
|
|
||||||
// Verify the folders dao is updated
|
// Verify the folders dao is updated
|
||||||
assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders)
|
assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
|||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
|
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
|
|
||||||
class FakeDomainsDao : DomainsDao {
|
class FakeDomainsDao : DomainsDao {
|
||||||
var storedDomains: DomainsEntity? = null
|
var storedDomains: DomainsEntity? = null
|
||||||
@@ -18,9 +17,9 @@ class FakeDomainsDao : DomainsDao {
|
|||||||
deleteDomainsCalled = true
|
deleteDomainsCalled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDomains(userId: String): Flow<DomainsEntity> {
|
override fun getDomains(userId: String): Flow<DomainsEntity?> {
|
||||||
getDomainsCalled = true
|
getDomainsCalled = true
|
||||||
return mutableDomainsFlow.filterNotNull()
|
return mutableDomainsFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertDomains(domains: DomainsEntity) {
|
override suspend fun insertDomains(domains: DomainsEntity) {
|
||||||
|
|||||||
@@ -343,6 +343,12 @@ class VaultRepositoryTest {
|
|||||||
)
|
)
|
||||||
setVaultToUnlocked(userId = userId)
|
setVaultToUnlocked(userId = userId)
|
||||||
|
|
||||||
|
ciphersFlow.tryEmit(listOf(createMockCipher(number = 1)))
|
||||||
|
collectionsFlow.tryEmit(listOf(createMockCollection(number = 1)))
|
||||||
|
foldersFlow.tryEmit(listOf(createMockFolder(number = 1)))
|
||||||
|
sendsFlow.tryEmit(listOf(createMockSend(number = 1)))
|
||||||
|
domainsFlow.tryEmit(createMockDomains(number = 1))
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DataState.Loaded(listOf(createMockCipherView(number = 1))),
|
DataState.Loaded(listOf(createMockCipherView(number = 1))),
|
||||||
ciphersStateFlow.awaitItem(),
|
ciphersStateFlow.awaitItem(),
|
||||||
@@ -484,7 +490,8 @@ class VaultRepositoryTest {
|
|||||||
assertEquals(DataState.Loading, collectionsStateFlow.awaitItem())
|
assertEquals(DataState.Loading, collectionsStateFlow.awaitItem())
|
||||||
assertEquals(DataState.Loading, foldersStateFlow.awaitItem())
|
assertEquals(DataState.Loading, foldersStateFlow.awaitItem())
|
||||||
assertEquals(DataState.Loading, sendsStateFlow.awaitItem())
|
assertEquals(DataState.Loading, sendsStateFlow.awaitItem())
|
||||||
assertEquals(DataState.Loading, domainsStateFlow.awaitItem())
|
// We already have the domain data
|
||||||
|
domainsStateFlow.expectNoEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1804,6 +1811,9 @@ class VaultRepositoryTest {
|
|||||||
settingsDiskSource.getLastSyncTime(userId = userId)
|
settingsDiskSource.getLastSyncTime(userId = userId)
|
||||||
} returns clock.instant()
|
} returns clock.instant()
|
||||||
|
|
||||||
|
mutableVaultStateFlow.update {
|
||||||
|
listOf(VaultUnlockData(userId, VaultUnlockData.Status.UNLOCKED))
|
||||||
|
}
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
setupEmptyDecryptionResults()
|
setupEmptyDecryptionResults()
|
||||||
setupVaultDiskSourceFlows(
|
setupVaultDiskSourceFlows(
|
||||||
@@ -1960,6 +1970,7 @@ class VaultRepositoryTest {
|
|||||||
expectNoEvents()
|
expectNoEvents()
|
||||||
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
setVaultToUnlocked(userId = MOCK_USER_STATE.activeUserId)
|
||||||
|
|
||||||
|
sendsFlow.tryEmit(emptyList())
|
||||||
assertEquals(DataState.Loaded<SendView?>(null), awaitItem())
|
assertEquals(DataState.Loaded<SendView?>(null), awaitItem())
|
||||||
sendsFlow.tryEmit(listOf(createMockSend(number = sendId)))
|
sendsFlow.tryEmit(listOf(createMockSend(number = sendId)))
|
||||||
assertEquals(DataState.Loaded<SendView?>(sendView), awaitItem())
|
assertEquals(DataState.Loaded<SendView?>(sendView), awaitItem())
|
||||||
@@ -4544,6 +4555,14 @@ class VaultRepositoryTest {
|
|||||||
*/
|
*/
|
||||||
private fun setVaultToUnlocked(userId: String) {
|
private fun setVaultToUnlocked(userId: String) {
|
||||||
mutableUnlockedUserIdsStateFlow.update { it + userId }
|
mutableUnlockedUserIdsStateFlow.update { it + userId }
|
||||||
|
mutableVaultStateFlow.tryEmit(
|
||||||
|
listOf(
|
||||||
|
VaultUnlockData(
|
||||||
|
userId,
|
||||||
|
VaultUnlockData.Status.UNLOCKED,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -237,6 +237,17 @@ platform :android do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Publish Play Store Beta bundle to Google Play Store"
|
||||||
|
lane :publishProdToPlayStore do
|
||||||
|
upload_to_play_store(
|
||||||
|
package_name: "com.x8bit.bitwarden",
|
||||||
|
track: "internal",
|
||||||
|
release_status: "completed",
|
||||||
|
rollout: "1",
|
||||||
|
aab: "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
desc "Generate release notes"
|
desc "Generate release notes"
|
||||||
lane :generateReleaseNotes do |options|
|
lane :generateReleaseNotes do |options|
|
||||||
branchName = `git rev-parse --abbrev-ref HEAD`.chomp()
|
branchName = `git rev-parse --abbrev-ref HEAD`.chomp()
|
||||||
|
|||||||
Reference in New Issue
Block a user