mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Compare commits
120 Commits
v2025.6.1-
...
v2025.7.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a721744a6b | ||
|
|
37af6a1773 | ||
|
|
557c5b46a5 | ||
|
|
390ef34398 | ||
|
|
d2f7d52132 | ||
|
|
0feac46711 | ||
|
|
bc50c0d873 | ||
|
|
fb3b9c9ea7 | ||
|
|
9a81e18cb4 | ||
|
|
f9914e5b46 | ||
|
|
e193661f5f | ||
|
|
532fcbb40e | ||
|
|
187d50faa2 | ||
|
|
8f5376c2de | ||
|
|
56192a7e8b | ||
|
|
70350746ce | ||
|
|
febfc82a53 | ||
|
|
5f5c71979f | ||
|
|
ba49a3e91f | ||
|
|
965ab67e58 | ||
|
|
2932ed831b | ||
|
|
2ff3f3e23d | ||
|
|
eb5893dde4 | ||
|
|
1165e7002b | ||
|
|
5fa7239130 | ||
|
|
fd9bdfa228 | ||
|
|
7db8f040e4 | ||
|
|
790331e058 | ||
|
|
d0640b7e20 | ||
|
|
5429e27228 | ||
|
|
917aaac3a6 | ||
|
|
0b7209b3c9 | ||
|
|
a7b3201015 | ||
|
|
348e14e52d | ||
|
|
ef9dda5159 | ||
|
|
b0309e876e | ||
|
|
59a49355fd | ||
|
|
901184db45 | ||
|
|
a2507c317d | ||
|
|
f608852dc7 | ||
|
|
e44d63229c | ||
|
|
f7b876f204 | ||
|
|
1268afaef8 | ||
|
|
3f1c1dec17 | ||
|
|
5eea55f173 | ||
|
|
1a8cf4055a | ||
|
|
defdf8eb58 | ||
|
|
9940c8cf9e | ||
|
|
e1058f5021 | ||
|
|
986cd2ee30 | ||
|
|
eae870cb3a | ||
|
|
79493a55bd | ||
|
|
18bafaba8a | ||
|
|
896be911a4 | ||
|
|
85a86106f6 | ||
|
|
edb7996c28 | ||
|
|
a806109380 | ||
|
|
4f5c28e248 | ||
|
|
b22f06cbf9 | ||
|
|
1070c9d46e | ||
|
|
b1dc894fe8 | ||
|
|
c76945161a | ||
|
|
789cd80eba | ||
|
|
9482890102 | ||
|
|
ed2d6ca585 | ||
|
|
d279f6acae | ||
|
|
6ebcab7b86 | ||
|
|
3ee74d3ec5 | ||
|
|
288efb3611 | ||
|
|
bbdf8552c9 | ||
|
|
44ef598df3 | ||
|
|
73a8e241d4 | ||
|
|
4d6260ea02 | ||
|
|
569bb4f110 | ||
|
|
ffc71371a9 | ||
|
|
8d0b23d166 | ||
|
|
5f525d9d95 | ||
|
|
b94d59ba6b | ||
|
|
4ff1a9ba94 | ||
|
|
9c1673f603 | ||
|
|
ddc099f727 | ||
|
|
fbfcfcd683 | ||
|
|
1234898786 | ||
|
|
182e6475c0 | ||
|
|
f27590a4d6 | ||
|
|
807c76f8ec | ||
|
|
3877c4bd64 | ||
|
|
8c88fd9d53 | ||
|
|
b92493611e | ||
|
|
9235f92206 | ||
|
|
a3610c22dd | ||
|
|
1e4fc31ed4 | ||
|
|
ac1a9a2dc0 | ||
|
|
fe0e6bc67b | ||
|
|
419e5ca918 | ||
|
|
be1a6e2097 | ||
|
|
4fe989ce68 | ||
|
|
8be7410302 | ||
|
|
16225f0d68 | ||
|
|
08679a8973 | ||
|
|
4d3e782b69 | ||
|
|
4d8fe722d1 | ||
|
|
c5600c1d84 | ||
|
|
9816321d93 | ||
|
|
56e8acf81f | ||
|
|
08b07a0050 | ||
|
|
25d7c1e72c | ||
|
|
e311a4f618 | ||
|
|
0eea6b07a3 | ||
|
|
c52e769327 | ||
|
|
292a28d155 | ||
|
|
6c41c358ac | ||
|
|
e7cf5a7efa | ||
|
|
f64364c1b8 | ||
|
|
d42b8ecd2d | ||
|
|
a6f7b1e176 | ||
|
|
d56b9fc0ff | ||
|
|
f290ae411b | ||
|
|
508566f06f | ||
|
|
95f146fb3e |
11
.github/workflows/build-authenticator.yml
vendored
11
.github/workflows/build-authenticator.yml
vendored
@@ -39,6 +39,15 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
@@ -113,7 +122,7 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -40,6 +40,15 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
@@ -121,7 +130,7 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
@@ -420,7 +429,7 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
|
||||
2
.github/workflows/crowdin-push.yml
vendored
2
.github/workflows/crowdin-push.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
|
||||
4
.github/workflows/publish-github-release.yml
vendored
4
.github/workflows/publish-github-release.yml
vendored
@@ -3,10 +3,12 @@ name: Publish GitHub Release as newest
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
stub:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Stub
|
||||
steps:
|
||||
- name: Stub
|
||||
run: echo "This is a stub job to trigger the workflow."
|
||||
run: echo "This is a stub job to trigger the workflow."
|
||||
|
||||
@@ -4,6 +4,8 @@ name: Publish
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
20
Gemfile.lock
20
Gemfile.lock
@@ -10,22 +10,22 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1113.0)
|
||||
aws-sdk-core (3.225.1)
|
||||
aws-partitions (1.1125.0)
|
||||
aws-sdk-core (3.226.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.104.0)
|
||||
aws-sdk-kms (1.106.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.189.0)
|
||||
aws-sdk-s3 (1.192.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.0)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
@@ -58,10 +58,10 @@ GEM
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
@@ -71,7 +71,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.227.2)
|
||||
fastlane (2.228.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -166,7 +166,7 @@ GEM
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
jwt (2.10.1)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
@@ -175,7 +175,7 @@ GEM
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.2)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
|
||||
10
README.md
10
README.md
@@ -52,6 +52,16 @@
|
||||
|
||||
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
|
||||
|
||||
4. Setup JDK `Version` `17`:
|
||||
|
||||
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
|
||||
- Hit the selected Gradle JDK next to `Gradle JDK:`.
|
||||
- Select a `17.x` version or hit `Download JDK...` if not present.
|
||||
- Select `Version` `17`.
|
||||
- Select your preferred `Vendor`.
|
||||
- Hit `Download`.
|
||||
- Hit `Apply`.
|
||||
|
||||
## Theme
|
||||
|
||||
### Icons & Illustrations
|
||||
|
||||
@@ -37,6 +37,6 @@ android {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.androidx.room)
|
||||
// Crashlytics is enabled for all builds initially but removed for FDroid builds in gradle and
|
||||
// standardDebug builds in the merged manifest.
|
||||
alias(libs.plugins.crashlytics)
|
||||
@@ -46,6 +47,10 @@ android {
|
||||
namespace = "com.x8bit.bitwarden"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
room {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.x8bit.bitwarden"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
@@ -55,11 +60,6 @@ android {
|
||||
|
||||
setProperty("archivesBaseName", "com.x8bit.bitwarden")
|
||||
|
||||
ksp {
|
||||
// The location in which the generated Room Database Schemas will be stored in the repo.
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField(
|
||||
@@ -99,6 +99,7 @@ android {
|
||||
applicationIdSuffix = ".beta"
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
matchingFallbacks += listOf("release")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
@@ -111,6 +112,7 @@ android {
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
@@ -193,7 +195,7 @@ android {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +298,7 @@ tasks {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "2g"
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors()
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
|
||||
android.sourceSets["main"].res.srcDirs("src/test/res")
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "4c6ad1f5268d7e8add7407201788aa2e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ciphers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `has_totp` INTEGER NOT NULL DEFAULT 1, `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": "hasTotp",
|
||||
"columnName": "has_totp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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, `manage` INTEGER, 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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isReadOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canManage",
|
||||
"columnName": "manage",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '4c6ad1f5268d7e8add7407201788aa2e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
21
app/src/beta/AndroidManifest.xml
Normal file
21
app/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:ignore="MissingApplicationIcon">
|
||||
<activity
|
||||
android:name=".MainActivity">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -7,6 +7,20 @@
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
tools:ignore="IntentFilterExportedReceiver">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
<data android:scheme="https" />
|
||||
<data android:host="*.bitwarden.com" />
|
||||
<data android:host="*.bitwarden.eu" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
@@ -330,11 +329,19 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent>
|
||||
<!-- To Query Privileged Apps -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
<!-- To Query Chrome Beta: -->
|
||||
<package android:name="com.chrome.beta" />
|
||||
|
||||
<!-- To Query Chrome Stable: -->
|
||||
<package android:name="com.android.chrome" />
|
||||
|
||||
<!-- To Query Brave Stable: -->
|
||||
<package android:name="com.brave.browser" />
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -779,6 +779,42 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "cz.seznam.sbrowser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.opera.mini.native",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.opera.mini.native.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -55,19 +55,13 @@ class AutofillTotpCopyViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
// Try and find the matching cipher.
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.mapNotNull { it.data }
|
||||
.first()
|
||||
.find { it.id == cipherId }
|
||||
?.let { cipherView ->
|
||||
sendEvent(
|
||||
AutofillTotpCopyEvent.CompleteAutofill(
|
||||
cipherView = cipherView,
|
||||
),
|
||||
)
|
||||
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
|
||||
GetCipherResult.CipherNotFound -> finishActivity()
|
||||
is GetCipherResult.Failure -> finishActivity()
|
||||
is GetCipherResult.Success -> {
|
||||
sendEvent(AutofillTotpCopyEvent.CompleteAutofill(result.cipherView))
|
||||
}
|
||||
?: finishActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
|
||||
@@ -66,7 +67,7 @@ class AuthRequestNotificationManagerImpl(
|
||||
?.let { context.getString(R.string.confim_log_in_attemp_for_x, it) }
|
||||
?: context.getString(R.string.confirm_log_in),
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setSmallIcon(BitwardenDrawable.ic_notification)
|
||||
.setColor(Color.White.value.toInt())
|
||||
.setAutoCancel(true)
|
||||
.setTimeoutAfter(NOTIFICATION_DEFAULT_TIMEOUT_MILLIS)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.R
|
||||
@@ -27,15 +26,15 @@ import timber.log.Timber
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class UserLogoutManagerImpl(
|
||||
private val context: Context,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val generatorDiskSource: GeneratorDiskSource,
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
private val pushDiskSource: PushDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val toastManager: ToastManager,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : UserLogoutManager {
|
||||
private val scope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
@@ -117,7 +116,7 @@ class UserLogoutManagerImpl(
|
||||
}
|
||||
|
||||
private fun showToast(@StringRes message: Int) {
|
||||
mainScope.launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
|
||||
mainScope.launch { toastManager.show(messageId = message) }
|
||||
}
|
||||
|
||||
private fun switchUserIfAvailable(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
@@ -107,23 +108,23 @@ object AuthManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserLogoutManager(
|
||||
@ApplicationContext context: Context,
|
||||
authDiskSource: AuthDiskSource,
|
||||
generatorDiskSource: GeneratorDiskSource,
|
||||
passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
pushDiskSource: PushDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
toastManager: ToastManager,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): UserLogoutManager =
|
||||
UserLogoutManagerImpl(
|
||||
context = context,
|
||||
authDiskSource = authDiskSource,
|
||||
generatorDiskSource = generatorDiskSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
pushDiskSource = pushDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
toastManager = toastManager,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.PowerManager
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
|
||||
@@ -89,6 +90,7 @@ object AccessibilityModule {
|
||||
accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
launcherPackageNameManager: LauncherPackageNameManager,
|
||||
powerManager: PowerManager,
|
||||
toastManager: ToastManager,
|
||||
): BitwardenAccessibilityProcessor =
|
||||
BitwardenAccessibilityProcessorImpl(
|
||||
context = context,
|
||||
@@ -96,6 +98,7 @@ object AccessibilityModule {
|
||||
accessibilityAutofillManager = accessibilityAutofillManager,
|
||||
launcherPackageNameManager = launcherPackageNameManager,
|
||||
powerManager = powerManager,
|
||||
toastManager = toastManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.PowerManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.Toast
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||
@@ -26,6 +27,7 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
private val accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
private val launcherPackageNameManager: LauncherPackageNameManager,
|
||||
private val powerManager: PowerManager,
|
||||
private val toastManager: ToastManager,
|
||||
) : BitwardenAccessibilityProcessor {
|
||||
override fun processAccessibilityEvent(
|
||||
event: AccessibilityEvent,
|
||||
@@ -110,13 +112,10 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
R.string.autofill_tile_uri_not_found,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
toastManager.show(
|
||||
messageId = R.string.autofill_tile_uri_not_found,
|
||||
duration = Toast.LENGTH_LONG,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.createTotpCopyIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.fillableAutofillIds
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The default implementation for [FillResponseBuilder]. This is a component for compiling fulfilled
|
||||
@@ -22,12 +23,9 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
saveInfo: SaveInfo?,
|
||||
): FillResponse? =
|
||||
if (filledData.fillableAutofillIds.isNotEmpty()) {
|
||||
Timber.w("Autofill request constructing FillResponse")
|
||||
val fillResponseBuilder = FillResponse.Builder()
|
||||
|
||||
saveInfo
|
||||
?.let { nonNullSaveInfo ->
|
||||
fillResponseBuilder.setSaveInfo(nonNullSaveInfo)
|
||||
}
|
||||
saveInfo?.let { nonNullSaveInfo -> fillResponseBuilder.setSaveInfo(nonNullSaveInfo) }
|
||||
|
||||
filledData
|
||||
.filledPartitions
|
||||
@@ -52,12 +50,7 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
|
||||
fillResponseBuilder
|
||||
// Add the Vault Item
|
||||
.addDataset(
|
||||
filledData
|
||||
.buildVaultItemDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
),
|
||||
)
|
||||
.addDataset(filledData.buildVaultItemDataset(autofillAppInfo = autofillAppInfo))
|
||||
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
||||
.build()
|
||||
} else {
|
||||
@@ -66,6 +59,7 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
// with a presentation view. Neither of these make sense in the case where we have no
|
||||
// views to fill. What we are supposed to do when we cannot fulfill a request is
|
||||
// replace [FillResponse] with null in order to avoid this crash.
|
||||
Timber.w("Autofill request has no fillable ids")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildFilledItemOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The maximum amount of filled partitions the user will see. Viewing the rest will require opening
|
||||
@@ -34,6 +35,7 @@ class FilledDataBuilderImpl(
|
||||
private val autofillCipherProvider: AutofillCipherProvider,
|
||||
) : FilledDataBuilder {
|
||||
override suspend fun build(autofillRequest: AutofillRequest.Fillable): FilledData {
|
||||
Timber.d("Autofill request constructing FilledData")
|
||||
val isVaultLocked = autofillCipherProvider.isVaultLocked()
|
||||
|
||||
// Subtract one to make sure there is space for the vault item.
|
||||
@@ -84,7 +86,7 @@ class FilledDataBuilderImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The primary implementation of [SaveInfoBuilder].This is used for converting autofill data into
|
||||
@@ -18,6 +19,7 @@ class SaveInfoBuilderImpl(
|
||||
fillRequest: FillRequest,
|
||||
packageName: String?,
|
||||
): SaveInfo? {
|
||||
Timber.d("Autofill request constructing SaveInfo -- ${fillRequest.id}")
|
||||
// Make sure that the save prompt is possible.
|
||||
val canPerformSaveRequest = autofillPartition.canPerformSaveRequest
|
||||
if (settingsRepository.isAutofillSavePromptDisabled || !canPerformSaveRequest) return null
|
||||
@@ -26,6 +28,7 @@ class SaveInfoBuilderImpl(
|
||||
// in Compat mode since they show as masked values.
|
||||
val isInCompatMode = (fillRequest.flags or
|
||||
FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) == fillRequest.flags
|
||||
Timber.d("Autofill request isInCompatMode=$isInCompatMode -- ${fillRequest.id}")
|
||||
|
||||
// If login and compat mode, the password might be obfuscated,
|
||||
// in which case we should skip the save request.
|
||||
|
||||
@@ -8,9 +8,9 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -29,9 +29,9 @@ object ActivityAutofillModule {
|
||||
@ActivityScoped
|
||||
@ActivityScopedManager
|
||||
@Provides
|
||||
fun provideActivityScopedChromeThirdPartyAutofillManager(
|
||||
fun provideActivityScopedBrowserThirdPartyAutofillManager(
|
||||
activity: Activity,
|
||||
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
|
||||
): BrowserThirdPartyAutofillManager = BrowserThirdPartyAutofillManagerImpl(
|
||||
context = activity.baseContext,
|
||||
)
|
||||
|
||||
@@ -39,19 +39,19 @@ object ActivityAutofillModule {
|
||||
@Provides
|
||||
fun provideAutofillActivityManager(
|
||||
@ActivityScopedManager autofillManager: AutofillManager,
|
||||
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
@ActivityScopedManager browserThirdPartyAutofillManager: BrowserThirdPartyAutofillManager,
|
||||
appStateManager: AppStateManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
): AutofillActivityManager =
|
||||
AutofillActivityManagerImpl(
|
||||
autofillManager = autofillManager,
|
||||
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
|
||||
browserThirdPartyAutofillManager = browserThirdPartyAutofillManager,
|
||||
appStateManager = appStateManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
lifecycleScope = lifecycleScope,
|
||||
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,8 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
@@ -59,10 +59,10 @@ object AutofillModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesChromeAutofillEnabledManager(
|
||||
fun providesBrowserAutofillEnabledManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): ChromeThirdPartyAutofillEnabledManager =
|
||||
ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
): BrowserThirdPartyAutofillEnabledManager =
|
||||
BrowserThirdPartyAutofillEnabledManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@@ -93,7 +93,6 @@ object AutofillModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillTotpManager(
|
||||
@ApplicationContext context: Context,
|
||||
clock: Clock,
|
||||
clipboardManager: BitwardenClipboardManager,
|
||||
authRepository: AuthRepository,
|
||||
@@ -101,7 +100,6 @@ object AutofillModule {
|
||||
vaultRepository: VaultRepository,
|
||||
): AutofillTotpManager =
|
||||
AutofillTotpManagerImpl(
|
||||
context = context,
|
||||
clock = clock,
|
||||
clipboardManager = clipboardManager,
|
||||
authRepository = authRepository,
|
||||
|
||||
@@ -2,9 +2,9 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -14,21 +14,22 @@ import kotlinx.coroutines.flow.onEach
|
||||
*/
|
||||
class AutofillActivityManagerImpl(
|
||||
private val autofillManager: AutofillManager,
|
||||
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
private val browserThirdPartyAutofillManager: BrowserThirdPartyAutofillManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
appStateManager: AppStateManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
) : AutofillActivityManager {
|
||||
private val isAutofillEnabledAndSupported: Boolean
|
||||
get() = autofillManager.isEnabled &&
|
||||
autofillManager.hasEnabledAutofillServices() &&
|
||||
autofillManager.isAutofillSupported
|
||||
|
||||
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
get() = ChromeThirdPartyAutofillStatus(
|
||||
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
|
||||
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
|
||||
private val browserAutofillStatus: BrowserThirdPartyAutofillStatus
|
||||
get() = BrowserThirdPartyAutofillStatus(
|
||||
braveStableStatusData = browserThirdPartyAutofillManager.stableBraveAutofillStatus,
|
||||
chromeStableStatusData = browserThirdPartyAutofillManager.stableChromeAutofillStatus,
|
||||
chromeBetaChannelStatusData = browserThirdPartyAutofillManager.betaChromeAutofillStatus,
|
||||
)
|
||||
|
||||
init {
|
||||
@@ -36,8 +37,8 @@ class AutofillActivityManagerImpl(
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
|
||||
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
|
||||
chromeAutofillStatus
|
||||
browserThirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus =
|
||||
browserAutofillStatus
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
@@ -16,7 +14,6 @@ import java.time.Clock
|
||||
* Default implementation of the [AutofillTotpManager].
|
||||
*/
|
||||
class AutofillTotpManagerImpl(
|
||||
private val context: Context,
|
||||
private val clock: Clock,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val authRepository: AuthRepository,
|
||||
@@ -39,13 +36,6 @@ class AutofillTotpManagerImpl(
|
||||
text = totpResult.code,
|
||||
toastDescriptorOverride = R.string.verification_code_totp.asText(),
|
||||
)
|
||||
Toast
|
||||
.makeText(
|
||||
context.applicationContext,
|
||||
R.string.verification_code_totp,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manager which provides whether specific browser versions have third party autofill available and
|
||||
* enabled.
|
||||
*/
|
||||
interface BrowserThirdPartyAutofillEnabledManager {
|
||||
/**
|
||||
* Combined status for all concerned browser versions.
|
||||
*/
|
||||
var browserThirdPartyAutofillStatus: BrowserThirdPartyAutofillStatus
|
||||
|
||||
/**
|
||||
* An observable [StateFlow] of the combined third party autofill status of all concerned
|
||||
* browser versions.
|
||||
*/
|
||||
val browserThirdPartyAutofillStatusFlow: Flow<BrowserThirdPartyAutofillStatus>
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Default implementation of [BrowserThirdPartyAutofillEnabledManager].
|
||||
*/
|
||||
class BrowserThirdPartyAutofillEnabledManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : BrowserThirdPartyAutofillEnabledManager {
|
||||
override var browserThirdPartyAutofillStatus: BrowserThirdPartyAutofillStatus = DEFAULT_STATUS
|
||||
set(value) {
|
||||
field = value
|
||||
mutableBrowserThirdPartyAutofillStatusStateFlow.update {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableBrowserThirdPartyAutofillStatusStateFlow = MutableStateFlow(
|
||||
value = browserThirdPartyAutofillStatus,
|
||||
)
|
||||
|
||||
override val browserThirdPartyAutofillStatusFlow: Flow<BrowserThirdPartyAutofillStatus>
|
||||
get() = mutableBrowserThirdPartyAutofillStatusStateFlow
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
|
||||
) { data, enabled ->
|
||||
if (enabled) {
|
||||
data
|
||||
} else {
|
||||
DEFAULT_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATUS = BrowserThirdPartyAutofillStatus(
|
||||
braveStableStatusData = BrowserThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
chromeStableStatusData = BrowserThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
chromeBetaChannelStatusData = BrowserThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* Manager class used to determine if a device has installed versions of a browser (either the
|
||||
* stable release or beta channel) which support and require opt in to third party autofill.
|
||||
*/
|
||||
interface BrowserThirdPartyAutofillManager {
|
||||
|
||||
/**
|
||||
* The data representing the status of the stable Brave version
|
||||
*/
|
||||
val stableBraveAutofillStatus: BrowserThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* The data representing the status of the stable Chrome version
|
||||
*/
|
||||
val stableChromeAutofillStatus: BrowserThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* The data representing the status of the beta Chrome version
|
||||
*/
|
||||
val betaChromeAutofillStatus: BrowserThirdPartyAutoFillData
|
||||
}
|
||||
@@ -1,35 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
|
||||
|
||||
private const val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
|
||||
private const val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
|
||||
private const val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"
|
||||
|
||||
/**
|
||||
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
|
||||
* [ContentResolver] to determine if the installed Chrome packages support and enable
|
||||
* third party autofill services.
|
||||
* Default implementation of the [BrowserThirdPartyAutofillManager] which uses a [ContentResolver]
|
||||
* to determine if the installed browser packages support and enable third party autofill services.
|
||||
*
|
||||
* Based off of [this blog post](https://android-developers.googleblog.com/2025/02/chrome-3p-autofill-services-update.html)
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ChromeThirdPartyAutofillManagerImpl(
|
||||
class BrowserThirdPartyAutofillManagerImpl(
|
||||
private val context: Context,
|
||||
) : ChromeThirdPartyAutofillManager {
|
||||
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
|
||||
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
|
||||
) : BrowserThirdPartyAutofillManager {
|
||||
override val stableBraveAutofillStatus: BrowserThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(BrowserPackage.BRAVE_RELEASE)
|
||||
override val stableChromeAutofillStatus: BrowserThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(BrowserPackage.CHROME_STABLE)
|
||||
override val betaChromeAutofillStatus: BrowserThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(BrowserPackage.CHROME_BETA)
|
||||
|
||||
private fun getThirdPartyAutoFillStatusForChannel(
|
||||
releaseChannel: ChromeReleaseChannel,
|
||||
): ChromeThirdPartyAutoFillData {
|
||||
releaseChannel: BrowserPackage,
|
||||
): BrowserThirdPartyAutoFillData {
|
||||
val uri = Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
|
||||
@@ -54,7 +55,7 @@ class ChromeThirdPartyAutofillManagerImpl(
|
||||
true
|
||||
}
|
||||
?: false
|
||||
return ChromeThirdPartyAutoFillData(
|
||||
return BrowserThirdPartyAutoFillData(
|
||||
isAvailable = isThirdPartyAvailable,
|
||||
isThirdPartyEnabled = thirdPartyEnabled,
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manager which provides whether specific Chrome versions have third party autofill available and
|
||||
* enabled.
|
||||
*/
|
||||
interface ChromeThirdPartyAutofillEnabledManager {
|
||||
/**
|
||||
* Combined status for all concerned Chrome versions.
|
||||
*/
|
||||
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
|
||||
/**
|
||||
* An observable [StateFlow] of the combined third party autofill status of all concerned
|
||||
* chrome versions.
|
||||
*/
|
||||
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
|
||||
*/
|
||||
class ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : ChromeThirdPartyAutofillEnabledManager {
|
||||
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
|
||||
set(value) {
|
||||
field = value
|
||||
mutableChromeThirdPartyAutofillStatusStateFlow.update {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
|
||||
chromeThirdPartyAutofillStatus,
|
||||
)
|
||||
|
||||
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
|
||||
get() = mutableChromeThirdPartyAutofillStatusStateFlow
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
|
||||
) { data, enabled ->
|
||||
if (enabled) {
|
||||
data
|
||||
} else {
|
||||
DEFAULT_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
|
||||
ChromeThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
ChromeThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
)
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* Manager class used to determine if a device has installed versions of Chrome (either the
|
||||
* stable release or beta channel) which support and require opt in to third party autofill.
|
||||
*/
|
||||
interface ChromeThirdPartyAutofillManager {
|
||||
|
||||
/**
|
||||
* The data representing the status of the stable chrome version
|
||||
*/
|
||||
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
|
||||
/**
|
||||
* The data representing the status of the beta chrome version
|
||||
*/
|
||||
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
|
||||
/**
|
||||
* The app information required for the autofill service.
|
||||
@@ -9,4 +10,10 @@ data class AutofillAppInfo(
|
||||
val context: Context,
|
||||
val packageName: String,
|
||||
val sdkInt: Int,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Returns true if the current [sdkInt] version is at least the provided [version].
|
||||
*/
|
||||
@ChecksSdkIntAtLeast(parameter = 0)
|
||||
fun isVersionAtLeast(version: Int): Boolean = sdkInt >= version
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.core.Uuid
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
|
||||
/**
|
||||
* A paired down model of the CipherView for use within the autofill feature.
|
||||
@@ -48,7 +48,7 @@ sealed class AutofillCipher {
|
||||
val number: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = R.drawable.ic_payment_card
|
||||
@DrawableRes get() = BitwardenDrawable.ic_payment_card
|
||||
|
||||
override val isTotpEnabled: Boolean
|
||||
get() = false
|
||||
@@ -67,6 +67,6 @@ sealed class AutofillCipher {
|
||||
val username: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = R.drawable.ic_globe
|
||||
@DrawableRes get() = BitwardenDrawable.ic_globe
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.browser
|
||||
|
||||
private const val BRAVE_CHANNEL_PACKAGE = "com.brave.browser"
|
||||
private const val CHROME_BETA_CHANNEL_PACKAGE = "com.chrome.beta"
|
||||
private const val CHROME_RELEASE_CHANNEL_PACKAGE = "com.android.chrome"
|
||||
|
||||
/**
|
||||
* Enumerated values of each browser that supports third party autofill checks.
|
||||
*
|
||||
* @property packageName the package name of the release channel for the browser version.
|
||||
*/
|
||||
enum class BrowserPackage(val packageName: String) {
|
||||
BRAVE_RELEASE(BRAVE_CHANNEL_PACKAGE),
|
||||
CHROME_STABLE(CHROME_RELEASE_CHANNEL_PACKAGE),
|
||||
CHROME_BETA(CHROME_BETA_CHANNEL_PACKAGE),
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.browser
|
||||
|
||||
/**
|
||||
* Relevant data relating to the third party autofill status of a specific browser app.
|
||||
*/
|
||||
data class BrowserThirdPartyAutoFillData(
|
||||
val isAvailable: Boolean,
|
||||
val isThirdPartyEnabled: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* The overall status for all relevant browsers.
|
||||
*/
|
||||
data class BrowserThirdPartyAutofillStatus(
|
||||
val braveStableStatusData: BrowserThirdPartyAutoFillData,
|
||||
val chromeStableStatusData: BrowserThirdPartyAutoFillData,
|
||||
val chromeBetaChannelStatusData: BrowserThirdPartyAutoFillData,
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.chrome
|
||||
|
||||
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
|
||||
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
|
||||
|
||||
/**
|
||||
* Enumerated values of each version of Chrome supported for third party autofill checks.
|
||||
*
|
||||
* @property packageName the package name of the release channel for the Chrome version.
|
||||
*/
|
||||
enum class ChromeReleaseChannel(val packageName: String) {
|
||||
STABLE(CHROME_CHANNEL_PACKAGE),
|
||||
BETA(BETA_CHANNEL_PACKAGE),
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model.chrome
|
||||
|
||||
/**
|
||||
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
|
||||
*/
|
||||
data class ChromeThirdPartyAutoFillData(
|
||||
val isAvailable: Boolean,
|
||||
val isThirdPartyEnabled: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* The overall status for all relevant release channels of Chrome.
|
||||
*/
|
||||
data class ChromeThirdPartyAutofillStatus(
|
||||
val stableStatusData: ChromeThirdPartyAutoFillData,
|
||||
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
|
||||
)
|
||||
@@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.autofill.util.getMaxInlineSuggestionsCount
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.util.website
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A list of URIs that should never be autofilled.
|
||||
@@ -23,6 +24,8 @@ private val BLOCK_LISTED_URIS: List<String> = listOf(
|
||||
"androidapp://android",
|
||||
"androidapp://com.android.settings",
|
||||
"androidapp://com.x8bit.bitwarden",
|
||||
"androidapp://com.x8bit.bitwarden.beta",
|
||||
"androidapp://com.x8bit.bitwarden.dev",
|
||||
"androidapp://com.oneplus.applocker",
|
||||
)
|
||||
|
||||
@@ -70,6 +73,7 @@ class AutofillParserImpl(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
fillRequest: FillRequest?,
|
||||
): AutofillRequest {
|
||||
Timber.d("Parsing AssistStructure -- ${fillRequest?.id}")
|
||||
// Parse the `assistStructure` into internal models.
|
||||
val traversalDataList = assistStructure.traverse()
|
||||
// Take only the autofill views from the node that currently has focus.
|
||||
@@ -131,6 +135,7 @@ class AutofillParserImpl(
|
||||
|
||||
// Get inline information if available
|
||||
val isInlineAutofillEnabled = settingsRepository.isInlineAutofillEnabled
|
||||
Timber.e("Autofill request isInlineEnabled=$isInlineAutofillEnabled -- ${fillRequest?.id}")
|
||||
val maxInlineSuggestionsCount = fillRequest.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = isInlineAutofillEnabled,
|
||||
|
||||
@@ -53,8 +53,12 @@ class AutofillProcessorImpl(
|
||||
fillCallback: FillCallback,
|
||||
request: FillRequest,
|
||||
) {
|
||||
Timber.d("Begin processing Autofill fill request -- ${request.id}")
|
||||
// Set the listener so that any long running work is cancelled when it is no longer needed.
|
||||
cancellationSignal.setOnCancelListener { job.cancel() }
|
||||
cancellationSignal.setOnCancelListener {
|
||||
Timber.d("Autofill job cancelled")
|
||||
job.cancel()
|
||||
}
|
||||
// Process the OS data and handle invoking the callback with the result.
|
||||
job.cancel()
|
||||
job = scope.launch {
|
||||
@@ -122,6 +126,7 @@ class AutofillProcessorImpl(
|
||||
)
|
||||
when (autofillRequest) {
|
||||
is AutofillRequest.Fillable -> {
|
||||
Timber.d("Autofill request is Fillable -- ${fillRequest.id}")
|
||||
// Fulfill the [autofillRequest].
|
||||
val filledData = filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
@@ -141,6 +146,7 @@ class AutofillProcessorImpl(
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
Timber.d("Autofill request success: Fillable -- ${fillRequest.id}")
|
||||
fillCallback.onSuccess(response)
|
||||
} catch (e: RuntimeException) {
|
||||
// This is to catch any TransactionTooLargeExceptions that could occur here.
|
||||
@@ -153,6 +159,7 @@ class AutofillProcessorImpl(
|
||||
// If we are unable to fulfill the request, we should invoke the callback
|
||||
// with null. This effectively disables autofill for this view set and
|
||||
// allows the [AutofillService] to be unbound.
|
||||
Timber.d("Autofill request success: Unfillable -- ${fillRequest.id}")
|
||||
fillCallback.onSuccess(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.autofill.FillRequest
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
@@ -9,12 +8,11 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
/**
|
||||
* Extract the list of [InlinePresentationSpec]s. If it fails, return an empty list.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FillRequest?.getInlinePresentationSpecs(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
isInlineAutofillEnabled: Boolean,
|
||||
): List<InlinePresentationSpec>? =
|
||||
if (autofillAppInfo.sdkInt < Build.VERSION_CODES.R) {
|
||||
if (!autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
|
||||
// When SDK version is bellow 30, InlinePresentationSpec is not available and null
|
||||
// must be returned.
|
||||
null
|
||||
@@ -28,14 +26,13 @@ fun FillRequest?.getInlinePresentationSpecs(
|
||||
* Extract the max inline suggestions count. If the OS is below Android R, this will always
|
||||
* return 0.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FillRequest?.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
isInlineAutofillEnabled: Boolean,
|
||||
): Int =
|
||||
if (this != null &&
|
||||
isInlineAutofillEnabled &&
|
||||
autofillAppInfo.sdkInt >= Build.VERSION_CODES.R
|
||||
autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)
|
||||
) {
|
||||
inlineSuggestionsRequest?.maxSuggestionCount ?: 0
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
@@ -28,7 +27,6 @@ val FilledData.fillableAutofillIds: List<AutofillId>
|
||||
/**
|
||||
* Builds a [Dataset] for the Vault item.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FilledData.buildVaultItemDataset(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
): Dataset {
|
||||
@@ -70,7 +68,7 @@ fun FilledData.buildVaultItemDataset(
|
||||
return Dataset.Builder()
|
||||
.setAuthentication(pendingIntent.intentSender)
|
||||
.apply {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
addVaultItemDataPostTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
@@ -132,8 +130,7 @@ private fun Dataset.Builder.addVaultItemDataPostTiramisu(
|
||||
/**
|
||||
* Adds the Vault data to the given [Dataset.Builder] for pre-Tiramisu versions.
|
||||
*/
|
||||
@Suppress("DEPRECATION", "LongParameterList")
|
||||
@SuppressLint("NewApi")
|
||||
@Suppress("LongParameterList")
|
||||
private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
pendingIntent: PendingIntent,
|
||||
@@ -142,7 +139,7 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
inlinePresentationSpec: InlinePresentationSpec?,
|
||||
isLocked: Boolean,
|
||||
): Dataset.Builder {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
|
||||
inlinePresentationSpec
|
||||
?.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
@@ -150,6 +147,7 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
isLocked = isLocked,
|
||||
)
|
||||
?.let { inlinePresentation ->
|
||||
@Suppress("DEPRECATION")
|
||||
this.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Field
|
||||
import android.service.autofill.Presentations
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
|
||||
/**
|
||||
* Set up an overlay presentation for this [FilledItem] in the [datasetBuilder] for Android devices
|
||||
* running on API Tiramisu or greater.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
fun FilledItem.applyToDatasetPostTiramisu(
|
||||
datasetBuilder: Dataset.Builder,
|
||||
presentations: Presentations,
|
||||
@@ -29,11 +30,11 @@ fun FilledItem.applyToDatasetPostTiramisu(
|
||||
* Set up an overlay presentation for this [FilledItem] in the [datasetBuilder] for Android devices
|
||||
* running on APIs that predate Tiramisu.
|
||||
*/
|
||||
@Suppress("Deprecation")
|
||||
fun FilledItem.applyToDatasetPreTiramisu(
|
||||
datasetBuilder: Dataset.Builder,
|
||||
remoteViews: RemoteViews,
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
datasetBuilder.setValue(
|
||||
autofillId,
|
||||
value,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
@@ -16,7 +15,6 @@ import com.x8bit.bitwarden.ui.autofill.util.createCipherInlinePresentationOrNull
|
||||
* Build a [Dataset] to represent the [FilledPartition]. This dataset includes an overlay UI
|
||||
* presentation for each filled item. If an [authIntentSender] is present, add it to the dataset.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FilledPartition.buildDataset(
|
||||
authIntentSender: IntentSender?,
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
@@ -26,13 +24,9 @@ fun FilledPartition.buildDataset(
|
||||
autofillCipher = autofillCipher,
|
||||
)
|
||||
val datasetBuilder = Dataset.Builder()
|
||||
authIntentSender?.let { intentSender -> datasetBuilder.setAuthentication(intentSender) }
|
||||
|
||||
authIntentSender
|
||||
?.let { intentSender ->
|
||||
datasetBuilder.setAuthentication(intentSender)
|
||||
}
|
||||
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
applyToDatasetPostTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
datasetBuilder = datasetBuilder,
|
||||
@@ -85,20 +79,19 @@ private fun FilledPartition.applyToDatasetPostTiramisu(
|
||||
* Apply this [FilledPartition] to the [datasetBuilder] on devices running OS versions that predate
|
||||
* Tiramisu.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("NewApi")
|
||||
private fun FilledPartition.buildDatasetPreTiramisu(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
datasetBuilder: Dataset.Builder,
|
||||
remoteViews: RemoteViews,
|
||||
) {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
|
||||
inlinePresentationSpec
|
||||
?.createCipherInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillCipher = autofillCipher,
|
||||
)
|
||||
?.let { inlinePresentation ->
|
||||
@Suppress("DEPRECATION")
|
||||
datasetBuilder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
|
||||
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
|
||||
@@ -87,9 +88,9 @@ class CredentialEntryBuilderImpl(
|
||||
.createWithResource(
|
||||
context,
|
||||
if (isPasskey) {
|
||||
R.drawable.ic_bw_passkey
|
||||
BitwardenDrawable.ic_bw_passkey
|
||||
} else {
|
||||
R.drawable.ic_globe
|
||||
BitwardenDrawable.ic_globe
|
||||
},
|
||||
)
|
||||
.toIcon(context)
|
||||
|
||||
@@ -118,9 +118,13 @@ object CredentialProviderModule {
|
||||
@Singleton
|
||||
fun providePrivilegedAppRepository(
|
||||
privilegedAppDiskSource: PrivilegedAppDiskSource,
|
||||
assetManager: AssetManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
json: Json,
|
||||
): PrivilegedAppRepository = PrivilegedAppRepositoryImpl(
|
||||
privilegedAppDiskSource = privilegedAppDiskSource,
|
||||
assetManager = assetManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
json = json,
|
||||
)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ data class PasskeyAttestationOptions(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("alg")
|
||||
val alg: Long,
|
||||
val alg: Double,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -34,7 +35,6 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -214,7 +214,7 @@ class CredentialProviderProcessorImpl(
|
||||
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher,
|
||||
): CreateEntry.Builder {
|
||||
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
return if (!isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
|
||||
this
|
||||
} else {
|
||||
setBiometricPromptData(
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
package com.x8bit.bitwarden.data.credentials.repository
|
||||
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import com.x8bit.bitwarden.data.credentials.repository.model.PrivilegedAppData
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Repository for managing privileged apps trusted by the user.
|
||||
*/
|
||||
interface PrivilegedAppRepository {
|
||||
|
||||
/**
|
||||
* Flow that represents the trusted privileged apps data.
|
||||
*/
|
||||
val trustedAppDataStateFlow: StateFlow<DataState<PrivilegedAppData>>
|
||||
|
||||
/**
|
||||
* Flow of the user's trusted privileged apps.
|
||||
*/
|
||||
val userTrustedPrivilegedAppsFlow: Flow<PrivilegedAppAllowListJson>
|
||||
val userTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
|
||||
|
||||
/**
|
||||
* Flow of the Google's trusted privileged apps.
|
||||
*/
|
||||
val googleTrustedPrivilegedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
|
||||
|
||||
/**
|
||||
* Flow of the community's trusted privileged apps.
|
||||
*/
|
||||
val communityTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
|
||||
|
||||
/**
|
||||
* List the user's trusted privileged apps.
|
||||
*/
|
||||
suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson
|
||||
suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
|
||||
|
||||
/**
|
||||
* List Google's trusted privileged apps.
|
||||
*/
|
||||
suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
|
||||
|
||||
/**
|
||||
* List community's trusted privileged apps.
|
||||
*/
|
||||
suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
|
||||
|
||||
/**
|
||||
* Returns true if the given [packageName] and [signature] are trusted.
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
package com.x8bit.bitwarden.data.credentials.repository
|
||||
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.combineDataStates
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.PrivilegedAppDiskSource
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.entity.PrivilegedAppEntity
|
||||
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import com.x8bit.bitwarden.data.credentials.repository.model.PrivilegedAppData
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
|
||||
* specified period of time after it no longer has subscribers.
|
||||
*/
|
||||
private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
|
||||
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
|
||||
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
|
||||
private const val ANDROID_TYPE = "android"
|
||||
private const val RELEASE_BUILD = "release"
|
||||
|
||||
@@ -15,17 +38,102 @@ private const val RELEASE_BUILD = "release"
|
||||
*/
|
||||
class PrivilegedAppRepositoryImpl(
|
||||
private val privilegedAppDiskSource: PrivilegedAppDiskSource,
|
||||
private val assetManager: AssetManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val json: Json,
|
||||
) : PrivilegedAppRepository {
|
||||
|
||||
override val userTrustedPrivilegedAppsFlow: Flow<PrivilegedAppAllowListJson> =
|
||||
privilegedAppDiskSource.userTrustedPrivilegedAppsFlow
|
||||
.map { it.toPrivilegedAppAllowListJson() }
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
override suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson =
|
||||
privilegedAppDiskSource.getAllUserTrustedPrivilegedApps()
|
||||
private val mutableUserTrustedAppsFlow =
|
||||
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
|
||||
private val mutableGoogleTrustedAppsFlow =
|
||||
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
|
||||
private val mutableCommunityTrustedPrivilegedAppsFlow =
|
||||
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
|
||||
|
||||
override val trustedAppDataStateFlow: StateFlow<DataState<PrivilegedAppData>> =
|
||||
combine(
|
||||
userTrustedAppsFlow,
|
||||
googleTrustedPrivilegedAppsFlow,
|
||||
communityTrustedAppsFlow,
|
||||
) { userAppsState, googleAppsState, communityAppsState ->
|
||||
combineDataStates(
|
||||
userAppsState,
|
||||
googleAppsState,
|
||||
communityAppsState,
|
||||
) { userApps, googleApps, communityApps ->
|
||||
PrivilegedAppData(
|
||||
googleTrustedApps = googleApps,
|
||||
communityTrustedApps = communityApps,
|
||||
userTrustedApps = userApps,
|
||||
)
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = STOP_TIMEOUT_DELAY_MS),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
|
||||
override val userTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
|
||||
get() = mutableUserTrustedAppsFlow.asStateFlow()
|
||||
|
||||
override val googleTrustedPrivilegedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
|
||||
get() = mutableGoogleTrustedAppsFlow.asStateFlow()
|
||||
|
||||
override val communityTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
|
||||
get() = mutableCommunityTrustedPrivilegedAppsFlow.asStateFlow()
|
||||
|
||||
init {
|
||||
ioScope.launch {
|
||||
mutableGoogleTrustedAppsFlow.value = assetManager
|
||||
.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME)
|
||||
.map { json.decodeFromString<PrivilegedAppAllowListJson>(it) }
|
||||
.fold(
|
||||
onSuccess = { DataState.Loaded(it) },
|
||||
onFailure = { DataState.Error(it) },
|
||||
)
|
||||
|
||||
mutableCommunityTrustedPrivilegedAppsFlow.value = assetManager
|
||||
.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME)
|
||||
.map { json.decodeFromString<PrivilegedAppAllowListJson>(it) }
|
||||
.fold(
|
||||
onSuccess = { DataState.Loaded(it) },
|
||||
onFailure = { DataState.Error(it) },
|
||||
)
|
||||
}
|
||||
|
||||
privilegedAppDiskSource
|
||||
.userTrustedPrivilegedAppsFlow
|
||||
.map { DataState.Loaded(it.toPrivilegedAppAllowListJson()) }
|
||||
.onEach { mutableUserTrustedAppsFlow.value = it }
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson =
|
||||
privilegedAppDiskSource
|
||||
.getAllUserTrustedPrivilegedApps()
|
||||
.toPrivilegedAppAllowListJson()
|
||||
|
||||
override suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? =
|
||||
withContext(ioScope.coroutineContext) {
|
||||
assetManager
|
||||
.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME)
|
||||
.map { json.decodeFromStringOrNull<PrivilegedAppAllowListJson>(it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? {
|
||||
return withContext(ioScope.coroutineContext) {
|
||||
assetManager
|
||||
.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME)
|
||||
.map { json.decodeFromStringOrNull<PrivilegedAppAllowListJson>(it) }
|
||||
.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isPrivilegedAppAllowed(
|
||||
packageName: String,
|
||||
signature: String,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.credentials.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
|
||||
|
||||
/**
|
||||
* Represents privileged applications that are trusted by various sources.
|
||||
*/
|
||||
data class PrivilegedAppData(
|
||||
val googleTrustedApps: PrivilegedAppAllowListJson,
|
||||
val communityTrustedApps: PrivilegedAppAllowListJson,
|
||||
val userTrustedApps: PrivilegedAppAllowListJson,
|
||||
)
|
||||
@@ -7,10 +7,10 @@ import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
|
||||
@@ -21,7 +21,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_UV_PERFORMED_DUR
|
||||
* [CredentialManager] creation process.
|
||||
*/
|
||||
fun Intent.getCreateCredentialRequestOrNull(): CreateCredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
|
||||
?: return null
|
||||
@@ -48,7 +48,7 @@ fun Intent.getCreateCredentialRequestOrNull(): CreateCredentialRequest? {
|
||||
* credential authentication process.
|
||||
*/
|
||||
fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveProviderGetCredentialRequest(this)
|
||||
@@ -84,7 +84,7 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
* [CredentialManager] credential lookup process.
|
||||
*/
|
||||
fun Intent.getGetCredentialsRequestOrNull(): GetCredentialsRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveBeginGetCredentialRequest(this)
|
||||
|
||||
@@ -5,7 +5,7 @@ package com.x8bit.bitwarden.data.credentials.util
|
||||
import android.os.Build
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
|
||||
cipher: Cipher?,
|
||||
isSingleTapAuthEnabled: Boolean,
|
||||
): PublicKeyCredentialEntry.Builder =
|
||||
if (!isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
|
||||
if (isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
|
||||
cipher != null &&
|
||||
isSingleTapAuthEnabled
|
||||
) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.bitwarden.core.util.getBinaryLongFromZoneDateTime
|
||||
import com.bitwarden.core.util.getZoneDateTimeFromBinaryLong
|
||||
import com.bitwarden.data.datasource.disk.BaseDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.util.getBinaryLongFromZoneDateTime
|
||||
import com.x8bit.bitwarden.data.platform.util.getZoneDateTimeFromBinaryLong
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
private const val CURRENT_PUSH_TOKEN_KEY = "pushCurrentToken"
|
||||
|
||||
@@ -93,7 +93,7 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlow = MutableSharedFlow<Boolean?>()
|
||||
private val mutableScreenCaptureAllowedFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableVaultRegisteredForExportFlow =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
@@ -17,12 +17,6 @@ data class MutualTlsCertificate(
|
||||
val leafCertificate: X509Certificate?
|
||||
get() = certificateChain.lastOrNull()
|
||||
|
||||
/**
|
||||
* Root certificate of the chain.
|
||||
*/
|
||||
val rootCertificate: X509Certificate?
|
||||
get() = certificateChain.firstOrNull()
|
||||
|
||||
override fun toString(): String = leafCertificate
|
||||
?.let {
|
||||
buildString {
|
||||
@@ -32,5 +26,5 @@ data class MutualTlsCertificate(
|
||||
appendLine("Valid Until: ${it.notAfter}")
|
||||
}
|
||||
}
|
||||
?: ""
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
return authDiskSource
|
||||
.getShowImportLoginsFlow(userId)
|
||||
.combine(
|
||||
vaultDiskSource.getCiphers(userId),
|
||||
vaultDiskSource.getCiphersFlow(userId),
|
||||
) { showImportLogins, ciphers ->
|
||||
showImportLogins ?: true && ciphers.isEmpty()
|
||||
}
|
||||
@@ -260,7 +260,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
return settingsDiskSource
|
||||
.getShowImportLoginsSettingBadgeFlow(userId)
|
||||
.combine(
|
||||
vaultDiskSource.getCiphers(userId),
|
||||
vaultDiskSource.getCiphersFlow(userId),
|
||||
) { showImportLogins, ciphers ->
|
||||
showImportLogins ?: false && ciphers.isEmpty()
|
||||
}
|
||||
@@ -297,7 +297,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
||||
.flatMapLatest { activeUserId ->
|
||||
combine(
|
||||
flow = this,
|
||||
flow2 = vaultDiskSource.getCiphers(activeUserId),
|
||||
flow2 = vaultDiskSource.getCiphersFlow(activeUserId),
|
||||
) { receiverCurrentValue, ciphers ->
|
||||
receiverCurrentValue && ciphers.none {
|
||||
it.login != null && it.organizationId == null
|
||||
|
||||
@@ -30,8 +30,15 @@ import kotlinx.serialization.json.Json
|
||||
import java.time.Clock
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
/**
|
||||
* The amount of time to delay before updating the push token against Bitwarden server.
|
||||
*/
|
||||
private val PUSH_TOKEN_UPDATE_DELAY: Duration = 7.days
|
||||
|
||||
/**
|
||||
* Primary implementation of [PushManager].
|
||||
@@ -279,11 +286,6 @@ class PushManagerImpl @Inject constructor(
|
||||
val userId = activeUserId ?: return
|
||||
if (!isLoggedIn(userId)) return
|
||||
|
||||
// If the last registered token is from less than a day before, skip this for now
|
||||
val lastRegistration = pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toInstant()
|
||||
val dayBefore = clock.instant().minus(1, ChronoUnit.DAYS)
|
||||
if (lastRegistration?.isAfter(dayBefore) == true) return
|
||||
|
||||
ioScope.launch {
|
||||
pushDiskSource.registeredPushToken?.let {
|
||||
registerPushTokenIfNecessaryInternal(
|
||||
@@ -296,14 +298,11 @@ class PushManagerImpl @Inject constructor(
|
||||
|
||||
private suspend fun registerPushTokenIfNecessaryInternal(userId: String, token: String) {
|
||||
val currentToken = pushDiskSource.getCurrentPushToken(userId)
|
||||
|
||||
if (token == currentToken) {
|
||||
// Our token is up-to-date, so just update the last registration date
|
||||
pushDiskSource.storeLastPushTokenRegistrationDate(
|
||||
userId = userId,
|
||||
registrationDate = ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC),
|
||||
)
|
||||
return
|
||||
val lastRegistration =
|
||||
pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toInstant() ?: return
|
||||
val updateTime = clock.instant().minus(PUSH_TOKEN_UPDATE_DELAY.toJavaDuration())
|
||||
if (updateTime.isBefore(lastRegistration)) return
|
||||
}
|
||||
|
||||
pushService
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.os.Build
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
class SdkClientManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
private val clientProvider: suspend () -> Client = {
|
||||
sdkRepoFactory: SdkRepositoryFactory,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) : SdkClientManager {
|
||||
@@ -22,14 +29,14 @@ class SdkClientManagerImpl(
|
||||
// The SDK requires access to Android APIs that were not made public until API 31. In order
|
||||
// to work around this limitation the SDK must be manually loaded prior to initializing any
|
||||
// [Client] instance.
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.S)) {
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.S)) {
|
||||
nativeLibraryManager.loadLibrary("bitwarden_uniffi")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateClient(
|
||||
userId: String?,
|
||||
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() }
|
||||
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider(userId) }
|
||||
|
||||
override fun destroyClient(
|
||||
userId: String?,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.os.persistableBundleOf
|
||||
@@ -12,6 +12,8 @@ import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.x8bit.bitwarden.R
|
||||
@@ -25,6 +27,7 @@ import java.util.concurrent.TimeUnit
|
||||
class BitwardenClipboardManagerImpl(
|
||||
private val context: Context,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val toastManager: ToastManager,
|
||||
) : BitwardenClipboardManager {
|
||||
private val clipboardManager: ClipboardManager = requireNotNull(context.getSystemService())
|
||||
|
||||
@@ -41,18 +44,22 @@ class BitwardenClipboardManagerImpl(
|
||||
.newPlainText("", text)
|
||||
.apply {
|
||||
description.extras = persistableBundleOf(
|
||||
"android.content.extra.IS_SENSITIVE" to isSensitive,
|
||||
if (isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
ClipDescription.EXTRA_IS_SENSITIVE to isSensitive
|
||||
} else {
|
||||
"android.content.extra.IS_SENSITIVE" to isSensitive
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
if (!isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
val descriptor = toastDescriptorOverride
|
||||
?.let { context.resources.getString(R.string.value_has_been_copied, it) }
|
||||
?: context.resources.getString(
|
||||
R.string.value_has_been_copied,
|
||||
context.resources.getString(R.string.value),
|
||||
)
|
||||
Toast.makeText(context, descriptor, Toast.LENGTH_SHORT).show()
|
||||
toastManager.show(message = descriptor)
|
||||
}
|
||||
|
||||
val frequency = clearClipboardFrequencySeconds ?: return
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.platform.manager.di
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManagerImpl
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.manager.DispatcherManagerImpl
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
@@ -67,6 +69,8 @@ import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManage
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
@@ -134,13 +138,11 @@ object PlatformManagerModule {
|
||||
addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
@ApplicationContext context: Context,
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): AuthenticatorBridgeProcessor = AuthenticatorBridgeProcessorImpl(
|
||||
authenticatorBridgeRepository = authenticatorBridgeRepository,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemFromAuthenticatorManager,
|
||||
context = context,
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -189,9 +191,19 @@ object PlatformManagerModule {
|
||||
fun provideBitwardenClipboardManager(
|
||||
@ApplicationContext context: Context,
|
||||
settingsRepository: SettingsRepository,
|
||||
toastManager: ToastManager,
|
||||
): BitwardenClipboardManager = BitwardenClipboardManagerImpl(
|
||||
context,
|
||||
settingsRepository,
|
||||
context = context,
|
||||
settingsRepository = settingsRepository,
|
||||
toastManager = toastManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideToastManager(
|
||||
@ApplicationContext context: Context,
|
||||
): ToastManager = ToastManagerImpl(
|
||||
context = context,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -233,9 +245,11 @@ object PlatformManagerModule {
|
||||
fun provideSdkClientManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
): SdkClientManager = SdkClientManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
nativeLibraryManager = nativeLibraryManager,
|
||||
sdkRepoFactory = sdkRepositoryFactory,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -374,6 +388,14 @@ object PlatformManagerModule {
|
||||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkRepositoryFactory(
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
): SdkRepositoryFactory = SdkRepositoryFactoryImpl(
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyManager(
|
||||
|
||||
@@ -21,7 +21,6 @@ sealed class FlagKey<out T : Any> {
|
||||
*/
|
||||
val activeFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
AuthenticatorSync,
|
||||
EmailVerification,
|
||||
ImportLoginsFlow,
|
||||
CredentialExchangeProtocolImport,
|
||||
@@ -33,22 +32,13 @@ sealed class FlagKey<out T : Any> {
|
||||
SimpleLoginSelfHostAlias,
|
||||
ChromeAutofill,
|
||||
MobileErrorReporting,
|
||||
FlightRecorder,
|
||||
RestrictCipherItemDeletion,
|
||||
PreAuthSettings,
|
||||
UserManagedPrivilegedApps,
|
||||
RemoveCardPolicy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-bwa-sync"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for Email Verification feature.
|
||||
*/
|
||||
@@ -65,14 +55,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for enabling the flught recorder feature.
|
||||
*/
|
||||
data object FlightRecorder : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-flight-recorder"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the import logins feature.
|
||||
*/
|
||||
@@ -165,14 +147,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the settings menu before login.
|
||||
*/
|
||||
data object PreAuthSettings : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-pm-prelogin-settings"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enabled user-managed privileged apps.
|
||||
*/
|
||||
@@ -181,6 +155,15 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to enable the removal of card item types.
|
||||
* This flag will hide card types from organizations with policy enable and individual vaults
|
||||
*/
|
||||
data object RemoveCardPolicy : FlagKey<Boolean>() {
|
||||
override val keyName: String = "pm-16442-remove-card-item-type-policy"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk
|
||||
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
|
||||
/**
|
||||
* Creates and manages sdk repositories.
|
||||
*/
|
||||
interface SdkRepositoryFactory {
|
||||
/**
|
||||
* Retrieves or creates a [CipherRepository] for use with the Bitwarden SDK.
|
||||
*/
|
||||
fun getCipherRepository(userId: String): CipherRepository
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk
|
||||
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkCipherRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
|
||||
/**
|
||||
* The default implementation for the [SdkRepositoryFactory].
|
||||
*/
|
||||
class SdkRepositoryFactoryImpl(
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
) : SdkRepositoryFactory {
|
||||
override fun getCipherRepository(
|
||||
userId: String,
|
||||
): CipherRepository =
|
||||
SdkCipherRepository(
|
||||
userId = userId,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk.repository
|
||||
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
import com.bitwarden.vault.Cipher
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A user-scoped implementation of a Bitwarden SDK [CipherRepository].
|
||||
*/
|
||||
class SdkCipherRepository(
|
||||
private val userId: String,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
) : CipherRepository {
|
||||
override suspend fun get(id: String): Cipher? =
|
||||
vaultDiskSource
|
||||
.getCipher(userId = userId, cipherId = id)
|
||||
?.toEncryptedSdkCipher()
|
||||
|
||||
override suspend fun has(id: String): Boolean = this.get(id = id) != null
|
||||
|
||||
override suspend fun list(): List<Cipher> =
|
||||
vaultDiskSource
|
||||
.getCiphers(userId = userId)
|
||||
.map { it.toEncryptedSdkCipher() }
|
||||
|
||||
override suspend fun remove(id: String) {
|
||||
vaultDiskSource.deleteCipher(userId = userId, cipherId = id)
|
||||
}
|
||||
|
||||
override suspend fun set(id: String, value: Cipher) {
|
||||
if (id != value.id) {
|
||||
Timber.e("SDK Cipher 'set' operation: ID's do not match")
|
||||
return
|
||||
}
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = value.toEncryptedNetworkCipherResponse(encryptedFor = userId),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,11 @@ import com.bitwarden.authenticatorbridge.util.decrypt
|
||||
import com.bitwarden.authenticatorbridge.util.encrypt
|
||||
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -33,7 +31,6 @@ import timber.log.Timber
|
||||
class AuthenticatorBridgeProcessorImpl(
|
||||
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
context: Context,
|
||||
) : AuthenticatorBridgeProcessor {
|
||||
@@ -44,12 +41,9 @@ class AuthenticatorBridgeProcessorImpl(
|
||||
|
||||
override val binder: IAuthenticatorBridgeService.Stub?
|
||||
get() {
|
||||
return if (
|
||||
!featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) ||
|
||||
isBuildVersionBelow(Build.VERSION_CODES.S)
|
||||
) {
|
||||
// If the feature flag is not enabled, OR if version is below Android 12,
|
||||
// return a null binder which will no-op all service calls
|
||||
return if (!isBuildVersionAtLeast(Build.VERSION_CODES.S)) {
|
||||
// If version is below Android 12, return a null binder which will no-op all
|
||||
// service calls
|
||||
null
|
||||
} else {
|
||||
// Otherwise, return real binder implementation:
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthenticatorBridgeRepository].
|
||||
@@ -19,9 +26,8 @@ import kotlinx.coroutines.flow.first
|
||||
class AuthenticatorBridgeRepositoryImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
) : AuthenticatorBridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
@@ -45,62 +51,50 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
|
||||
val allAccounts = authDiskSource.userState?.accounts.orEmpty()
|
||||
|
||||
return allAccounts
|
||||
.mapNotNull { account ->
|
||||
val userId = account.userId
|
||||
|
||||
.mapNotNull { (userId, account) ->
|
||||
// Grab the user's authenticator sync unlock key. If it is null,
|
||||
// the user has not enabled authenticator sync.
|
||||
// the user has not enabled authenticator sync and we skip the account.
|
||||
val decryptedUserKey = authDiskSource.getAuthenticatorSyncUnlockKey(userId)
|
||||
?: return@mapNotNull null
|
||||
|
||||
// Wait for any unlocking actions to finish:
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
// Unlock vault if necessary:
|
||||
val isVaultAlreadyUnlocked = vaultRepository.isVaultUnlocked(userId = userId)
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
val unlockResult = vaultRepository
|
||||
.unlockVaultWithDecryptedUserKey(
|
||||
val vaultUnlockResult = unlockClient(
|
||||
userId = userId,
|
||||
account = account,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
)
|
||||
when (vaultUnlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
// Destroy our stand-alone instance of the vault.
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
val totpUris = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.first()
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers
|
||||
.filter { it.login?.totp != null && it.deletedDate == null }
|
||||
.getTotpCiphers(userId = userId)
|
||||
// Filter out any deleted ciphers.
|
||||
.filter { it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
val decryptedCipher = vaultSdkSource
|
||||
val decryptedCipher = scopedVaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
@@ -114,19 +108,18 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(
|
||||
userId = userId,
|
||||
isUserInitiated = false,
|
||||
)
|
||||
}
|
||||
// Lock and destroy our stand-alone instance of the vault:
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
userId = userId,
|
||||
name = account.profile.name,
|
||||
email = account.profile.email,
|
||||
environmentLabel = account
|
||||
.settings
|
||||
.environmentUrlData
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.label,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
@@ -134,4 +127,44 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
SharedAccountData(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlockClient(
|
||||
userId: String,
|
||||
account: AccountJson,
|
||||
decryptedUserKey: String,
|
||||
): VaultUnlockResult {
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError(MissingPropertyException("Private key"))
|
||||
return scopedVaultSdkSource
|
||||
.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = userId,
|
||||
kdfParams = account.profile.toSdkParams(),
|
||||
email = account.profile.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
// Initialize the SDK for organizations if necessary
|
||||
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
|
||||
if (organizationKeys != null && result is InitializeCryptoResult.Success) {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
} else {
|
||||
result.asSuccess()
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { VaultUnlockResult.GenericError(error = it) },
|
||||
onSuccess = { it.toVaultUnlockResult() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -41,15 +41,13 @@ object PlatformRepositoryModule {
|
||||
fun providesAuthenticatorBridgeRepository(
|
||||
authRepository: AuthRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
scopedVaultSdkSource = scopedVaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -56,7 +56,7 @@ fun <T, R> MutableStateFlow<T>.observeWhenSubscribedAndUnlocked(
|
||||
.filterNotNull()
|
||||
.flatMapLatest { activeUserId ->
|
||||
vaultUnlockFlow
|
||||
.map { it.any { it.userId == activeUserId } }
|
||||
.map { unlockData -> unlockData.any { it.userId == activeUserId } }
|
||||
.distinctUntilChanged()
|
||||
},
|
||||
) { isSubscribed, activeUserId, isUnlocked ->
|
||||
|
||||
@@ -9,12 +9,12 @@ import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.x8bit.bitwarden.AccessibilityActivity
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -45,7 +45,7 @@ class BitwardenAutofillTileService : TileService() {
|
||||
accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptParseUri
|
||||
val intent = Intent(applicationContext, AccessibilityActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
@@ -22,18 +22,17 @@ class BitwardenGeneratorTileService : TileService() {
|
||||
|
||||
override fun onClick() {
|
||||
if (isLocked) {
|
||||
unlockAndRun(Runnable { launchGenerator() })
|
||||
unlockAndRun { launchGenerator() }
|
||||
} else {
|
||||
launchGenerator()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun launchGenerator() {
|
||||
val intent = intentManager.createTileIntent("bitwarden://password_generator")
|
||||
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
startActivityAndCollapse(intentManager.createTilePendingIntent(0, intent))
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.Keep
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
@@ -22,18 +22,17 @@ class BitwardenVaultTileService : TileService() {
|
||||
|
||||
override fun onClick() {
|
||||
if (isLocked) {
|
||||
unlockAndRun(Runnable { launchVault() })
|
||||
unlockAndRun { launchVault() }
|
||||
} else {
|
||||
launchVault()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun launchVault() {
|
||||
val intent = intentManager.createTileIntent("bitwarden://my_vault")
|
||||
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
startActivityAndCollapse(intent)
|
||||
} else {
|
||||
startActivityAndCollapse(intentManager.createTilePendingIntent(0, intent))
|
||||
|
||||
@@ -14,10 +14,25 @@ interface VaultDiskSource {
|
||||
*/
|
||||
suspend fun saveCipher(userId: String, cipher: SyncResponseJson.Cipher)
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the data source for a given [userId] as a [Flow].
|
||||
*/
|
||||
fun getCiphersFlow(userId: String): Flow<List<SyncResponseJson.Cipher>>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the data source for a given [userId].
|
||||
*/
|
||||
fun getCiphers(userId: String): Flow<List<SyncResponseJson.Cipher>>
|
||||
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
|
||||
*/
|
||||
suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher>
|
||||
|
||||
/**
|
||||
* Retrieves a cipher from the data source for a given [userId] and [cipherId].
|
||||
*/
|
||||
suspend fun getCipher(userId: String, cipherId: String): SyncResponseJson.Cipher?
|
||||
|
||||
/**
|
||||
* Deletes a cipher from the data source for the given [userId] and [cipherId].
|
||||
@@ -72,13 +87,13 @@ interface VaultDiskSource {
|
||||
/**
|
||||
* Replaces all [vault] data for a given [userId] with the new `vault`.
|
||||
*
|
||||
* This will always cause the [getCiphers], [getCollections], and [getFolders] functions to
|
||||
* This will always cause the [getCiphersFlow], [getCollections], and [getFolders] functions to
|
||||
* re-emit even if the underlying data has not changed.
|
||||
*/
|
||||
suspend fun replaceVaultData(userId: String, vault: SyncResponseJson)
|
||||
|
||||
/**
|
||||
* Trigger re-emissions from the [getCiphers], [getCollections], [getFolders], and [getSends]
|
||||
* Trigger re-emissions from the [getCiphersFlow], [getCollections], [getFolders], and [getSends]
|
||||
* functions.
|
||||
*/
|
||||
suspend fun resyncVaultData(userId: String)
|
||||
|
||||
@@ -52,6 +52,7 @@ class VaultDiskSourceImpl(
|
||||
CipherEntity(
|
||||
id = cipher.id,
|
||||
userId = userId,
|
||||
hasTotp = cipher.login?.totp != null,
|
||||
cipherType = json.encodeToString(cipher.type),
|
||||
cipherJson = json.encodeToString(cipher),
|
||||
),
|
||||
@@ -59,13 +60,13 @@ class VaultDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCiphers(
|
||||
override fun getCiphersFlow(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Cipher>> =
|
||||
merge(
|
||||
forceCiphersFlow,
|
||||
ciphersDao
|
||||
.getAllCiphers(userId = userId)
|
||||
.getAllCiphersFlow(userId = userId)
|
||||
.map { entities ->
|
||||
withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
@@ -81,6 +82,55 @@ class VaultDiskSourceImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getAllCiphers(userId = userId)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
||||
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
|
||||
return withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize TOTP Cipher in Vault") }
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.filter {
|
||||
// A safety-check since after the DB migration, we will temporarily think
|
||||
// all ciphers contain a totp code
|
||||
it.login?.totp != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCipher(
|
||||
userId: String,
|
||||
cipherId: String,
|
||||
): SyncResponseJson.Cipher? =
|
||||
ciphersDao
|
||||
.getCipher(userId = userId, cipherId = cipherId)
|
||||
?.let { entity ->
|
||||
withContext(context = dispatcherManager.default) {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteCipher(userId: String, cipherId: String) {
|
||||
ciphersDao.deleteCipher(userId, cipherId)
|
||||
}
|
||||
@@ -220,6 +270,7 @@ class VaultDiskSourceImpl(
|
||||
CipherEntity(
|
||||
id = cipher.id,
|
||||
userId = userId,
|
||||
hasTotp = cipher.login?.totp != null,
|
||||
cipherType = json.encodeToString(cipher.type),
|
||||
cipherJson = json.encodeToString(cipher),
|
||||
)
|
||||
@@ -296,7 +347,7 @@ class VaultDiskSourceImpl(
|
||||
|
||||
override suspend fun resyncVaultData(userId: String) {
|
||||
coroutineScope {
|
||||
val deferredCiphers = async { getCiphers(userId = userId).first() }
|
||||
val deferredCiphers = async { getCiphersFlow(userId = userId).first() }
|
||||
val deferredCollections = async { getCollections(userId = userId).first() }
|
||||
val deferredFolders = async { getFolders(userId = userId).first() }
|
||||
val deferredSends = async { getSends(userId = userId).first() }
|
||||
|
||||
@@ -21,13 +21,38 @@ interface CiphersDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertCiphers(ciphers: List<CipherEntity>)
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database for a given [userId] as a [Flow].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId")
|
||||
fun getAllCiphersFlow(
|
||||
userId: String,
|
||||
): Flow<List<CipherEntity>>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId")
|
||||
fun getAllCiphers(
|
||||
suspend fun getAllCiphers(
|
||||
userId: String,
|
||||
): Flow<List<CipherEntity>>
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves all ciphers from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND has_totp = 1")
|
||||
suspend fun getAllTotpCiphers(
|
||||
userId: String,
|
||||
): List<CipherEntity>
|
||||
|
||||
/**
|
||||
* Retrieves a cipher from the database for a given [userId] and [cipherId].
|
||||
*/
|
||||
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND id = :cipherId LIMIT 1")
|
||||
suspend fun getCipher(
|
||||
userId: String,
|
||||
cipherId: String,
|
||||
): CipherEntity?
|
||||
|
||||
/**
|
||||
* Deletes all the stored ciphers associated with the given [userId]. This will return the
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.database
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
@@ -26,8 +27,11 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
|
||||
FolderEntity::class,
|
||||
SendEntity::class,
|
||||
],
|
||||
version = 6,
|
||||
version = 7,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 6, to = 7),
|
||||
],
|
||||
)
|
||||
@TypeConverters(ZonedDateTimeTypeConverter::class)
|
||||
abstract class VaultDatabase : RoomDatabase() {
|
||||
|
||||
@@ -16,6 +16,11 @@ data class CipherEntity(
|
||||
@ColumnInfo(name = "user_id", index = true)
|
||||
val userId: String,
|
||||
|
||||
// Default to true for initial migration.
|
||||
// Subsequent syncs will populate with correct values for optimizations.
|
||||
@ColumnInfo(name = "has_totp", defaultValue = "1")
|
||||
val hasTotp: Boolean,
|
||||
|
||||
@ColumnInfo(name = "cipher_type")
|
||||
val cipherType: String,
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
/**
|
||||
* This is a non-singleton instance of the [VaultSdkSource] that is intentionally separate; this
|
||||
* allows you to temporarily unlock vaults for a given user within its own scope without affecting
|
||||
* the foreground behavior of the app.
|
||||
*
|
||||
* Users of this class must always call [ScopedVaultSdkSource.clearCrypto] when they are done using
|
||||
* the unlocked vault in order to ensure that this instance of the vault is re-locked.
|
||||
*/
|
||||
interface ScopedVaultSdkSource : VaultSdkSource
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
/**
|
||||
* The default instance of the [ScopedVaultSdkSource]. This uses its own instance of the
|
||||
* [SdkClientManagerImpl] to keep it separate from the rest of the app.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
sdkClientManager = SdkClientManagerImpl(
|
||||
// We do not want to have the real NativeLibraryManager used here to avoid
|
||||
// initializing the library twice.
|
||||
nativeLibraryManager = object : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> = Unit.asSuccess()
|
||||
},
|
||||
sdkRepoFactory = sdkRepositoryFactory,
|
||||
featureFlagManager = featureFlagManager,
|
||||
),
|
||||
dispatcherManager = dispatcherManager,
|
||||
),
|
||||
) : ScopedVaultSdkSource, VaultSdkSource by vaultSdkSource
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
@@ -22,6 +23,7 @@ import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.Collection
|
||||
import com.bitwarden.vault.CollectionView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.EncryptionContext
|
||||
import com.bitwarden.vault.Folder
|
||||
import com.bitwarden.vault.FolderView
|
||||
@@ -226,6 +228,17 @@ interface VaultSdkSource {
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherView>>
|
||||
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s for the user with the given [userId].
|
||||
*
|
||||
* @return A [DecryptCipherListResult] containing the decrypted [CipherListView]s and references
|
||||
* to [Cipher]s that cannot be decrypted.
|
||||
*/
|
||||
suspend fun decryptCipherListWithFailures(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
): Result<DecryptCipherListResult>
|
||||
|
||||
/**
|
||||
* Decrypts a [Collection] for the user with the given [userId], returning a [CollectionView]
|
||||
* wrapped in a [Result].
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.Collection
|
||||
import com.bitwarden.vault.CollectionView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.EncryptionContext
|
||||
import com.bitwarden.vault.Folder
|
||||
import com.bitwarden.vault.FolderView
|
||||
@@ -311,6 +312,17 @@ class VaultSdkSourceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun decryptCipherListWithFailures(
|
||||
userId: String,
|
||||
cipherList: List<Cipher>,
|
||||
): Result<DecryptCipherListResult> =
|
||||
runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.vault()
|
||||
.ciphers()
|
||||
.decryptListWithFailures(cipherList)
|
||||
}
|
||||
|
||||
override suspend fun decryptCollection(
|
||||
userId: String,
|
||||
collection: Collection,
|
||||
|
||||
@@ -3,7 +3,11 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialStoreImpl
|
||||
@@ -32,6 +36,18 @@ object VaultSdkModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
fun providesScopedVaultSdkSource(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
): ScopedVaultSdkSource =
|
||||
ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
sdkRepositoryFactory = sdkRepositoryFactory,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFido2CredentialStore(
|
||||
|
||||
@@ -21,7 +21,7 @@ class Fido2CredentialAuthenticationUserInterfaceImpl(
|
||||
override suspend fun checkUser(
|
||||
options: CheckUserOptions,
|
||||
hint: UiHint,
|
||||
): CheckUserResult = CheckUserResult(true, true)
|
||||
): CheckUserResult = CheckUserResult(userPresent = true, userVerified = true)
|
||||
|
||||
override suspend fun checkUserAndPickCredentialForCreation(
|
||||
options: CheckUserOptions,
|
||||
|
||||
@@ -22,14 +22,14 @@ class Fido2CredentialRegistrationUserInterfaceImpl(
|
||||
override suspend fun checkUser(
|
||||
options: CheckUserOptions,
|
||||
hint: UiHint,
|
||||
): CheckUserResult = CheckUserResult(true, true)
|
||||
): CheckUserResult = CheckUserResult(userPresent = true, userVerified = true)
|
||||
|
||||
override suspend fun checkUserAndPickCredentialForCreation(
|
||||
options: CheckUserOptions,
|
||||
newCredential: Fido2CredentialNewView,
|
||||
): CheckUserAndPickCredentialForCreationResult = CheckUserAndPickCredentialForCreationResult(
|
||||
cipher = CipherViewWrapper(selectedCipherView),
|
||||
checkUserResult = CheckUserResult(true, true),
|
||||
checkUserResult = CheckUserResult(userPresent = true, userVerified = true),
|
||||
)
|
||||
|
||||
override suspend fun isVerificationEnabled(): Boolean = isVerificationSupported
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
|
||||
@@ -51,6 +52,11 @@ interface CipherManager {
|
||||
attachmentId: String,
|
||||
): DownloadAttachmentResult
|
||||
|
||||
/**
|
||||
* Attempt to retrieve a decrypted cipher based on the [cipherId].
|
||||
*/
|
||||
suspend fun getCipher(cipherId: String): GetCipherResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a cipher.
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
import com.bitwarden.network.model.ShareCipherJsonRequest
|
||||
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
|
||||
import com.bitwarden.network.model.UpdateCipherResponseJson
|
||||
@@ -20,6 +21,7 @@ import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
|
||||
@@ -52,19 +54,33 @@ class CipherManagerImpl(
|
||||
|
||||
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateCipherResult.Error(error = NoActiveUserException())
|
||||
?: return CreateCipherResult.Error(
|
||||
error = NoActiveUserException(),
|
||||
errorMessage = null,
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptCipher(
|
||||
userId = userId,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
.flatMap { ciphersService.createCipher(body = it.toEncryptedNetworkCipher()) }
|
||||
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
|
||||
.map { response ->
|
||||
when (response) {
|
||||
is CreateCipherResponseJson.Invalid -> {
|
||||
CreateCipherResult.Error(errorMessage = response.message, error = null)
|
||||
}
|
||||
|
||||
is CreateCipherResponseJson.Success -> {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = response.cipher)
|
||||
CreateCipherResult.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateCipherResult.Error(error = it) },
|
||||
onFailure = { CreateCipherResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerAddCipherAction()
|
||||
CreateCipherResult.Success
|
||||
it
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -74,7 +90,7 @@ class CipherManagerImpl(
|
||||
collectionIds: List<String>,
|
||||
): CreateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateCipherResult.Error(error = NoActiveUserException())
|
||||
?: return CreateCipherResult.Error(errorMessage = null, error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptCipher(
|
||||
userId = userId,
|
||||
@@ -88,17 +104,26 @@ class CipherManagerImpl(
|
||||
),
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = it.copy(collectionIds = collectionIds),
|
||||
)
|
||||
.map { response ->
|
||||
when (response) {
|
||||
is CreateCipherResponseJson.Invalid -> {
|
||||
CreateCipherResult.Error(errorMessage = response.message, error = null)
|
||||
}
|
||||
|
||||
is CreateCipherResponseJson.Success -> {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = response.cipher.copy(collectionIds = collectionIds),
|
||||
)
|
||||
CreateCipherResult.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateCipherResult.Error(error = it) },
|
||||
onFailure = { CreateCipherResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerAddCipherAction()
|
||||
CreateCipherResult.Success
|
||||
it
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -194,6 +219,24 @@ class CipherManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCipher(cipherId: String): GetCipherResult {
|
||||
val userId = activeUserId ?: return GetCipherResult.Failure(NoActiveUserException())
|
||||
return vaultDiskSource
|
||||
.getCipher(userId = userId, cipherId = cipherId)
|
||||
?.let { syncResponseCipher ->
|
||||
vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = syncResponseCipher.toEncryptedSdkCipher(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { GetCipherResult.Success(it) },
|
||||
onFailure = { GetCipherResult.Failure(it) },
|
||||
)
|
||||
}
|
||||
?: GetCipherResult.CipherNotFound
|
||||
}
|
||||
|
||||
override suspend fun restoreCipher(
|
||||
cipherId: String,
|
||||
cipherView: CipherView,
|
||||
|
||||
@@ -182,6 +182,7 @@ class VaultLockManagerImpl(
|
||||
privateKey = privateKey,
|
||||
method = initUserCryptoMethod,
|
||||
userId = userId,
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager.model
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
||||
/**
|
||||
* Models result of getting a cipher.
|
||||
*/
|
||||
sealed class GetCipherResult {
|
||||
/**
|
||||
* Cipher retrieved successfully.
|
||||
*
|
||||
* @param cipherView The cipher retrieved.
|
||||
*/
|
||||
data class Success(
|
||||
val cipherView: CipherView,
|
||||
) : GetCipherResult()
|
||||
|
||||
/**
|
||||
* Cipher not found.
|
||||
*/
|
||||
data object CipherNotFound : GetCipherResult()
|
||||
|
||||
/**
|
||||
* Generic error while retrieving cipher.
|
||||
*/
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : GetCipherResult()
|
||||
}
|
||||
@@ -147,7 +147,7 @@ class VaultRepositoryImpl(
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val totpCodeManager: TotpCodeManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val databaseSchemeManager: DatabaseSchemeManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
pushManager: PushManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
@@ -498,23 +498,20 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
return getVaultItemStateFlow(cipherId)
|
||||
.flatMapLatest { cipherDataState ->
|
||||
val cipher = cipherDataState.data
|
||||
?: return@flatMapLatest flowOf(DataState.Loaded(null))
|
||||
totpCodeManager
|
||||
.getTotpCodeStateFlow(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
)
|
||||
.map { totpCodeDataState ->
|
||||
combineDataStates(
|
||||
totpCodeDataState,
|
||||
cipherDataState,
|
||||
) { _, _ ->
|
||||
// We are only combining the DataStates to know the overall state,
|
||||
// we map it to the appropriate value below.
|
||||
}
|
||||
.mapNullable { totpCodeDataState.data }
|
||||
cipherDataState
|
||||
.data
|
||||
?.let {
|
||||
totpCodeManager
|
||||
.getTotpCodeStateFlow(userId = userId, cipher = it)
|
||||
.map { totpCodeDataState ->
|
||||
combineDataStates(totpCodeDataState, cipherDataState) { _, _ ->
|
||||
// We are only combining the DataStates to know the overall
|
||||
// state, we map it to the appropriate value below.
|
||||
}
|
||||
.mapNullable { totpCodeDataState.data }
|
||||
}
|
||||
}
|
||||
?: flowOf(DataState.Loaded(null))
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
@@ -608,12 +605,12 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
.also {
|
||||
if (it is VaultUnlockResult.Success) {
|
||||
encryptedBiometricsKey?.let {
|
||||
encryptedBiometricsKey?.let { key ->
|
||||
// If this key is present, we store it and the associated IV for future use
|
||||
// since we want to migrate the user to a more secure form of biometrics.
|
||||
authDiskSource.storeUserBiometricUnlockKey(
|
||||
userId = userId,
|
||||
biometricsKey = it,
|
||||
biometricsKey = key,
|
||||
)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
|
||||
}
|
||||
@@ -925,7 +922,7 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun clearFolderIdFromCiphers(folderId: String, userId: String) {
|
||||
vaultDiskSource.getCiphers(userId).firstOrNull()?.forEach {
|
||||
vaultDiskSource.getCiphersFlow(userId).firstOrNull()?.forEach {
|
||||
if (it.folderId == folderId) {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId, it.copy(folderId = null),
|
||||
@@ -944,7 +941,7 @@ class VaultRepositoryImpl(
|
||||
.map { it.toEncryptedSdkFolder() }
|
||||
|
||||
val ciphers = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.getCiphersFlow(userId)
|
||||
.firstOrNull()
|
||||
.orEmpty()
|
||||
.map { it.toEncryptedSdkCipher() }
|
||||
@@ -1070,7 +1067,7 @@ class VaultRepositoryImpl(
|
||||
userId: String,
|
||||
): Flow<DataState<List<CipherView>>> =
|
||||
vaultDiskSource
|
||||
.getCiphers(userId = userId)
|
||||
.getCiphersFlow(userId = userId)
|
||||
.onStart { mutableCiphersStateFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
waitUntilUnlocked(userId = userId)
|
||||
@@ -1091,7 +1088,7 @@ class VaultRepositoryImpl(
|
||||
userId: String,
|
||||
): Flow<DataState<List<CipherListView>>> =
|
||||
vaultDiskSource
|
||||
.getCiphers(userId = userId)
|
||||
.getCiphersFlow(userId = userId)
|
||||
.onStart { mutableCiphersListViewStateFlow.updateToPendingOrLoading() }
|
||||
.map {
|
||||
waitUntilUnlocked(userId = userId)
|
||||
@@ -1491,7 +1488,7 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
vaultDiskSource.resyncVaultData(userId = userId)
|
||||
val itemsAvailable = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.getCiphersFlow(userId)
|
||||
.firstOrNull()
|
||||
?.isNotEmpty() == true
|
||||
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||
|
||||
@@ -11,7 +11,8 @@ sealed class CreateCipherResult {
|
||||
data object Success : CreateCipherResult()
|
||||
|
||||
/**
|
||||
* Generic error while creating cipher.
|
||||
* Generic error while creating cipher. The optional [errorMessage] may be displayed directly in
|
||||
* the UI when present.
|
||||
*/
|
||||
data class Error(val error: Throwable) : CreateCipherResult()
|
||||
data class Error(val errorMessage: String?, val error: Throwable?) : CreateCipherResult()
|
||||
}
|
||||
|
||||
@@ -1,37 +1,59 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.toRoute
|
||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import com.x8bit.bitwarden.ui.platform.util.toObjectRoute
|
||||
import com.bitwarden.ui.platform.util.ParcelableRouteSerializer
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the setup autofill screen.
|
||||
*/
|
||||
sealed class SetupAutofillRoute {
|
||||
@Parcelize
|
||||
@Serializable(with = SetupAutofillRoute.Serializer::class)
|
||||
sealed class SetupAutofillRoute : Parcelable {
|
||||
/**
|
||||
* The [isInitialSetup] value used in the setup autofill screen.
|
||||
*/
|
||||
abstract val isInitialSetup: Boolean
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<SetupAutofillRoute>(SetupAutofillRoute::class)
|
||||
|
||||
/**
|
||||
* The type-safe route for the standard setup autofill screen.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
@Serializable(with = Standard.Serializer::class)
|
||||
data object Standard : SetupAutofillRoute() {
|
||||
override val isInitialSetup: Boolean get() = false
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<Standard>(Standard::class)
|
||||
}
|
||||
|
||||
/**
|
||||
* The type-safe route for the root setup autofill screen.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
@Serializable(with = AsRoot.Serializer::class)
|
||||
data object AsRoot : SetupAutofillRoute() {
|
||||
override val isInitialSetup: Boolean get() = true
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<AsRoot>(AsRoot::class)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +66,8 @@ data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean)
|
||||
* Constructs a [SetupAutoFillScreenArgs] from the [SavedStateHandle] and internal route data.
|
||||
*/
|
||||
fun SavedStateHandle.toSetupAutoFillArgs(): SetupAutoFillScreenArgs {
|
||||
val route = (this.toObjectRoute<SetupAutofillRoute.AsRoot>()
|
||||
?: this.toObjectRoute<SetupAutofillRoute.Standard>())
|
||||
return route
|
||||
?.let { SetupAutoFillScreenArgs(isInitialSetup = it.isInitialSetup) }
|
||||
?: throw IllegalStateException("Missing correct route for SetupAutofillScreen")
|
||||
val route = this.toRoute<SetupAutofillRoute>()
|
||||
return SetupAutoFillScreenArgs(isInitialSetup = route.isInitialSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,7 +34,9 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.model.WindowSize
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
@@ -42,12 +44,10 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.rememberWindowSize
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.rememberSetupAutoFillHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
||||
@@ -223,7 +223,7 @@ private fun SetupAutoFillContentHeader(
|
||||
@Composable
|
||||
private fun OrderedHeaderContent() {
|
||||
BitwardenGifImage(
|
||||
resId = R.drawable.img_setup_autofill,
|
||||
resId = BitwardenDrawable.img_setup_autofill,
|
||||
modifier = Modifier
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
@@ -79,7 +80,7 @@ private fun SetupCompleteContent(
|
||||
) {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(R.drawable.img_setup_complete),
|
||||
painter = rememberVectorPainter(BitwardenDrawable.img_setup_complete),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
|
||||
@@ -1,37 +1,59 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.toRoute
|
||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
import com.x8bit.bitwarden.ui.platform.util.toObjectRoute
|
||||
import com.bitwarden.ui.platform.util.ParcelableRouteSerializer
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the setup unlock screen.
|
||||
*/
|
||||
sealed class SetupUnlockRoute {
|
||||
@Parcelize
|
||||
@Serializable(with = SetupUnlockRoute.Serializer::class)
|
||||
sealed class SetupUnlockRoute : Parcelable {
|
||||
/**
|
||||
* The [isInitialSetup] value used in the setup unlock screen.
|
||||
*/
|
||||
abstract val isInitialSetup: Boolean
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<SetupUnlockRoute>(SetupUnlockRoute::class)
|
||||
|
||||
/**
|
||||
* The type-safe route for the standard setup unlock screen.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
@Serializable(with = Standard.Serializer::class)
|
||||
data object Standard : SetupUnlockRoute() {
|
||||
override val isInitialSetup: Boolean get() = false
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<Standard>(Standard::class)
|
||||
}
|
||||
|
||||
/**
|
||||
* The type-safe route for the root setup unlock screen.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
@Serializable(with = AsRoot.Serializer::class)
|
||||
data object AsRoot : SetupUnlockRoute() {
|
||||
override val isInitialSetup: Boolean get() = true
|
||||
|
||||
/**
|
||||
* Custom serializer to support polymorphic routes.
|
||||
*/
|
||||
class Serializer : ParcelableRouteSerializer<AsRoot>(AsRoot::class)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +68,8 @@ data class SetupUnlockArgs(
|
||||
* Constructs a [SetupUnlockArgs] from the [SavedStateHandle] and internal route data.
|
||||
*/
|
||||
fun SavedStateHandle.toSetupUnlockArgs(): SetupUnlockArgs {
|
||||
val route = this.toObjectRoute<SetupUnlockRoute.AsRoot>()
|
||||
?: this.toObjectRoute<SetupUnlockRoute.Standard>()
|
||||
return route
|
||||
?.let { SetupUnlockArgs(isInitialSetup = it.isInitialSetup) }
|
||||
?: throw IllegalStateException("Missing correct route for SetupUnlockScreen")
|
||||
val route = this.toRoute<SetupUnlockRoute>()
|
||||
return SetupUnlockArgs(isInitialSetup = route.isInitialSetup)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.model.WindowSize
|
||||
@@ -46,7 +47,6 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.rememberWindowSize
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.SetupUnlockHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
@@ -244,7 +244,7 @@ private fun SetUpLaterButton(
|
||||
private fun ColumnScope.SetupUnlockHeaderCompact() {
|
||||
Spacer(modifier = Modifier.height(height = 32.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.account_setup),
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.account_setup),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
@@ -288,7 +288,7 @@ private fun SetupUnlockHeaderMedium(
|
||||
.standardHorizontalMargin(),
|
||||
) {
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.account_setup),
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.account_setup),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = 100.dp)
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.checkemail.handlers.rememberCheckEmailHandler
|
||||
@@ -78,7 +79,7 @@ fun CheckEmailScreen(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
)
|
||||
@@ -115,7 +116,7 @@ private fun CheckEmailContent(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.open_email),
|
||||
painter = rememberVectorPainter(id = BitwardenDrawable.open_email),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user