mirror of
https://github.com/bitwarden/android.git
synced 2026-05-13 15:21:11 -05:00
Compare commits
2 Commits
agalles/20
...
v2025.6.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5b022caaa | ||
|
|
8a371f8c21 |
20
.github/actions/log-inputs/action.yml
vendored
20
.github/actions/log-inputs/action.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 'Log Inputs to Job Summary'
|
||||
description: 'Log workflow inputs to the GitHub Actions job summary'
|
||||
|
||||
inputs:
|
||||
inputs:
|
||||
description: 'Workflow inputs as JSON'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
shell: bash
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ inputs.inputs }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
49
.github/actions/setup-android-build/action.yml
vendored
49
.github/actions/setup-android-build/action.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: 'Setup Android Build'
|
||||
description: 'Setup Android build environment with Gradle, Ruby, and Fastlane'
|
||||
inputs:
|
||||
java-version:
|
||||
description: 'Java version to use'
|
||||
required: false
|
||||
default: '17'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ inputs.java-version }}
|
||||
|
||||
- name: Install Fastlane
|
||||
shell: bash
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
70
.github/workflows/build-authenticator.yml
vendored
70
.github/workflows/build-authenticator.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/**/*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -33,7 +32,6 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -41,23 +39,14 @@ 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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -67,7 +56,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -82,7 +71,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -110,10 +99,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -124,18 +113,9 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "BWA-AAB-KEYSTORE-STORE-PASSWORD,BWA-AAB-KEYSTORE-KEY-PASSWORD,BWA-APK-KEYSTORE-STORE-PASSWORD,BWA-APK-KEYSTORE-KEY-PASSWORD"
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -179,9 +159,6 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: AZ Logout
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
if: ${{ inputs.publish-to-play-store }}
|
||||
run: |
|
||||
@@ -189,10 +166,10 @@ jobs:
|
||||
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -202,7 +179,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -216,25 +193,16 @@ jobs:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Update app CI Build info
|
||||
run: |
|
||||
./scripts/update_app_ci_build_info.sh \
|
||||
$GITHUB_REPOSITORY \
|
||||
$GITHUB_REF_NAME \
|
||||
$GITHUB_SHA \
|
||||
$GITHUB_RUN_ID \
|
||||
$GITHUB_RUN_ATTEMPT
|
||||
|
||||
- name: Increment version
|
||||
run: |
|
||||
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
|
||||
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
bundle exec fastlane setAuthenticatorBuildVersionInfo \
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='appVersionName = "([^"]+)"'
|
||||
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat authenticator/build.gradle.kts)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -245,18 +213,18 @@ jobs:
|
||||
run: |
|
||||
bundle exec fastlane bundleAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
|
||||
storePassword:'${{ secrets.BWA_AAB_KEYSTORE_STORE_PASSWORD }}' \
|
||||
keyAlias:authenticatorupload \
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
|
||||
keyPassword:'${{ secrets.BWA_AAB_KEYSTORE_KEY_PASSWORD }}'
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
run: |
|
||||
bundle exec fastlane buildAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
|
||||
storePassword:'${{ secrets.BWA_APK_KEYSTORE_STORE_PASSWORD }}' \
|
||||
keyAlias:bitwardenauthenticator \
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
|
||||
keyPassword:'${{ secrets.BWA_APK_KEYSTORE_KEY_PASSWORD }}'
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
|
||||
115
.github/workflows/build.yml
vendored
115
.github/workflows/build.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/**/*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
@@ -34,7 +33,6 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -42,23 +40,14 @@ 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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -68,7 +57,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -83,7 +72,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -118,10 +107,10 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -132,18 +121,9 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -180,14 +160,11 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -197,7 +174,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -230,48 +207,48 @@ jobs:
|
||||
- name: Generate release Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
|
||||
env:
|
||||
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreRelease \
|
||||
storeFile:app_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }} \
|
||||
keyAlias:upload \
|
||||
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store bundle
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
env:
|
||||
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
|
||||
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_BETA_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_BETA_KEY_PASSWORD: ${{ secrets.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreBeta \
|
||||
storeFile:app_beta_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD_BETA_KEYSTORE_PASSWORD }} \
|
||||
keyAlias:bitwarden-beta-upload \
|
||||
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreReleaseApk \
|
||||
storeFile:app_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
|
||||
storePassword:${{ env.PLAY_KEYSTORE_PASSWORD }} \
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
|
||||
keyPassword:${{ env.PLAY_KEYSTORE_PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
PLAY_BETA_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
|
||||
PLAY_BETA_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreBetaApk \
|
||||
storeFile:app_beta_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
|
||||
storePassword:${{ env.PLAY_BETA_KEYSTORE_PASSWORD }} \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
@@ -429,10 +406,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -443,18 +420,9 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "FDROID-KEYSTORE-PASSWORD,FDROID-BETA-KEYSTORE-PASSWORD,FDROID-BETA-KEY-PASSWORD"
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -477,14 +445,11 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -494,7 +459,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -526,15 +491,15 @@ jobs:
|
||||
versionCode:$VERSION_CODE \
|
||||
versionName:${{ inputs.version-name || '' }}
|
||||
|
||||
regex='appVersionName = "([^"]+)"'
|
||||
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
|
||||
regex='versionName = "([^"]+)"'
|
||||
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Generate F-Droid artifacts
|
||||
env:
|
||||
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
|
||||
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidReleaseApk \
|
||||
storeFile:app_fdroid-keystore.jks \
|
||||
@@ -544,14 +509,14 @@ jobs:
|
||||
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
|
||||
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
FDROID_BETA_KEY_PASSWORD: ${{ secrets.FDROID_BETA_KEY_PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidBetaApk \
|
||||
storeFile:app_beta_fdroid-keystore.jks \
|
||||
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
|
||||
storePassword:"${{ env.FDROID_BETA_KEYSTORE_PASSWORD }}" \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
|
||||
|
||||
- name: Download Google Privileged Browsers List
|
||||
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
|
||||
|
||||
56
.github/workflows/crowdin-pull.yml
vendored
56
.github/workflows/crowdin-pull.yml
vendored
@@ -8,29 +8,30 @@ on:
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Crowdin Pull - ${{ github.event_name }}
|
||||
name: Crowdin Pull - ${{ matrix.name }} - ${{ github.event_name }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: Password Manager
|
||||
project_id: 269690
|
||||
config: crowdin-bwpm.yml
|
||||
branch: crowdin-pull-bwpm
|
||||
- name: Authenticator
|
||||
project_id: 673718
|
||||
config: crowdin-bwa.yml
|
||||
branch: crowdin-pull-bwa
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -39,33 +40,30 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
- name: Download translations for ${{ matrix.name }}
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
_CROWDIN_PROJECT_ID: ${{ matrix.project_id }}
|
||||
with:
|
||||
config: crowdin.yml
|
||||
config: ${{ matrix.config }}
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
github_user_name: "bitwarden-devops-bot"
|
||||
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
commit_message: "Crowdin Pull"
|
||||
localization_branch_name: "crowdin-pull"
|
||||
commit_message: "Crowdin Pull - ${{ matrix.name }}"
|
||||
localization_branch_name: ${{ matrix.branch }}
|
||||
create_pull_request: true
|
||||
pull_request_title: "Crowdin Pull"
|
||||
pull_request_body: ":inbox_tray: New translations received!"
|
||||
pull_request_title: "Crowdin Pull - ${{ matrix.name }}"
|
||||
pull_request_body: ":inbox_tray: New translations for ${{ matrix.name }} received!"
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
|
||||
27
.github/workflows/crowdin-push.yml
vendored
27
.github/workflows/crowdin-push.yml
vendored
@@ -13,17 +13,14 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -32,16 +29,24 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@590c05e09a29f392b203faf4d6aa8e0cd32c7835 # v2.9.1
|
||||
- name: Upload sources for Password Manager
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
with:
|
||||
config: crowdin.yml
|
||||
config: crowdin-bwpm.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
- name: Upload sources for Authenticator
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
_CROWDIN_PROJECT_ID: "673718"
|
||||
with:
|
||||
config: crowdin-bwa.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
100
.github/workflows/github-release.yml
vendored
100
.github/workflows/github-release.yml
vendored
@@ -21,19 +21,13 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Get branch from workflow run
|
||||
id: get_release_branch
|
||||
env:
|
||||
@@ -50,29 +44,23 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔖 Release branch: $release_branch"
|
||||
echo "🔖 Workflow name: $workflow_name"
|
||||
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
|
||||
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
|
||||
|
||||
case "$workflow_name" in
|
||||
*"Password Manager"* | "Build")
|
||||
app_name="Password Manager"
|
||||
app_name_suffix="bwpm"
|
||||
echo "app_name=Password Manager" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=bwpm" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*"Authenticator"*)
|
||||
app_name="Authenticator"
|
||||
app_name_suffix="bwa"
|
||||
echo "app_name=Authenticator" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=bwa" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unknown workflow name: $workflow_name"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "🔖 App name: $app_name"
|
||||
echo "🔖 App name suffix: $app_name_suffix"
|
||||
echo "app_name=$app_name" >> $GITHUB_OUTPUT
|
||||
echo "app_name_suffix=$app_name_suffix" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get version info from run logs and set release tag name
|
||||
id: get_release_info
|
||||
@@ -110,7 +98,7 @@ jobs:
|
||||
echo "🔖 New tag name: $tag_name"
|
||||
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
|
||||
|
||||
last_release_tag=$(git tag -l --sort=-authordate | grep "$_APP_NAME_SUFFIX" | head -n 1)
|
||||
last_release_tag=$(git tag -l --sort=-authordate | grep "$APP_NAME_SUFFIX" | head -n 1)
|
||||
echo "🔖 Last release tag: $last_release_tag"
|
||||
echo "last_release_tag=$last_release_tag" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -127,51 +115,6 @@ jobs:
|
||||
find $ARTIFACTS_PATH -type f
|
||||
fi
|
||||
|
||||
# Files that won't be included in any release
|
||||
files_to_remove=(
|
||||
"com.x8bit.bitwarden.aab"
|
||||
"com.x8bit.bitwarden.aab-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.beta.apk"
|
||||
"com.x8bit.bitwarden.beta.apk-sha256.txt"
|
||||
"com.x8bit.bitwarden.beta.aab"
|
||||
"com.x8bit.bitwarden.beta.aab-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.beta-fdroid.apk"
|
||||
"com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt"
|
||||
|
||||
"com.x8bit.bitwarden.dev.apk"
|
||||
"com.x8bit.bitwarden.dev.apk-sha256.txt"
|
||||
|
||||
"com.bitwarden.authenticator.aab"
|
||||
"authenticator-android-aab-sha256.txt"
|
||||
)
|
||||
|
||||
for file in "${files_to_remove[@]}"; do
|
||||
find $ARTIFACTS_PATH -name "$file" -type f -delete
|
||||
done
|
||||
echo "🔖 Removed internal artifacts."
|
||||
echo ""
|
||||
echo "🔖 Files to be included in the release:"
|
||||
find $ARTIFACTS_PATH -type f
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "JIRA-API-EMAIL,JIRA-API-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Get product release notes
|
||||
id: get_release_notes
|
||||
env:
|
||||
@@ -179,8 +122,8 @@ jobs:
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
|
||||
_RELEASE_TICKET_ID: ${{ inputs.release-ticket-id }}
|
||||
_JIRA_API_EMAIL: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
|
||||
_JIRA_API_EMAIL: ${{ secrets.JIRA_API_EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
run: |
|
||||
echo "Getting product release notes"
|
||||
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py $_RELEASE_TICKET_ID $_JIRA_API_EMAIL $_JIRA_API_TOKEN)
|
||||
@@ -206,28 +149,23 @@ jobs:
|
||||
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
|
||||
_LAST_RELEASE_TAG: ${{ steps.get_release_info.outputs.last_release_tag }}
|
||||
run: |
|
||||
is_latest_release=false
|
||||
if [[ "$_APP_NAME" == "Password Manager" ]]; then
|
||||
is_latest_release=true
|
||||
fi
|
||||
|
||||
echo "⌛️ Creating release for $_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER) on $_TARGET_COMMIT"
|
||||
release_url=$(gh release create "$_TAG_NAME" \
|
||||
--title "$_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER)" \
|
||||
--target "$_TARGET_COMMIT" \
|
||||
--generate-notes \
|
||||
--notes-start-tag "$_LAST_RELEASE_TAG" \
|
||||
--latest=$is_latest_release \
|
||||
--draft \
|
||||
$ARTIFACTS_PATH/*/*)
|
||||
|
||||
# Extract release tag from URL
|
||||
release_id_from_url=$(echo "$release_url" | sed 's/.*\/tag\///')
|
||||
echo "release_id_from_url=$release_id_from_url" >> $GITHUB_OUTPUT
|
||||
echo "url=$release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ Release created: $release_url"
|
||||
echo "🔖 Release ID from URL: $release_id_from_url"
|
||||
|
||||
# Get release info for outputs
|
||||
release_data=$(gh release view "$_TAG_NAME" --json id)
|
||||
release_id=$(echo "$release_data" | jq -r .id)
|
||||
|
||||
echo "id=$release_id" >> $GITHUB_OUTPUT
|
||||
echo "url=$release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update Release Description
|
||||
id: update_release_description
|
||||
@@ -235,10 +173,10 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
|
||||
_RELEASE_ID: ${{ steps.create_release.outputs.release_id_from_url }}
|
||||
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
|
||||
run: |
|
||||
echo "Getting current release body. Release ID: $_RELEASE_ID"
|
||||
current_body=$(gh release view "$_RELEASE_ID" --json body --jq .body)
|
||||
echo "Getting current release body. Tag: $_TAG_NAME"
|
||||
current_body=$(gh release view "$_TAG_NAME" --json body --jq .body)
|
||||
|
||||
product_release_notes=$(cat product_release_notes.txt)
|
||||
|
||||
@@ -249,7 +187,7 @@ jobs:
|
||||
${current_body}
|
||||
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
|
||||
|
||||
new_release_url=$(gh release edit "$_RELEASE_ID" --notes "$updated_body")
|
||||
new_release_url=$(gh release edit "$_TAG_NAME" --notes "$updated_body")
|
||||
|
||||
# draft release links change after editing
|
||||
echo "release_url=$new_release_url" >> $GITHUB_OUTPUT
|
||||
|
||||
38
.github/workflows/publish-github-release.yml
vendored
38
.github/workflows/publish-github-release.yml
vendored
@@ -1,36 +1,12 @@
|
||||
name: Publish Password Manager and Authenticator GitHub Release as newest
|
||||
name: Publish GitHub Release as newest
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1-5'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
publish-release-password-manager:
|
||||
name: Publish Password Manager Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Password Manager"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "play_creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
|
||||
secrets: inherit
|
||||
|
||||
publish-release-authenticator:
|
||||
name: Publish Authenticator Release
|
||||
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
|
||||
with:
|
||||
release_name: "Authenticator"
|
||||
workflow_name: "publish-github-release.yml"
|
||||
credentials_filename: "authenticator_play_store-creds.json"
|
||||
project_type: android
|
||||
check_release_command: >
|
||||
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
|
||||
secrets: inherit
|
||||
stub:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Stub
|
||||
steps:
|
||||
- name: Stub
|
||||
run: echo "This is a stub job to trigger the workflow."
|
||||
160
.github/workflows/publish-store.yml
vendored
160
.github/workflows/publish-store.yml
vendored
@@ -1,160 +0,0 @@
|
||||
name: Publish to Google Play
|
||||
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
product:
|
||||
description: "Which app is being released."
|
||||
type: choice
|
||||
options:
|
||||
- Password Manager
|
||||
- Authenticator
|
||||
version-name:
|
||||
description: "Version name to promote to production ex 2025.1.1"
|
||||
type: string
|
||||
version-code:
|
||||
description: "Build number to promote to production."
|
||||
required: true
|
||||
type: string
|
||||
rollout-percentage:
|
||||
description: "Percentage of users who will receive this version update."
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 10%
|
||||
- 30%
|
||||
- 50%
|
||||
- 100%
|
||||
default: 10%
|
||||
release-notes:
|
||||
description: "Change notes to be included with this release."
|
||||
type: string
|
||||
default: "Bug fixes."
|
||||
required: true
|
||||
track-from:
|
||||
description: "Track to promote from."
|
||||
type: choice
|
||||
options:
|
||||
- internal
|
||||
- Fastlane Automation Source
|
||||
required: true
|
||||
default: "internal"
|
||||
track-target:
|
||||
description: "Track to promote to."
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- Fastlane Automation Target
|
||||
required: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Promote build to Production in Play Store
|
||||
|
||||
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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/secrets
|
||||
mkdir -p ${{ github.workspace }}/app/src/standardRelease
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
|
||||
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Format Release Notes
|
||||
run: |
|
||||
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
|
||||
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
|
||||
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Promote Play Store version to production
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
VERSION_CODE_INPUT: ${{ inputs.version-code }}
|
||||
VERSION_NAME: ${{inputs.version-name}}
|
||||
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
|
||||
PRODUCT: ${{ inputs.product }}
|
||||
TRACK_FROM: ${{ inputs.track-from }}
|
||||
TRACK_TARGET: ${{ inputs.track-target }}
|
||||
run: |
|
||||
if [ "$PRODUCT" = "Password Manager" ]; then
|
||||
PACKAGE_NAME="com.x8bit.bitwarden"
|
||||
elif [ "$PRODUCT" = "Authenticator" ]; then
|
||||
PACKAGE_NAME="com.bitwarden.authenticator"
|
||||
else
|
||||
echo "Unsupported product: $PRODUCT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
|
||||
|
||||
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
|
||||
|
||||
bundle exec fastlane updateReleaseNotes \
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
versionCode:"$VERSION_CODE" \
|
||||
packageName:"$PACKAGE_NAME"
|
||||
|
||||
bundle exec fastlane promoteToProduction \
|
||||
versionCode:"$VERSION_CODE" \
|
||||
versionName:"$VERSION_NAME" \
|
||||
rolloutPercentage:"$decimal" \
|
||||
packageName:"$PACKAGE_NAME" \
|
||||
releaseNotes:"$RELEASE_NOTES" \
|
||||
track:"$TRACK_FROM" \
|
||||
trackPromoteTo:"$TRACK_TARGET"
|
||||
14
.github/workflows/publish.yml
vendored
Normal file
14
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
name: Promote build to Production in Play Store
|
||||
|
||||
steps:
|
||||
- name: TEST STEP
|
||||
run: exit 0
|
||||
53
.github/workflows/release-branch.yml
vendored
53
.github/workflows/release-branch.yml
vendored
@@ -9,9 +9,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- RC
|
||||
- Hotfix Password Manager
|
||||
- Hotfix Authenticator
|
||||
- Test
|
||||
- Hotfix
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
@@ -19,54 +17,38 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create RC or Test Branch
|
||||
id: rc_branch
|
||||
if: inputs.release_type == 'RC' || inputs.release_type == 'Test'
|
||||
- name: Create RC Branch
|
||||
if: inputs.release_type == 'RC'
|
||||
env:
|
||||
_TEST_MODE: ${{ inputs.release_type == 'Test' }}
|
||||
_RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
RC_PREFIX_DATE: "true" # replace with input if needed
|
||||
run: |
|
||||
current_date=$(date +'%Y.%-m')
|
||||
branch_name="${current_date}-rc${{ github.run_number }}"
|
||||
|
||||
if [ "$_TEST_MODE" = "true" ]; then
|
||||
branch_name="WORKFLOW-TEST-${branch_name}"
|
||||
if [ "$RC_PREFIX_DATE" = "true" ]; then
|
||||
current_date=$(date +'%Y.%m')
|
||||
branch_name="release/${current_date}-rc${{ github.run_number }}"
|
||||
else
|
||||
branch_name="release/rc${{ github.run_number }}"
|
||||
fi
|
||||
branch_name="release/${branch_name}"
|
||||
|
||||
git switch main
|
||||
git switch -c $branch_name
|
||||
git push origin $branch_name
|
||||
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Create Hotfix Branch
|
||||
id: hotfix_branch
|
||||
if: startsWith(inputs.release_type, 'Hotfix')
|
||||
env:
|
||||
_RELEASE_TYPE: ${{ inputs.release_type }}
|
||||
if: inputs.release_type == 'Hotfix'
|
||||
run: |
|
||||
app_codename="bwpm"
|
||||
if [ "$_RELEASE_TYPE" == "Hotfix Authenticator" ]; then
|
||||
app_codename="bwa"
|
||||
fi
|
||||
echo "🌿 app codename: $app_codename"
|
||||
|
||||
latest_tag=$(git tag -l --sort=-creatordate | grep "$app_codename" | head -n 1)
|
||||
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "::error::No tags found in the repository"
|
||||
exit 1
|
||||
fi
|
||||
branch_name="release/hotfix-${latest_tag}"
|
||||
echo "🌿 branch name: $branch_name"
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
|
||||
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
@@ -74,12 +56,3 @@ jobs:
|
||||
git switch -c $branch_name $latest_tag
|
||||
git push origin $branch_name
|
||||
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Trigger CI Workflows
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
_BRANCH_NAME: ${{ steps.rc_branch.outputs.branch_name || steps.hotfix_branch.outputs.branch_name }}
|
||||
run: |
|
||||
echo "🌿 branch name: $_BRANCH_NAME"
|
||||
gh workflow run build.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
|
||||
gh workflow run build-authenticator.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
|
||||
|
||||
61
.github/workflows/scan-ci.yml
vendored
61
.github/workflows/scan-ci.yml
vendored
@@ -6,30 +6,55 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
sast:
|
||||
name: Checkmarx
|
||||
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path .
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Sonar
|
||||
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
|
||||
63
.github/workflows/scan.yml
vendored
63
.github/workflows/scan.yml
vendored
@@ -21,28 +21,63 @@ jobs:
|
||||
contents: read
|
||||
|
||||
sast:
|
||||
name: Checkmarx
|
||||
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
|
||||
quality:
|
||||
name: Sonar
|
||||
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
||||
|
||||
167
.github/workflows/sdlc-sdk-update.yml
vendored
167
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -1,167 +0,0 @@
|
||||
name: SDLC / SDK Update
|
||||
run-name: "SDK ${{inputs.run-mode == 'Update' && format('Update - {0}', inputs.sdk-version) || format('Test #{0} - {1}', inputs.pr-id, inputs.sdk-version)}}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-mode:
|
||||
description: "Run Mode"
|
||||
type: choice
|
||||
options:
|
||||
- Test # used for testing sdk-internal repo PRs
|
||||
- Update # opens a PR in this repo updating the SDK
|
||||
default: Test
|
||||
sdk-package:
|
||||
description: "SDK Package ID"
|
||||
required: true
|
||||
default: "com.bitwarden:sdk-android.dev"
|
||||
sdk-version:
|
||||
description: "SDK Version"
|
||||
required: true
|
||||
default: "1.0.0-2686-km-update-kdf-sdk"
|
||||
pr-id:
|
||||
description: "Pull Request ID"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update and PR
|
||||
if: ${{ inputs.run-mode == 'Update' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Switch to branch
|
||||
id: switch-branch
|
||||
run: |
|
||||
BRANCH_NAME="sdlc/sdk-update"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git switch -c $BRANCH_NAME
|
||||
|
||||
- name: Get current SDK version
|
||||
id: get-current-sdk
|
||||
run: |
|
||||
SDK_VERSION=$(grep "bitwardenSdk =" gradle/libs.versions.toml | cut -d'"' -f2)
|
||||
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
|
||||
echo "Current SDK version: $SDK_VERSION"
|
||||
echo "Current SDK git ref: $GIT_REF"
|
||||
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
run: |
|
||||
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
|
||||
|
||||
- name: Create branch and commit
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
run: |
|
||||
echo "👀 Committing SDK version update..."
|
||||
|
||||
git config user.name "bw-ghapp[bot]"
|
||||
git config user.email "178206702+bw-ghapp[bot]@users.noreply.github.com"
|
||||
|
||||
git add gradle/libs.versions.toml
|
||||
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
|
||||
git push origin $_BRANCH_NAME
|
||||
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
_OLD_SDK_VERSION: ${{ steps.get-current-sdk.outputs.version }}
|
||||
_OLD_SDK_GIT_REF: ${{ steps.get-current-sdk.outputs.git_ref }}
|
||||
run: |
|
||||
NEW_SDK_GIT_REF=$(echo "$_SDK_VERSION" | cut -d'-' -f3-)
|
||||
CHANGELOG=$(./scripts/get-repo-changelog.sh "bitwarden/sdk-internal" "$_OLD_SDK_GIT_REF" "$NEW_SDK_GIT_REF")
|
||||
PR_BODY="Updates the SDK version from \`$_OLD_SDK_VERSION\` to \`$_SDK_PACKAGE $_SDK_VERSION\`
|
||||
|
||||
## What's Changed
|
||||
|
||||
$CHANGELOG"
|
||||
|
||||
# Use echo -e to interpret escape sequences and pipe to gh pr create
|
||||
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
|
||||
--title "Update SDK to $_SDK_VERSION" \
|
||||
--body-file - \
|
||||
--base main \
|
||||
--head $_BRANCH_NAME \
|
||||
--label "automated-pr" \
|
||||
--label "t:ci")
|
||||
|
||||
echo "🚀 Created PR: $PR_URL"
|
||||
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test:
|
||||
name: Test Update
|
||||
if: ${{ inputs.run-mode == 'Test' }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Log inputs to job summary
|
||||
uses: ./.github/actions/log-inputs
|
||||
with:
|
||||
inputs: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Setup Android Build
|
||||
uses: ./.github/actions/setup-android-build
|
||||
|
||||
- name: Update SDK Version
|
||||
env:
|
||||
_SDK_PACKAGE: ${{ inputs.sdk-package }}
|
||||
_SDK_VERSION: ${{ inputs.sdk-version }}
|
||||
run: |
|
||||
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
|
||||
run: |
|
||||
./gradlew assembleDebug --warn
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -27,13 +27,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4 # v1.255.0
|
||||
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
id: upload-to-codecov
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,13 +3,6 @@
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
|
||||
# Ruby / Bundler
|
||||
.bundle/
|
||||
vendor/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.4.2
|
||||
3.3.1
|
||||
|
||||
9
Gemfile
9
Gemfile
@@ -7,12 +7,3 @@ gem 'time'
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
||||
# Since ruby 3.4.0 these are not included in the standard library
|
||||
gem 'abbrev'
|
||||
gem 'logger'
|
||||
gem 'mutex_m'
|
||||
gem 'csv'
|
||||
|
||||
# Starting with Ruby 3.5.0, these are not included in the standard library
|
||||
gem 'ostruct'
|
||||
|
||||
42
Gemfile.lock
42
Gemfile.lock
@@ -5,39 +5,35 @@ GEM
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1139.0)
|
||||
aws-sdk-core (3.228.0)
|
||||
aws-partitions (1.1113.0)
|
||||
aws-sdk-core (3.225.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.109.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (1.104.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.195.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-s3 (1.189.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-sigv4 (1.12.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.2)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
date (3.4.1)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
@@ -62,10 +58,10 @@ GEM
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
@@ -75,7 +71,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.228.0)
|
||||
fastlane (2.227.2)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -169,21 +165,20 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
json (2.12.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.17.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
naturally (2.2.2)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
@@ -235,17 +230,12 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
abbrev
|
||||
csv
|
||||
fastlane
|
||||
fastlane-plugin-firebase_app_distribution
|
||||
logger
|
||||
mutex_m
|
||||
ostruct
|
||||
time
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.2p28
|
||||
ruby 3.3.1p55
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.9
|
||||
2.6.6
|
||||
|
||||
41
README.md
41
README.md
@@ -52,47 +52,6 @@
|
||||
|
||||
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`.
|
||||
|
||||
5. Setup `detekt` pre-commit hook (optional):
|
||||
|
||||
Run the following script from the root of the repository to install the hook. This will overwrite any existing pre-commit hook if present.
|
||||
|
||||
```shell
|
||||
echo "Writing detekt pre-commit hook..."
|
||||
cat << 'EOL' > .git/hooks/pre-commit
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Running detekt check..."
|
||||
OUTPUT="/tmp/detekt-$(date +%s)"
|
||||
./gradlew -Pprecommit=true detekt > $OUTPUT
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
cat $OUTPUT
|
||||
rm $OUTPUT
|
||||
echo "***********************************************"
|
||||
echo " detekt failed "
|
||||
echo " Please fix the above issues before committing "
|
||||
echo "***********************************************"
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
rm $OUTPUT
|
||||
EOL
|
||||
echo "detekt pre-commit hook written to .git/hooks/pre-commit"
|
||||
echo "Making the hook executable"
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
echo "detekt pre-commit hook installed successfully to .git/hooks/pre-commit"
|
||||
```
|
||||
|
||||
## Theme
|
||||
|
||||
### Icons & Illustrations
|
||||
|
||||
@@ -37,6 +37,6 @@ android {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ 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)
|
||||
@@ -47,32 +46,26 @@ 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()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = libs.versions.appVersionCode.get().toInt()
|
||||
versionName = libs.versions.appVersionName.get()
|
||||
versionCode = 1
|
||||
versionName = "2025.4.0"
|
||||
|
||||
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"
|
||||
|
||||
// Set the base archive name for publishing purposes. This is used to derive the APK and AAB
|
||||
// artifact names when uploading to Firebase and Play Store.
|
||||
base.archivesName = "com.x8bit.bitwarden"
|
||||
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "CI_INFO",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"\uD83D\uDCBB local\"")}",
|
||||
)
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "SDK_VERSION",
|
||||
value = "\"${libs.versions.bitwardenSdk.get()}\"",
|
||||
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,7 +99,6 @@ android {
|
||||
applicationIdSuffix = ".beta"
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
matchingFallbacks += listOf("release")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
@@ -119,7 +111,6 @@ android {
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
@@ -202,7 +193,7 @@ android {
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +255,11 @@ dependencies {
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(platform(libs.square.okhttp.bom))
|
||||
implementation(libs.square.okhttp)
|
||||
implementation(libs.square.okhttp.logging)
|
||||
implementation(platform(libs.square.retrofit.bom))
|
||||
implementation(libs.square.retrofit)
|
||||
implementation(libs.square.retrofit.kotlinx.serialization)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.zxing.zxing.core)
|
||||
|
||||
@@ -295,6 +287,7 @@ dependencies {
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.mockk.mockk)
|
||||
testImplementation(libs.robolectric.robolectric)
|
||||
testImplementation(libs.square.okhttp.mockwebserver)
|
||||
testImplementation(libs.square.turbine)
|
||||
}
|
||||
|
||||
@@ -303,7 +296,8 @@ tasks {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "2g"
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors()
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
|
||||
android.sourceSets["main"].res.srcDirs("src/test/res")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +322,6 @@ private fun renameFile(path: String, newName: String) {
|
||||
if (originalFile.renameTo(newFile)) {
|
||||
println("Renamed $originalFile to $newFile")
|
||||
} else {
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
throw RuntimeException("Failed to rename $originalFile to $newFile")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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,20 +7,6 @@
|
||||
<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>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<base-config
|
||||
cleartextTrafficPermitted="true"
|
||||
tools:ignore="InsecureBaseConfiguration">
|
||||
<trust-anchors>
|
||||
<!-- Trust pre-installed CAs -->
|
||||
<certificates src="system" />
|
||||
<!-- Additionally trust user added CAs -->
|
||||
<certificates
|
||||
src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain includeSubdomains="true">bitwarden.com</domain>
|
||||
<domain includeSubdomains="true">bitwarden.eu</domain>
|
||||
<domain includeSubdomains="true">bitwarden.pw</domain>
|
||||
<trust-anchors>
|
||||
<!-- Only trust pre-installed CAs for Bitwarden domains and all subdomains -->
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
|
||||
</network-security-config>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider>
|
||||
<capabilities>
|
||||
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
@@ -81,12 +81,12 @@
|
||||
<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>
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -115,11 +115,11 @@
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".AutofillCallbackActivity"
|
||||
android:name=".AutofillTotpCopyActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/AutofillCallbackTheme" />
|
||||
android:theme="@style/AutofillTotpCopyTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".AuthCallbackActivity"
|
||||
@@ -133,6 +133,16 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="captcha-callback"
|
||||
android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="duo-callback"
|
||||
android:scheme="bitwarden" />
|
||||
@@ -320,19 +330,11 @@
|
||||
<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,42 +779,6 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
|
||||
/**
|
||||
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
|
||||
@@ -14,16 +11,7 @@ import com.bitwarden.ui.platform.util.validate
|
||||
@OmitFromCoverage
|
||||
class AccessibilityActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
@@ -23,7 +21,6 @@ class AuthCallbackActivity : AppCompatActivity() {
|
||||
private val viewModel: AuthCallbackViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = intent))
|
||||
@@ -38,12 +35,4 @@ class AuthCallbackActivity : AppCompatActivity() {
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@@ -26,6 +27,7 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
|
||||
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
|
||||
val webAuthResult = action.intent.getWebAuthResultOrNull()
|
||||
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
||||
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
|
||||
val ssoCallbackResult = action.intent.getSsoCallbackResult()
|
||||
when {
|
||||
@@ -33,6 +35,12 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
|
||||
}
|
||||
|
||||
captchaCallbackTokenResult != null -> {
|
||||
authRepository.setCaptchaCallbackTokenResult(
|
||||
tokenResult = captchaCallbackTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
duoCallbackTokenResult != null -> {
|
||||
authRepository.setDuoCallbackTokenResult(
|
||||
tokenResult = duoCallbackTokenResult,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -15,43 +12,43 @@ import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* An activity that is launched to complete Autofill. This is done when an autofill item is selected
|
||||
* and is associated with a valid cipher. Due to the constraints of the autofill framework, we also
|
||||
* have to re-fulfill the autofill for the views that are being filled.
|
||||
* An activity for copying a TOTP code to the clipboard. This is done when an autofill item is
|
||||
* selected and it requires TOTP authentication. Due to the constraints of the autofill framework,
|
||||
* we also have to re-fulfill the autofill for the views that are being filled.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@AndroidEntryPoint
|
||||
class AutofillCallbackActivity : AppCompatActivity() {
|
||||
class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
private val viewModel: AutofillCallbackViewModel by viewModels()
|
||||
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
viewModel.trySendAction(AutofillCallbackAction.IntentReceived(intent = intent))
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent.validate())
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
super.onNewIntent(intent.validate(), caller)
|
||||
autofillTotpCopyViewModel.trySendAction(
|
||||
AutofillTotpCopyAction.IntentReceived(
|
||||
intent = intent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
viewModel
|
||||
autofillTotpCopyViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is AutofillCallbackEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
is AutofillCallbackEvent.FinishActivity -> finishActivity()
|
||||
is AutofillTotpCopyEvent.CompleteAutofill -> {
|
||||
handleCompleteAutofill(event)
|
||||
}
|
||||
|
||||
is AutofillTotpCopyEvent.FinishActivity -> {
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
@@ -60,7 +57,7 @@ class AutofillCallbackActivity : AppCompatActivity() {
|
||||
/**
|
||||
* Complete autofill with the provided data.
|
||||
*/
|
||||
private fun handleCompleteAutofill(event: AutofillCallbackEvent.CompleteAutofill) {
|
||||
private fun handleCompleteAutofill(event: AutofillTotpCopyEvent.CompleteAutofill) {
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
@@ -5,15 +5,14 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillCallbackIntentOrNull
|
||||
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 timber.log.Timber
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -22,65 +21,53 @@ import javax.inject.Inject
|
||||
private const val CIPHER_WAIT_TIMEOUT_MILLIS: Long = 500
|
||||
|
||||
/**
|
||||
* A view model that handles logic for the [AutofillCallbackActivity].
|
||||
* A view model that handles logic for the [AutofillTotpCopyActivity].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AutofillCallbackViewModel @Inject constructor(
|
||||
class AutofillTotpCopyViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<Unit, AutofillCallbackEvent, AutofillCallbackAction>(Unit) {
|
||||
) : BaseViewModel<Unit, AutofillTotpCopyEvent, AutofillTotpCopyAction>(Unit) {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
override fun handleAction(action: AutofillCallbackAction): Unit = when (action) {
|
||||
is AutofillCallbackAction.IntentReceived -> handleIntentReceived(action)
|
||||
override fun handleAction(action: AutofillTotpCopyAction): Unit = when (action) {
|
||||
is AutofillTotpCopyAction.IntentReceived -> handleIntentReceived(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the received intent and alert the activity of what to do next.
|
||||
*/
|
||||
private fun handleIntentReceived(action: AutofillCallbackAction.IntentReceived) {
|
||||
private fun handleIntentReceived(action: AutofillTotpCopyAction.IntentReceived) {
|
||||
viewModelScope
|
||||
.launchWithTimeout(
|
||||
timeoutBlock = {
|
||||
Timber.w("Autofill -- Timeout")
|
||||
finishActivity()
|
||||
},
|
||||
timeoutBlock = { finishActivity() },
|
||||
timeoutDuration = CIPHER_WAIT_TIMEOUT_MILLIS,
|
||||
) {
|
||||
// Extract TOTP copy data from the intent.
|
||||
val cipherId = action
|
||||
.intent
|
||||
.getAutofillCallbackIntentOrNull()
|
||||
.getTotpCopyIntentOrNull()
|
||||
?.cipherId
|
||||
|
||||
if (cipherId == null) {
|
||||
Timber.w("Autofill -- Cipher was not provided")
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
if (isVaultLocked()) {
|
||||
Timber.w("Autofill -- Vault is locked")
|
||||
if (cipherId == null || isVaultLocked()) {
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
|
||||
// Try and find the matching cipher.
|
||||
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.w("Autofill -- Cipher not found")
|
||||
finishActivity()
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.mapNotNull { it.data }
|
||||
.first()
|
||||
.find { it.id == cipherId }
|
||||
?.let { cipherView ->
|
||||
sendEvent(
|
||||
AutofillTotpCopyEvent.CompleteAutofill(
|
||||
cipherView = cipherView,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.w(result.error, "Autofill -- Get cipher failure")
|
||||
finishActivity()
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> {
|
||||
Timber.d("Autofill -- Cipher found")
|
||||
sendEvent(AutofillCallbackEvent.CompleteAutofill(result.cipherView))
|
||||
}
|
||||
}
|
||||
?: finishActivity()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +75,7 @@ class AutofillCallbackViewModel @Inject constructor(
|
||||
* Send an event to the activity that signals it to finish.
|
||||
*/
|
||||
private fun finishActivity() {
|
||||
sendEvent(AutofillCallbackEvent.FinishActivity)
|
||||
sendEvent(AutofillTotpCopyEvent.FinishActivity)
|
||||
}
|
||||
|
||||
private suspend fun isVaultLocked(): Boolean {
|
||||
@@ -105,30 +92,30 @@ class AutofillCallbackViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions that can be sent to the [AutofillCallbackViewModel].
|
||||
* Represents actions that can be sent to the [AutofillTotpCopyViewModel].
|
||||
*/
|
||||
sealed class AutofillCallbackAction {
|
||||
sealed class AutofillTotpCopyAction {
|
||||
/**
|
||||
* An [intent] has been received and is ready to be processed.
|
||||
*/
|
||||
data class IntentReceived(
|
||||
val intent: Intent,
|
||||
) : AutofillCallbackAction()
|
||||
) : AutofillTotpCopyAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events emitted by the [AutofillCallbackViewModel].
|
||||
* Represents events emitted by the [AutofillTotpCopyViewModel].
|
||||
*/
|
||||
sealed class AutofillCallbackEvent {
|
||||
sealed class AutofillTotpCopyEvent {
|
||||
/**
|
||||
* Complete autofill with the provided [cipherView].
|
||||
*/
|
||||
data class CompleteAutofill(
|
||||
val cipherView: CipherView,
|
||||
) : AutofillCallbackEvent()
|
||||
) : AutofillTotpCopyEvent()
|
||||
|
||||
/**
|
||||
* Finish the activity.
|
||||
*/
|
||||
data object FinishActivity : AutofillCallbackEvent()
|
||||
data object FinishActivity : AutofillTotpCopyEvent()
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -39,17 +38,6 @@ class BitwardenApplication : Application() {
|
||||
@Inject
|
||||
lateinit var restrictionManager: RestrictionManager
|
||||
|
||||
@Inject
|
||||
lateinit var environmentRepository: EnvironmentRepository
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// These must be initialized in order to ensure that the restrictionManager does not
|
||||
// override the environmentRepository values.
|
||||
restrictionManager.initialize()
|
||||
environmentRepository.initialize()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Timber.w("onLowMemory")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.ComponentCaller
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -14,7 +14,6 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -24,7 +23,6 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.platform.util.setupEdgeToEdge
|
||||
import com.bitwarden.ui.platform.util.validate
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
@@ -43,8 +41,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ANDROID_15_BUG_MAX_REVISION: Int = 241007
|
||||
|
||||
/**
|
||||
* Primary entry point for the application.
|
||||
*/
|
||||
@@ -71,11 +67,10 @@ class MainActivity : AppCompatActivity() {
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
window.decorView.filterTouchesWhenObscured = true
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = intent))
|
||||
}
|
||||
@@ -119,15 +114,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
val newIntent = intent.validate()
|
||||
super.onNewIntent(newIntent)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent))
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
|
||||
val newIntent = intent.validate()
|
||||
super.onNewIntent(newIntent, caller)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent))
|
||||
super.onNewIntent(intent)
|
||||
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = intent))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -185,6 +173,12 @@ class MainActivity : AppCompatActivity() {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(baseContext, event.message.invoke(resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
is MainEvent.UpdateAppLocale -> {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(event.localeName),
|
||||
@@ -217,35 +211,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun handleRecreate() {
|
||||
val isOldAndroidBuildRevision = {
|
||||
// This fetches the date portion of the ID in order to determine the revision of
|
||||
// Android 15 being used and whether we want to use the `recreate` API or not.
|
||||
// If we fail to parse a date, we assume it is not an old revision.
|
||||
"\\.([^.]+)\\."
|
||||
.toRegex()
|
||||
.find(Build.ID)
|
||||
?.groups
|
||||
?.get(1)
|
||||
?.value
|
||||
?.toIntOrNull()
|
||||
?.let { it <= ANDROID_15_BUG_MAX_REVISION } == true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM &&
|
||||
isOldAndroidBuildRevision()
|
||||
) {
|
||||
// This is done to avoid a bug in specific older revisions of Android 15. The bug has
|
||||
// been fixed but certain phones that are no longer supported will never get the fix.
|
||||
// The OS bug is tracked here: https://issuetracker.google.com/issues/370180732
|
||||
startActivity(
|
||||
Intent
|
||||
.makeMainActivity(componentName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION),
|
||||
)
|
||||
finish()
|
||||
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
|
||||
} else {
|
||||
ActivityCompat.recreate(this)
|
||||
}
|
||||
recreate()
|
||||
}
|
||||
|
||||
private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) {
|
||||
|
||||
@@ -4,11 +4,10 @@ import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -23,12 +22,13 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
import com.x8bit.bitwarden.data.credentials.util.getCreateCredentialRequestOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.util.getFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.util.getGetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.credentials.util.getProviderGetPasswordRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -36,6 +36,7 @@ import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticato
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
@@ -43,15 +44,12 @@ import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -60,17 +58,17 @@ import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
|
||||
private const val ANIMATION_REFRESH_DELAY = 500L
|
||||
|
||||
/**
|
||||
* A view model that helps launch actions for the [MainActivity].
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
@@ -83,11 +81,13 @@ class MainViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val appResumeManager: AppResumeManager,
|
||||
private val clock: Clock,
|
||||
private val toastManager: ToastManager,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
|
||||
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
|
||||
key = FlagKey.MobileErrorReporting,
|
||||
),
|
||||
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
|
||||
),
|
||||
) {
|
||||
@@ -106,6 +106,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager
|
||||
.getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
|
||||
.map { MainAction.Internal.OnMobileErrorReportingReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
accessibilitySelectionManager
|
||||
.accessibilitySelectionFlow
|
||||
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
|
||||
@@ -139,23 +145,36 @@ class MainViewModel @Inject constructor(
|
||||
.onEach(::trySendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
merge(
|
||||
authRepository
|
||||
.userStateFlow
|
||||
.drop(count = 1)
|
||||
// Trigger an action whenever the current user changes or we go into/out of a
|
||||
// pending account state (which acts like switching to a temporary user).
|
||||
.map { it?.activeUserId to it?.hasPendingAccountAddition }
|
||||
.distinctUntilChanged(),
|
||||
vaultRepository
|
||||
.vaultStateEventFlow
|
||||
.filter { it is VaultStateEvent.Locked },
|
||||
)
|
||||
// This debounce ensure we do not emit multiple times rapidly and also acts as a short
|
||||
// delay to give animations time to finish (ex: account switcher).
|
||||
.debounce(timeoutMillis = ANIMATION_DEBOUNCE_DELAY_MS)
|
||||
.map { MainAction.Internal.CurrentUserOrVaultStateChange }
|
||||
.onEach(::sendAction)
|
||||
authRepository
|
||||
.userStateFlow
|
||||
.drop(count = 1)
|
||||
// Trigger an action whenever the current user changes or we go into/out of a pending
|
||||
// account state (which acts like switching to a temporary user).
|
||||
.map { it?.activeUserId to it?.hasPendingAccountAddition }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
// Switching between account states often involves some kind of animation (ex:
|
||||
// account switcher) that we might want to give time to finish before triggering
|
||||
// a refresh.
|
||||
delay(ANIMATION_REFRESH_DELAY)
|
||||
trySendAction(MainAction.Internal.CurrentUserStateChange)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
vaultRepository
|
||||
.vaultStateEventFlow
|
||||
.onEach {
|
||||
when (it) {
|
||||
is VaultStateEvent.Locked -> {
|
||||
// Similar to account switching, triggering this action too soon can
|
||||
// interfere with animations or navigation logic, so we will delay slightly.
|
||||
delay(ANIMATION_REFRESH_DELAY)
|
||||
trySendAction(MainAction.Internal.VaultUnlockStateChange)
|
||||
}
|
||||
|
||||
is VaultStateEvent.Unlocked -> Unit
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// On app launch, mark all active users as having previously logged in.
|
||||
@@ -193,13 +212,22 @@ class MainViewModel @Inject constructor(
|
||||
handleAutofillSelectionReceive(action)
|
||||
}
|
||||
|
||||
is MainAction.Internal.CurrentUserOrVaultStateChange -> {
|
||||
handleCurrentUserOrVaultStateChange()
|
||||
}
|
||||
|
||||
is MainAction.Internal.CurrentUserStateChange -> handleCurrentUserStateChange()
|
||||
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
|
||||
is MainAction.Internal.OnMobileErrorReportingReceive -> {
|
||||
handleOnMobileErrorReportingReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnMobileErrorReportingReceive(
|
||||
action: MainAction.Internal.OnMobileErrorReportingReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +260,8 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
private fun handleCurrentUserOrVaultStateChange() {
|
||||
sendEvent(MainEvent.Recreate)
|
||||
garbageCollectionManager.tryCollect()
|
||||
private fun handleCurrentUserStateChange() {
|
||||
recreateUiAndGarbageCollect()
|
||||
}
|
||||
|
||||
private fun handleScreenCaptureUpdate(action: MainAction.Internal.ScreenCaptureUpdate) {
|
||||
@@ -246,6 +273,10 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
|
||||
}
|
||||
|
||||
private fun handleVaultUnlockStateChange() {
|
||||
recreateUiAndGarbageCollect()
|
||||
}
|
||||
|
||||
private fun handleDynamicColorsUpdate(action: MainAction.Internal.DynamicColorsUpdate) {
|
||||
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
|
||||
}
|
||||
@@ -294,7 +325,6 @@ class MainViewModel @Inject constructor(
|
||||
val createCredentialRequest = intent.getCreateCredentialRequestOrNull()
|
||||
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
|
||||
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
@@ -385,19 +415,6 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
providerGetPasswordRequest != null -> {
|
||||
// Set the user's verification status when a new GetPassword request is
|
||||
// received to force explicit verification if the user's vault is
|
||||
// unlocked when the request is received.
|
||||
bitwardenCredentialManager.isUserVerified =
|
||||
providerGetPasswordRequest.isUserPreVerified
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderGetPasswordRequest(
|
||||
passwordGetRequest = providerGetPasswordRequest,
|
||||
)
|
||||
}
|
||||
|
||||
getCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderGetCredentials(
|
||||
@@ -421,6 +438,11 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun recreateUiAndGarbageCollect() {
|
||||
sendEvent(MainEvent.Recreate)
|
||||
garbageCollectionManager.tryCollect()
|
||||
}
|
||||
|
||||
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
|
||||
viewModelScope.launch {
|
||||
// Attempt to load the environment for the user if they have a pre-auth environment
|
||||
@@ -433,15 +455,15 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
when (emailTokenResult) {
|
||||
is EmailTokenResult.Error -> {
|
||||
emailTokenResult
|
||||
.message
|
||||
?.let { toastManager.show(message = it) }
|
||||
?: run {
|
||||
toastManager.show(
|
||||
messageId = BitwardenString
|
||||
.there_was_an_issue_validating_the_registration_token,
|
||||
)
|
||||
}
|
||||
sendEvent(
|
||||
MainEvent.ShowToast(
|
||||
message = emailTokenResult
|
||||
.message
|
||||
?.asText()
|
||||
?: R.string.there_was_an_issue_validating_the_registration_token
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
EmailTokenResult.Expired -> {
|
||||
@@ -473,12 +495,15 @@ data class MainState(
|
||||
val theme: AppTheme,
|
||||
val isScreenCaptureAllowed: Boolean,
|
||||
val isDynamicColorsEnabled: Boolean,
|
||||
private val isErrorReportingDialogEnabled: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Contains all feature flags that are available to the UI.
|
||||
*/
|
||||
val featureFlagsState: FeatureFlagsState
|
||||
get() = FeatureFlagsState
|
||||
get() = FeatureFlagsState(
|
||||
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,6 +548,13 @@ sealed class MainAction {
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the Mobile Error Reporting feature flag has been updated.
|
||||
*/
|
||||
data class OnMobileErrorReportingReceive(
|
||||
val isErrorReportingEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
@@ -531,9 +563,9 @@ sealed class MainAction {
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a relevant change in the current user state or vault locked state.
|
||||
* Indicates a relevant change in the current user state.
|
||||
*/
|
||||
data object CurrentUserOrVaultStateChange : Internal()
|
||||
data object CurrentUserStateChange : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the screen capture state has changed.
|
||||
@@ -549,6 +581,11 @@ sealed class MainAction {
|
||||
val theme: AppTheme,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a relevant change in the current vault lock state.
|
||||
*/
|
||||
data object VaultUnlockStateChange : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the dynamic colors state has changed.
|
||||
*/
|
||||
@@ -584,6 +621,11 @@ sealed class MainEvent {
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
|
||||
/**
|
||||
* Indicates that the app language has been updated.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.bitwarden.network.model.AccountKeysJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.provider.AppIdProvider
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
@@ -127,34 +126,13 @@ interface AuthDiskSource : AppIdProvider {
|
||||
/**
|
||||
* Retrieves a private key using a [userId].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Use getAccountKeys instead.",
|
||||
replaceWith = ReplaceWith("getAccountKeys"),
|
||||
)
|
||||
fun getPrivateKey(userId: String): String?
|
||||
|
||||
/**
|
||||
* Stores a private key using a [userId].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Use storeAccountKeys instead.",
|
||||
replaceWith = ReplaceWith("storeAccountKeys"),
|
||||
)
|
||||
fun storePrivateKey(userId: String, privateKey: String?)
|
||||
|
||||
/**
|
||||
* Returns the profile account keys for the given [userId].
|
||||
*/
|
||||
fun getAccountKeys(userId: String): AccountKeysJson?
|
||||
|
||||
/**
|
||||
* Stores the profile account keys for the given [userId].
|
||||
*/
|
||||
fun storeAccountKeys(
|
||||
userId: String,
|
||||
accountKeys: AccountKeysJson?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves a user auto-unlock key for the given [userId].
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.SharedPreferences
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
|
||||
import com.bitwarden.network.model.AccountKeysJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
@@ -49,7 +48,6 @@ private const val USES_KEY_CONNECTOR = "usesKeyConnector"
|
||||
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
|
||||
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
|
||||
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
|
||||
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -144,7 +142,6 @@ class AuthDiskSourceImpl(
|
||||
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
|
||||
storeEncryptedPin(userId = userId, encryptedPin = null)
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeAccountKeys(userId = userId, accountKeys = null)
|
||||
storeOrganizationKeys(userId = userId, organizationKeys = null)
|
||||
storeOrganizations(userId = userId, organizations = null)
|
||||
storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
@@ -231,11 +228,9 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use getAccountKeys instead.", replaceWith = ReplaceWith("getAccountKeys"))
|
||||
override fun getPrivateKey(userId: String): String? =
|
||||
getString(key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId))
|
||||
|
||||
@Deprecated("Use storeAccountKeys instead.", replaceWith = ReplaceWith("storeAccountKeys"))
|
||||
override fun storePrivateKey(userId: String, privateKey: String?) {
|
||||
putString(
|
||||
key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId),
|
||||
@@ -243,20 +238,6 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAccountKeys(userId: String): AccountKeysJson? =
|
||||
getEncryptedString(key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId))
|
||||
?.let { json.decodeFromStringOrNull(it) }
|
||||
|
||||
override fun storeAccountKeys(
|
||||
userId: String,
|
||||
accountKeys: AccountKeysJson?,
|
||||
) {
|
||||
putEncryptedString(
|
||||
key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId),
|
||||
value = accountKeys?.let { json.encodeToString(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun getUserAutoUnlockKey(userId: String): String? =
|
||||
getEncryptedString(
|
||||
key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId),
|
||||
|
||||
@@ -2,14 +2,12 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Container for the user's API tokens.
|
||||
*
|
||||
* @property accessToken The user's primary access token.
|
||||
* @property refreshToken The user's refresh token.
|
||||
* @property expiresAtSec The time at which the token expires in epoch seconds.
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountTokensJson(
|
||||
@@ -18,9 +16,6 @@ data class AccountTokensJson(
|
||||
|
||||
@SerialName("refreshToken")
|
||||
val refreshToken: String?,
|
||||
|
||||
@SerialName("expiresAtSec")
|
||||
val expiresAtSec: Long = Instant.MAX.epochSecond,
|
||||
) {
|
||||
/**
|
||||
* Returns `true` if the user is logged in, `false otherwise.
|
||||
|
||||
@@ -8,12 +8,11 @@ import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
|
||||
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -49,14 +48,14 @@ class AuthRequestNotificationManagerImpl(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
.setName(context.getString(BitwardenString.pending_log_in_requests))
|
||||
.setName(context.getString(R.string.pending_log_in_requests))
|
||||
.build(),
|
||||
)
|
||||
if (!notificationManager.areNotificationsEnabled(NOTIFICATION_CHANNEL_ID)) return
|
||||
// Create the notification
|
||||
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentIntent(createContentIntent(data))
|
||||
.setContentTitle(context.getString(BitwardenString.log_in_requested))
|
||||
.setContentTitle(context.getString(R.string.log_in_requested))
|
||||
.setContentText(
|
||||
authDiskSource
|
||||
.userState
|
||||
@@ -64,10 +63,10 @@ class AuthRequestNotificationManagerImpl(
|
||||
?.get(data.userId)
|
||||
?.profile
|
||||
?.email
|
||||
?.let { context.getString(BitwardenString.confim_log_in_attemp_for_x, it) }
|
||||
?: context.getString(BitwardenString.confirm_log_in),
|
||||
?.let { context.getString(R.string.confim_log_in_attemp_for_x, it) }
|
||||
?: context.getString(R.string.confirm_log_in),
|
||||
)
|
||||
.setSmallIcon(BitwardenDrawable.ic_notification)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setColor(Color.White.value.toInt())
|
||||
.setAutoCancel(true)
|
||||
.setTimeoutAfter(NOTIFICATION_DEFAULT_TIMEOUT_MILLIS)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.network.model.AuthTokenData
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
|
||||
/**
|
||||
@@ -10,19 +9,9 @@ class AuthTokenManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
) : AuthTokenManager {
|
||||
|
||||
override fun getAuthTokenDataOrNull(): AuthTokenData? = authDiskSource
|
||||
override fun getActiveAccessTokenOrNull(): String? = authDiskSource
|
||||
.userState
|
||||
?.activeUserId
|
||||
?.let { userId ->
|
||||
authDiskSource
|
||||
.getAccountTokens(userId = userId)
|
||||
?.takeIf { it.accessToken != null }
|
||||
?.let {
|
||||
AuthTokenData(
|
||||
userId = userId,
|
||||
accessToken = requireNotNull(it.accessToken),
|
||||
expiresAtSec = it.expiresAtSec,
|
||||
)
|
||||
}
|
||||
}
|
||||
?.let { authDiskSource.getAccountTokens(it) }
|
||||
?.accessToken
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
@@ -26,15 +27,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,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
) : UserLogoutManager {
|
||||
private val scope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
@@ -48,7 +49,7 @@ class UserLogoutManagerImpl(
|
||||
Timber.d("logout reason=$reason")
|
||||
val isExpired = reason == LogoutReason.SecurityStamp
|
||||
if (isExpired) {
|
||||
showToast(message = BitwardenString.login_expired)
|
||||
showToast(message = R.string.login_expired)
|
||||
}
|
||||
|
||||
val ableToSwitchToNewAccount = switchUserIfAvailable(
|
||||
@@ -70,7 +71,7 @@ class UserLogoutManagerImpl(
|
||||
Timber.d("softLogout reason=$reason")
|
||||
val isExpired = reason == LogoutReason.SecurityStamp
|
||||
if (isExpired) {
|
||||
showToast(message = BitwardenString.login_expired)
|
||||
showToast(message = R.string.login_expired)
|
||||
}
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
@@ -80,7 +81,6 @@ class UserLogoutManagerImpl(
|
||||
// Save any data that will still need to be retained after otherwise clearing all dat
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
|
||||
|
||||
switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
@@ -102,10 +102,6 @@ class UserLogoutManagerImpl(
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
authDiskSource.storePinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearData(userId: String) {
|
||||
@@ -121,7 +117,7 @@ class UserLogoutManagerImpl(
|
||||
}
|
||||
|
||||
private fun showToast(@StringRes message: Int) {
|
||||
mainScope.launch { toastManager.show(messageId = message) }
|
||||
mainScope.launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
private fun switchUserIfAvailable(
|
||||
@@ -140,7 +136,7 @@ class UserLogoutManagerImpl(
|
||||
// Check if there is a new active user
|
||||
return if (updatedAccounts.isNotEmpty()) {
|
||||
if (currentUserId == currentUserState.activeUserId && !isExpired) {
|
||||
showToast(message = BitwardenString.account_switched_automatically)
|
||||
showToast(message = R.string.account_switched_automatically)
|
||||
}
|
||||
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
@@ -108,23 +107,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,
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
@@ -55,6 +56,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Flow of the current [CaptchaCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setCaptchaCallbackTokenResult] is called.
|
||||
*/
|
||||
val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult>
|
||||
|
||||
/**
|
||||
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||
@@ -179,6 +186,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@@ -193,6 +201,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
asymmetricalKey: String,
|
||||
requestPrivateKey: String,
|
||||
masterPasswordHash: String?,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@@ -204,6 +213,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
@@ -216,6 +226,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
organizationIdentifier: String,
|
||||
): LoginResult
|
||||
|
||||
@@ -228,6 +239,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
@@ -282,6 +294,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String? = null,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
): RegisterResult
|
||||
@@ -319,6 +332,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
passwordHint: String?,
|
||||
): SetPasswordResult
|
||||
|
||||
/**
|
||||
* Set the value of [captchaTokenResultFlow].
|
||||
*/
|
||||
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
|
||||
|
||||
/**
|
||||
* Set the value of [duoTokenResultFlow].
|
||||
*/
|
||||
|
||||
@@ -87,6 +87,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
@@ -145,7 +146,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -154,7 +154,6 @@ import javax.inject.Singleton
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
@Singleton
|
||||
class AuthRepositoryImpl(
|
||||
private val clock: Clock,
|
||||
private val accountsService: AccountsService,
|
||||
private val devicesService: DevicesService,
|
||||
private val haveIBeenPwnedService: HaveIBeenPwnedService,
|
||||
@@ -330,6 +329,10 @@ class AuthRepositoryImpl(
|
||||
),
|
||||
)
|
||||
|
||||
private val captchaTokenChannel = Channel<CaptchaCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
|
||||
captchaTokenChannel.receiveAsFlow()
|
||||
|
||||
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
|
||||
|
||||
@@ -401,7 +404,10 @@ class AuthRepositoryImpl(
|
||||
.onEach {
|
||||
val userId = activeUserId ?: return@onEach
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronously(userId = userId)
|
||||
refreshAccessTokenSynchronouslyInternal(
|
||||
userId = userId,
|
||||
logOutOnFailure = false,
|
||||
)
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
|
||||
@@ -558,10 +564,6 @@ class AuthRepositoryImpl(
|
||||
.map { keys }
|
||||
}
|
||||
.onSuccess { keys ->
|
||||
// TDE and SSO user creation still uses crypto-v1. These users are not
|
||||
// expected to have the AEAD keys so we only store the private key for now.
|
||||
// See https://github.com/bitwarden/android/pull/5682#discussion_r2273940332
|
||||
// for more details.
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = keys.privateKey,
|
||||
@@ -590,15 +592,11 @@ class AuthRepositoryImpl(
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
|
||||
val userId = profile.userId
|
||||
val accountKeys = authDiskSource.getAccountKeys(userId = userId)
|
||||
val privateKey = accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey
|
||||
?: authDiskSource.getPrivateKey(userId = userId)
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userId)
|
||||
?: return LoginResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Private Key"),
|
||||
)
|
||||
val signingKey = accountKeys?.signatureKeyPair?.wrappedSigningKey
|
||||
val securityState = accountKeys?.securityState?.securityState
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { error ->
|
||||
@@ -608,8 +606,6 @@ class AuthRepositoryImpl(
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
signingKey = signingKey,
|
||||
securityState = securityState,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
@@ -624,6 +620,7 @@ class AuthRepositoryImpl(
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult = identityService
|
||||
.preLogin(email = email)
|
||||
.flatMap {
|
||||
@@ -642,6 +639,7 @@ class AuthRepositoryImpl(
|
||||
username = email,
|
||||
password = passwordHash,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
@@ -661,6 +659,7 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey: String,
|
||||
requestPrivateKey: String,
|
||||
masterPasswordHash: String?,
|
||||
captchaToken: String?,
|
||||
): LoginResult =
|
||||
loginCommon(
|
||||
email = email,
|
||||
@@ -675,12 +674,14 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey = asymmetricalKey,
|
||||
privateKey = requestPrivateKey,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
@@ -689,6 +690,7 @@ class AuthRepositoryImpl(
|
||||
password = password,
|
||||
authModel = it,
|
||||
twoFactorData = twoFactorData,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
@@ -702,6 +704,7 @@ class AuthRepositoryImpl(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
@@ -710,6 +713,7 @@ class AuthRepositoryImpl(
|
||||
password = password,
|
||||
authModel = it,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
@@ -743,6 +747,7 @@ class AuthRepositoryImpl(
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
organizationIdentifier: String,
|
||||
): LoginResult = loginCommon(
|
||||
email = email,
|
||||
@@ -751,62 +756,15 @@ class AuthRepositoryImpl(
|
||||
ssoCodeVerifier = ssoCodeVerifier,
|
||||
ssoRedirectUri = ssoRedirectUri,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
orgIdentifier = organizationIdentifier,
|
||||
)
|
||||
|
||||
override fun refreshAccessTokenSynchronously(
|
||||
userId: String,
|
||||
): Result<String> {
|
||||
val refreshToken = authDiskSource
|
||||
.getAccountTokens(userId = userId)
|
||||
?.refreshToken
|
||||
?: return IllegalStateException("Must be logged in.").asFailure()
|
||||
return identityService
|
||||
.refreshTokenSynchronously(refreshToken)
|
||||
.flatMap { refreshTokenResponse ->
|
||||
// Check to make sure the user is still logged in after making the request
|
||||
authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(userId)
|
||||
?.let { refreshTokenResponse.asSuccess() }
|
||||
?: IllegalStateException("Must be logged in.").asFailure()
|
||||
}
|
||||
.flatMap { refreshTokenResponse ->
|
||||
when (refreshTokenResponse) {
|
||||
is RefreshTokenResponseJson.Error -> {
|
||||
if (refreshTokenResponse.isInvalidGrant) {
|
||||
logout(userId = userId, reason = LogoutReason.InvalidGrant)
|
||||
}
|
||||
IllegalStateException(refreshTokenResponse.error).asFailure()
|
||||
}
|
||||
|
||||
is RefreshTokenResponseJson.Forbidden -> {
|
||||
logout(userId = userId, reason = LogoutReason.RefreshForbidden)
|
||||
refreshTokenResponse.error.asFailure()
|
||||
}
|
||||
|
||||
is RefreshTokenResponseJson.Unauthorized -> {
|
||||
logout(userId = userId, reason = LogoutReason.RefreshUnauthorized)
|
||||
refreshTokenResponse.error.asFailure()
|
||||
}
|
||||
|
||||
is RefreshTokenResponseJson.Success -> {
|
||||
// Store the new token information
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = refreshTokenResponse.accessToken,
|
||||
refreshToken = refreshTokenResponse.refreshToken,
|
||||
expiresAtSec = clock.instant().epochSecond +
|
||||
refreshTokenResponse.expiresIn,
|
||||
),
|
||||
)
|
||||
refreshTokenResponse.accessToken.asSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson> =
|
||||
refreshAccessTokenSynchronouslyInternal(
|
||||
userId = userId,
|
||||
logOutOnFailure = true,
|
||||
)
|
||||
|
||||
override fun logout(reason: LogoutReason) {
|
||||
activeUserId?.let { userId -> logout(userId = userId, reason = reason) }
|
||||
@@ -900,6 +858,7 @@ class AuthRepositoryImpl(
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String?,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
): RegisterResult {
|
||||
@@ -934,6 +893,7 @@ class AuthRepositoryImpl(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
@@ -950,6 +910,7 @@ class AuthRepositoryImpl(
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
captchaResponse = captchaToken,
|
||||
userSymmetricKey = registerKeyResponse.encryptedUserKey,
|
||||
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
@@ -964,9 +925,18 @@ class AuthRepositoryImpl(
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is RegisterResponseJson.CaptchaRequired -> {
|
||||
it.validationErrors.captchaKeys.firstOrNull()
|
||||
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
|
||||
?: RegisterResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Captcha ID"),
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Success -> {
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
RegisterResult.Success
|
||||
RegisterResult.Success(captchaToken = it.captchaBypassToken)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Invalid -> {
|
||||
@@ -1180,9 +1150,6 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.onSuccess {
|
||||
rsaKeys?.private?.let {
|
||||
// This process is used by TDE and Enterprise accounts during initial
|
||||
// login. We continue to store the locally generated keys
|
||||
// until TDE and Enterprise accounts support AEAD keys.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
|
||||
@@ -1215,6 +1182,10 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
||||
captchaTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
|
||||
override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) {
|
||||
duoTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
@@ -1451,6 +1422,42 @@ class AuthRepositoryImpl(
|
||||
onFailure = { LeaveOrganizationResult.Error(error = it) },
|
||||
)
|
||||
|
||||
private fun refreshAccessTokenSynchronouslyInternal(
|
||||
userId: String,
|
||||
logOutOnFailure: Boolean,
|
||||
): Result<RefreshTokenResponseJson> {
|
||||
val refreshToken = authDiskSource
|
||||
.getAccountTokens(userId = userId)
|
||||
?.refreshToken
|
||||
?: return IllegalStateException("Must be logged in.").asFailure()
|
||||
return identityService
|
||||
.refreshTokenSynchronously(refreshToken)
|
||||
.flatMap { refreshTokenResponse ->
|
||||
// Check to make sure the user is still logged in after making the request
|
||||
authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(userId)
|
||||
?.let { refreshTokenResponse.asSuccess() }
|
||||
?: IllegalStateException("Must be logged in.").asFailure()
|
||||
}
|
||||
.onFailure {
|
||||
if (logOutOnFailure) {
|
||||
logout(userId = userId, reason = LogoutReason.TokenRefreshFail)
|
||||
}
|
||||
}
|
||||
.onSuccess { refreshTokenResponse ->
|
||||
// Update the existing UserState with updated token information
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = refreshTokenResponse.accessToken,
|
||||
refreshToken = refreshTokenResponse.refreshToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
@@ -1606,6 +1613,7 @@ class AuthRepositoryImpl(
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
deviceData: DeviceDataModel? = null,
|
||||
orgIdentifier: String? = null,
|
||||
captchaToken: String?,
|
||||
newDeviceOtp: String? = null,
|
||||
): LoginResult = identityService
|
||||
.getToken(
|
||||
@@ -1613,6 +1621,7 @@ class AuthRepositoryImpl(
|
||||
email = email,
|
||||
authModel = authModel,
|
||||
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
|
||||
captchaToken = captchaToken,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.fold(
|
||||
@@ -1631,6 +1640,10 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
onSuccess = { loginResponse ->
|
||||
when (loginResponse) {
|
||||
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
|
||||
captchaId = loginResponse.captchaKey,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.TwoFactorRequired -> handleLoginCommonTwoFactorRequired(
|
||||
loginResponse = loginResponse,
|
||||
email = email,
|
||||
@@ -1767,7 +1780,6 @@ class AuthRepositoryImpl(
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = loginResponse.accessToken,
|
||||
refreshToken = loginResponse.refreshToken,
|
||||
expiresAtSec = clock.instant().epochSecond + loginResponse.expiresInSeconds,
|
||||
),
|
||||
)
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
@@ -1778,18 +1790,11 @@ class AuthRepositoryImpl(
|
||||
// when we completed the pending admin auth request.
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
||||
}
|
||||
// We continue to store the private key for backwards compatibility. Key connector
|
||||
// conversion still relies on the private key.
|
||||
loginResponse.privateKey?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
loginResponse.accountKeys?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storeAccountKeys(userId = userId, accountKeys = it)
|
||||
}
|
||||
// If the user just authenticated with a two-factor code and selected the option to
|
||||
// remember it, then the API response will return a token that will be used in place
|
||||
// of the two-factor code on the next login attempt.
|
||||
@@ -1890,8 +1895,6 @@ class AuthRepositoryImpl(
|
||||
masterKey = it.masterKey,
|
||||
userKey = key,
|
||||
),
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
@@ -1915,8 +1918,6 @@ class AuthRepositoryImpl(
|
||||
val result = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
@@ -1929,16 +1930,10 @@ class AuthRepositoryImpl(
|
||||
userId = profile.userId,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
)
|
||||
// We continue to store the private key for backwards compatibility since
|
||||
// key connector conversion still relies on the private key.
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = profile.userId,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
)
|
||||
authDiskSource.storeAccountKeys(
|
||||
userId = profile.userId,
|
||||
accountKeys = loginResponse.accountKeys,
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -1960,13 +1955,11 @@ class AuthRepositoryImpl(
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
val masterPassword = password ?: return null
|
||||
val privateKey = loginResponse.privateKeyOrNull() ?: return null
|
||||
val privateKey = loginResponse.privateKey ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
return unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = key,
|
||||
@@ -1984,15 +1977,13 @@ class AuthRepositoryImpl(
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
// These values will only be null during the Just-in-Time provisioning flow.
|
||||
val privateKey = loginResponse.privateKeyOrNull()
|
||||
val privateKey = loginResponse.privateKey
|
||||
val key = loginResponse.key
|
||||
if (privateKey != null && key != null) {
|
||||
deviceData?.let { model ->
|
||||
return unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = loginResponse.accountKeys?.securityState?.securityState,
|
||||
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
method = model
|
||||
@@ -2017,26 +2008,13 @@ class AuthRepositoryImpl(
|
||||
.userDecryptionOptions
|
||||
?.trustedDeviceUserDecryptionOptions
|
||||
?.let { options ->
|
||||
loginResponse.accountKeys
|
||||
?.let { accountKeys ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
profile = profile,
|
||||
privateKey = accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey,
|
||||
securityState = accountKeys.securityState?.securityState,
|
||||
signingKey = accountKeys.signatureKeyPair?.wrappedSigningKey,
|
||||
)
|
||||
}
|
||||
?: loginResponse.privateKey
|
||||
?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
profile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = null,
|
||||
signingKey = null,
|
||||
)
|
||||
}
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
profile = profile,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2048,8 +2026,6 @@ class AuthRepositoryImpl(
|
||||
options: TrustedDeviceUserDecryptionOptionsJson,
|
||||
profile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
securityState: String?,
|
||||
signingKey: String?,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = profile.userId
|
||||
@@ -2068,8 +2044,6 @@ class AuthRepositoryImpl(
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
signingKey = signingKey,
|
||||
securityState = securityState,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
|
||||
@@ -2097,8 +2071,6 @@ class AuthRepositoryImpl(
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
securityState = securityState,
|
||||
signingKey = signingKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
protectedDevicePrivateKey = encryptedPrivateKey,
|
||||
@@ -2118,8 +2090,6 @@ class AuthRepositoryImpl(
|
||||
private suspend fun unlockVault(
|
||||
accountProfile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
securityState: String?,
|
||||
signingKey: String?,
|
||||
initUserCryptoMethod: InitUserCryptoMethod,
|
||||
): VaultUnlockResult {
|
||||
val userId = accountProfile.userId
|
||||
@@ -2128,8 +2098,6 @@ class AuthRepositoryImpl(
|
||||
email = accountProfile.email,
|
||||
kdf = accountProfile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
signingKey = signingKey,
|
||||
securityState = securityState,
|
||||
initUserCryptoMethod = initUserCryptoMethod,
|
||||
// The value for the organization keys here will typically be null. We can separately
|
||||
// unlock the vault for organization data after receiving the sync response if this
|
||||
@@ -2173,11 +2141,3 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to extract the private key from the
|
||||
* [GetTokenResponseJson.Success] response.
|
||||
*/
|
||||
private fun GetTokenResponseJson.Success.privateKeyOrNull(): String? =
|
||||
this.accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey
|
||||
?: this.privateKey
|
||||
|
||||
@@ -27,7 +27,6 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -40,7 +39,6 @@ object AuthRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAuthRepository(
|
||||
clock: Clock,
|
||||
accountsService: AccountsService,
|
||||
devicesService: DevicesService,
|
||||
identityService: IdentityService,
|
||||
@@ -63,7 +61,6 @@ object AuthRepositoryModule {
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
clock = clock,
|
||||
accountsService = accountsService,
|
||||
devicesService = devicesService,
|
||||
identityService = identityService,
|
||||
|
||||
@@ -9,6 +9,11 @@ sealed class LoginResult {
|
||||
*/
|
||||
data object Success : LoginResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : LoginResult()
|
||||
|
||||
/**
|
||||
* Encryption key migration is required.
|
||||
*/
|
||||
|
||||
@@ -29,24 +29,6 @@ sealed class LogoutReason {
|
||||
data object NoLongerSupported : Biometrics()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the logout is happening because the there was an "invalid_grant" response
|
||||
* from the network.
|
||||
*/
|
||||
data object InvalidGrant : LogoutReason()
|
||||
|
||||
/**
|
||||
* Indicates that the logout is happening because the there was a "Forbidden" response from
|
||||
* token refresh API.
|
||||
*/
|
||||
data object RefreshForbidden : LogoutReason()
|
||||
|
||||
/**
|
||||
* Indicates that the logout is happening because the there was a "Unauthorized" response from
|
||||
* token refresh API.
|
||||
*/
|
||||
data object RefreshUnauthorized : LogoutReason()
|
||||
|
||||
/**
|
||||
* Indicates that the logout is happening because of an invalid state.
|
||||
*/
|
||||
@@ -76,6 +58,11 @@ sealed class LogoutReason {
|
||||
*/
|
||||
data object Timeout : LogoutReason()
|
||||
|
||||
/**
|
||||
* Indicates that the logout is happening because the access token could not be refreshed.
|
||||
*/
|
||||
data object TokenRefreshFail : LogoutReason()
|
||||
|
||||
/**
|
||||
* Indicates that the logout is happening because the user tried to unlock the vault
|
||||
* unsuccessfully too many times.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@@ -7,8 +7,16 @@ sealed class RegisterResult {
|
||||
/**
|
||||
* Register succeeded.
|
||||
*
|
||||
* @param captchaToken the captcha bypass token to bypass future captcha verifications.
|
||||
*/
|
||||
data object Success : RegisterResult()
|
||||
data class Success(val captchaToken: String?) : RegisterResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*
|
||||
* @param captchaId the captcha id for performing the captcha verification.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : RegisterResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.net.URLEncoder
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
|
||||
private const val CAPTCHA_HOST: String = "captcha-callback"
|
||||
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
|
||||
|
||||
/**
|
||||
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
|
||||
*/
|
||||
fun generateUriForCaptcha(captchaId: String): Uri {
|
||||
val json = buildJsonObject {
|
||||
put(key = "siteKey", value = captchaId)
|
||||
put(key = "locale", value = Locale.getDefault().toString())
|
||||
put(key = "callbackUri", value = CALLBACK_URI)
|
||||
put(key = "captchaRequiredText", value = "Captcha required")
|
||||
}
|
||||
val base64Data = Base64
|
||||
.getEncoder()
|
||||
.encodeToString(
|
||||
json
|
||||
.toString()
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
|
||||
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=$base64Data&parent=$parentParam&v=1"
|
||||
return Uri.parse(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a [CaptchaCallbackTokenResult] from an Intent. There are three possible cases.
|
||||
*
|
||||
* - `null`: Intent is not a captcha callback, or data is null.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.MissingToken]:
|
||||
* Intent is the captcha callback, but its missing a token value.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.Success]:
|
||||
* Intent is the captcha callback, and it has a token.
|
||||
*/
|
||||
fun Intent.getCaptchaCallbackTokenResult(): CaptchaCallbackTokenResult? {
|
||||
val localData = data
|
||||
return if (
|
||||
action == Intent.ACTION_VIEW && localData != null && localData.host == CAPTCHA_HOST
|
||||
) {
|
||||
localData.getQueryParameter("token")?.let {
|
||||
CaptchaCallbackTokenResult.Success(token = it)
|
||||
} ?: CaptchaCallbackTokenResult.MissingToken
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed class representing the result of captcha callback token extraction.
|
||||
*/
|
||||
sealed class CaptchaCallbackTokenResult {
|
||||
/**
|
||||
* Represents a missing token in the captcha callback.
|
||||
*/
|
||||
data object MissingToken : CaptchaCallbackTokenResult()
|
||||
|
||||
/**
|
||||
* Represents a token present in the captcha callback.
|
||||
*/
|
||||
data class Success(val token: String) : CaptchaCallbackTokenResult()
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package com.x8bit.bitwarden.data.auth.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
|
||||
private const val NOTIFICATION_DATA: String = "notificationData"
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -90,7 +89,6 @@ object AccessibilityModule {
|
||||
accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
launcherPackageNameManager: LauncherPackageNameManager,
|
||||
powerManager: PowerManager,
|
||||
toastManager: ToastManager,
|
||||
): BitwardenAccessibilityProcessor =
|
||||
BitwardenAccessibilityProcessorImpl(
|
||||
context = context,
|
||||
@@ -98,7 +96,6 @@ object AccessibilityModule {
|
||||
accessibilityAutofillManager = accessibilityAutofillManager,
|
||||
launcherPackageNameManager = launcherPackageNameManager,
|
||||
powerManager = powerManager,
|
||||
toastManager = toastManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -5,8 +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.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
@@ -27,7 +26,6 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
private val accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
private val launcherPackageNameManager: LauncherPackageNameManager,
|
||||
private val powerManager: PowerManager,
|
||||
private val toastManager: ToastManager,
|
||||
) : BitwardenAccessibilityProcessor {
|
||||
override fun processAccessibilityEvent(
|
||||
event: AccessibilityEvent,
|
||||
@@ -112,10 +110,13 @@ class BitwardenAccessibilityProcessorImpl(
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
toastManager.show(
|
||||
messageId = BitwardenString.autofill_tile_uri_not_found,
|
||||
duration = Toast.LENGTH_LONG,
|
||||
)
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
R.string.autofill_tile_uri_not_found,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillCallbackIntentSender
|
||||
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
|
||||
@@ -23,9 +22,12 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
saveInfo: SaveInfo?,
|
||||
): FillResponse? =
|
||||
if (filledData.fillableAutofillIds.isNotEmpty()) {
|
||||
Timber.d("Autofill request constructing FillResponse")
|
||||
val fillResponseBuilder = FillResponse.Builder()
|
||||
saveInfo?.let { nonNullSaveInfo -> fillResponseBuilder.setSaveInfo(nonNullSaveInfo) }
|
||||
|
||||
saveInfo
|
||||
?.let { nonNullSaveInfo ->
|
||||
fillResponseBuilder.setSaveInfo(nonNullSaveInfo)
|
||||
}
|
||||
|
||||
filledData
|
||||
.filledPartitions
|
||||
@@ -50,7 +52,12 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
|
||||
fillResponseBuilder
|
||||
// Add the Vault Item
|
||||
.addDataset(filledData.buildVaultItemDataset(autofillAppInfo = autofillAppInfo))
|
||||
.addDataset(
|
||||
filledData
|
||||
.buildVaultItemDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
),
|
||||
)
|
||||
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
||||
.build()
|
||||
} else {
|
||||
@@ -59,14 +66,13 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if the
|
||||
* [FilledPartition.autofillCipher] has a valid cipher id.
|
||||
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if totp is enabled
|
||||
* and there the [FilledPartition.autofillCipher] has a valid cipher id.
|
||||
*/
|
||||
private fun FilledPartition.toAuthIntentSenderOrNull(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
@@ -74,7 +80,8 @@ private fun FilledPartition.toAuthIntentSenderOrNull(
|
||||
autofillCipher
|
||||
.cipherId
|
||||
?.let { cipherId ->
|
||||
createAutofillCallbackIntentSender(
|
||||
// We always do this even if there is no TOTP code because we want to log the events
|
||||
createTotpCopyIntentSender(
|
||||
cipherId = cipherId,
|
||||
context = autofillAppInfo.context,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -35,7 +34,6 @@ 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.
|
||||
@@ -86,7 +84,7 @@ class FilledDataBuilderImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
.orEmpty()
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,13 +114,15 @@ class FilledDataBuilderImpl(
|
||||
): FilledPartition {
|
||||
val filledItems = autofillViews
|
||||
.mapNotNull { autofillView ->
|
||||
autofillCipher
|
||||
.getAutofillValueOrNull(autofillView)
|
||||
?.let { value ->
|
||||
autofillView.buildFilledItemOrNull(
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
val value = when (autofillView) {
|
||||
is AutofillView.Card.ExpirationMonth -> autofillCipher.expirationMonth
|
||||
is AutofillView.Card.ExpirationYear -> autofillCipher.expirationYear
|
||||
is AutofillView.Card.Number -> autofillCipher.number
|
||||
is AutofillView.Card.SecurityCode -> autofillCipher.code
|
||||
}
|
||||
autofillView.buildFilledItemOrNull(
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
|
||||
return FilledPartition(
|
||||
@@ -160,48 +160,6 @@ class FilledDataBuilderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the autofill value for the given [autofillView], or null if no value is available.
|
||||
*/
|
||||
private fun AutofillCipher.Card.getAutofillValueOrNull(autofillView: AutofillView.Card): String? =
|
||||
when (autofillView) {
|
||||
is AutofillView.Card.CardholderName -> {
|
||||
cardholderName.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationMonth -> {
|
||||
expirationMonth.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationYear -> {
|
||||
expirationYear.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.Number -> {
|
||||
number
|
||||
.filter { it.isDigit() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.SecurityCode -> {
|
||||
code
|
||||
.filter { it.isDigit() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationDate -> {
|
||||
if (expirationMonth.isNotBlank() && expirationYear.isNotBlank()) {
|
||||
expirationMonth.padStart(2, '0') + expirationYear.takeLast(2)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
is AutofillView.Card.Brand -> {
|
||||
brand.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item at the [index]. If that fails, return the last item in the list. If that also fails,
|
||||
* return null.
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -19,7 +18,6 @@ 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
|
||||
@@ -28,7 +26,6 @@ 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.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.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.platform.manager.AppStateManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -29,9 +29,9 @@ object ActivityAutofillModule {
|
||||
@ActivityScoped
|
||||
@ActivityScopedManager
|
||||
@Provides
|
||||
fun provideActivityScopedBrowserThirdPartyAutofillManager(
|
||||
fun provideActivityScopedChromeThirdPartyAutofillManager(
|
||||
activity: Activity,
|
||||
): BrowserThirdPartyAutofillManager = BrowserThirdPartyAutofillManagerImpl(
|
||||
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
|
||||
context = activity.baseContext,
|
||||
)
|
||||
|
||||
@@ -39,19 +39,19 @@ object ActivityAutofillModule {
|
||||
@Provides
|
||||
fun provideAutofillActivityManager(
|
||||
@ActivityScopedManager autofillManager: AutofillManager,
|
||||
@ActivityScopedManager browserThirdPartyAutofillManager: BrowserThirdPartyAutofillManager,
|
||||
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
appStateManager: AppStateManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
): AutofillActivityManager =
|
||||
AutofillActivityManagerImpl(
|
||||
autofillManager = autofillManager,
|
||||
browserThirdPartyAutofillManager = browserThirdPartyAutofillManager,
|
||||
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
|
||||
appStateManager = appStateManager,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
lifecycleScope = lifecycleScope,
|
||||
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
|
||||
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,14 +16,15 @@ 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.browser.BrowserThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
@@ -58,8 +59,12 @@ object AutofillModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesBrowserAutofillEnabledManager(): BrowserThirdPartyAutofillEnabledManager =
|
||||
BrowserThirdPartyAutofillEnabledManagerImpl()
|
||||
fun providesChromeAutofillEnabledManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): ChromeThirdPartyAutofillEnabledManager =
|
||||
ChromeThirdPartyAutofillEnabledManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@@ -88,6 +93,7 @@ object AutofillModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillTotpManager(
|
||||
@ApplicationContext context: Context,
|
||||
clock: Clock,
|
||||
clipboardManager: BitwardenClipboardManager,
|
||||
authRepository: AuthRepository,
|
||||
@@ -95,6 +101,7 @@ object AutofillModule {
|
||||
vaultRepository: VaultRepository,
|
||||
): AutofillTotpManager =
|
||||
AutofillTotpManagerImpl(
|
||||
context = context,
|
||||
clock = clock,
|
||||
clipboardManager = clipboardManager,
|
||||
authRepository = authRepository,
|
||||
@@ -108,13 +115,11 @@ object AutofillModule {
|
||||
authRepository: AuthRepository,
|
||||
cipherMatchingManager: CipherMatchingManager,
|
||||
vaultRepository: VaultRepository,
|
||||
policyManager: PolicyManager,
|
||||
): AutofillCipherProvider =
|
||||
AutofillCipherProviderImpl(
|
||||
authRepository = authRepository,
|
||||
cipherMatchingManager = cipherMatchingManager,
|
||||
vaultRepository = vaultRepository,
|
||||
policyManager = policyManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -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.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.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.platform.manager.AppStateManager
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -14,22 +14,21 @@ import kotlinx.coroutines.flow.onEach
|
||||
*/
|
||||
class AutofillActivityManagerImpl(
|
||||
private val autofillManager: AutofillManager,
|
||||
private val browserThirdPartyAutofillManager: BrowserThirdPartyAutofillManager,
|
||||
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
appStateManager: AppStateManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
|
||||
) : AutofillActivityManager {
|
||||
private val isAutofillEnabledAndSupported: Boolean
|
||||
get() = autofillManager.isEnabled &&
|
||||
autofillManager.hasEnabledAutofillServices() &&
|
||||
autofillManager.isAutofillSupported
|
||||
|
||||
private val browserAutofillStatus: BrowserThirdPartyAutofillStatus
|
||||
get() = BrowserThirdPartyAutofillStatus(
|
||||
braveStableStatusData = browserThirdPartyAutofillManager.stableBraveAutofillStatus,
|
||||
chromeStableStatusData = browserThirdPartyAutofillManager.stableChromeAutofillStatus,
|
||||
chromeBetaChannelStatusData = browserThirdPartyAutofillManager.betaChromeAutofillStatus,
|
||||
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
|
||||
get() = ChromeThirdPartyAutofillStatus(
|
||||
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
|
||||
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
|
||||
)
|
||||
|
||||
init {
|
||||
@@ -37,8 +36,8 @@ class AutofillActivityManagerImpl(
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
|
||||
browserThirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus =
|
||||
browserAutofillStatus
|
||||
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
|
||||
chromeAutofillStatus
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [AutofillCompletionManager].
|
||||
@@ -42,7 +41,6 @@ class AutofillCompletionManagerImpl(
|
||||
.intent
|
||||
?.getAutofillAssistStructureOrNull()
|
||||
?: run {
|
||||
Timber.w("Assist structure not found")
|
||||
activity.cancelAndFinish()
|
||||
return
|
||||
}
|
||||
@@ -53,7 +51,6 @@ class AutofillCompletionManagerImpl(
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
if (autofillRequest !is AutofillRequest.Fillable) {
|
||||
Timber.w("Request is not fillable")
|
||||
activity.cancelAndFinish()
|
||||
return
|
||||
}
|
||||
@@ -71,13 +68,11 @@ class AutofillCompletionManagerImpl(
|
||||
authIntentSender = null,
|
||||
)
|
||||
?: run {
|
||||
Timber.w("Dataset not found")
|
||||
activity.cancelAndFinish()
|
||||
return@launch
|
||||
}
|
||||
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
val resultIntent = createAutofillSelectionResultIntent(dataset)
|
||||
Timber.d("Autofill success")
|
||||
activity.setResultAndFinish(resultIntent = resultIntent)
|
||||
cipherView.id?.let {
|
||||
organizationEventManager.trackEvent(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -14,6 +16,7 @@ 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,
|
||||
@@ -24,19 +27,25 @@ class AutofillTotpManagerImpl(
|
||||
if (settingsRepository.isAutoCopyTotpDisabled) return
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
if (!isPremium && !cipherView.organizationUseTotp) return
|
||||
cipherView.login?.totp ?: return
|
||||
val cipherId = cipherView.id ?: return
|
||||
val totpCode = cipherView.login?.totp ?: return
|
||||
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
time = clock.instant(),
|
||||
cipherId = cipherId,
|
||||
totpCode = totpCode,
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
clipboardManager.setText(
|
||||
text = totpResult.code,
|
||||
toastDescriptorOverride = BitwardenString.verification_code_totp.asText(),
|
||||
toastDescriptorOverride = R.string.verification_code_totp.asText(),
|
||||
)
|
||||
Toast
|
||||
.makeText(
|
||||
context.applicationContext,
|
||||
R.string.verification_code_totp,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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>
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* Default implementation of [BrowserThirdPartyAutofillEnabledManager].
|
||||
*/
|
||||
class BrowserThirdPartyAutofillEnabledManagerImpl : 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
|
||||
}
|
||||
|
||||
private val DEFAULT_STATUS = BrowserThirdPartyAutofillStatus(
|
||||
braveStableStatusData = BrowserThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
chromeStableStatusData = BrowserThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
chromeBetaChannelStatusData = BrowserThirdPartyAutoFillData(
|
||||
isAvailable = false,
|
||||
isThirdPartyEnabled = false,
|
||||
),
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
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,36 +1,35 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager.browser
|
||||
package com.x8bit.bitwarden.data.autofill.manager.chrome
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
|
||||
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
|
||||
|
||||
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 [BrowserThirdPartyAutofillManager] which uses a [ContentResolver]
|
||||
* to determine if the installed browser packages support and enable third party autofill services.
|
||||
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
|
||||
* [ContentResolver] to determine if the installed Chrome 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 BrowserThirdPartyAutofillManagerImpl(
|
||||
class ChromeThirdPartyAutofillManagerImpl(
|
||||
private val context: Context,
|
||||
) : 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)
|
||||
) : ChromeThirdPartyAutofillManager {
|
||||
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
|
||||
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
|
||||
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
|
||||
|
||||
private fun getThirdPartyAutoFillStatusForChannel(
|
||||
releaseChannel: BrowserPackage,
|
||||
): BrowserThirdPartyAutoFillData {
|
||||
releaseChannel: ChromeReleaseChannel,
|
||||
): ChromeThirdPartyAutoFillData {
|
||||
val uri = Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
|
||||
@@ -55,7 +54,7 @@ class BrowserThirdPartyAutofillManagerImpl(
|
||||
true
|
||||
}
|
||||
?: false
|
||||
return BrowserThirdPartyAutoFillData(
|
||||
return ChromeThirdPartyAutoFillData(
|
||||
isAvailable = isThirdPartyAvailable,
|
||||
isThirdPartyEnabled = thirdPartyEnabled,
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
|
||||
/**
|
||||
* The app information required for the autofill service.
|
||||
@@ -10,10 +9,4 @@ 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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents data for the autofill flow via authentication intents.
|
||||
*
|
||||
* @property cipherId The ID of the cipher associated with this Autofill instance.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AutofillCallbackData(
|
||||
val cipherId: String,
|
||||
) : Parcelable
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.core.Uuid
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* A paired down model of the CipherView for use within the autofill feature.
|
||||
@@ -46,10 +46,9 @@ sealed class AutofillCipher {
|
||||
val expirationMonth: String,
|
||||
val expirationYear: String,
|
||||
val number: String,
|
||||
val brand: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = BitwardenDrawable.ic_payment_card
|
||||
@DrawableRes get() = R.drawable.ic_payment_card
|
||||
|
||||
override val isTotpEnabled: Boolean
|
||||
get() = false
|
||||
@@ -68,6 +67,6 @@ sealed class AutofillCipher {
|
||||
val username: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = BitwardenDrawable.ic_globe
|
||||
@DrawableRes get() = R.drawable.ic_globe
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
/**
|
||||
* Autofill hints used to determine what data an input field is associated with.
|
||||
*/
|
||||
enum class AutofillHint {
|
||||
CARD_CARDHOLDER,
|
||||
CARD_EXPIRATION_DATE,
|
||||
CARD_EXPIRATION_MONTH,
|
||||
CARD_EXPIRATION_YEAR,
|
||||
CARD_NUMBER,
|
||||
CARD_SECURITY_CODE,
|
||||
CARD_BRAND,
|
||||
PASSWORD,
|
||||
USERNAME,
|
||||
}
|
||||
@@ -16,16 +16,13 @@ sealed class AutofillSaveItem : Parcelable {
|
||||
* @property expirationMonth The expiration month in string form (if applicable).
|
||||
* @property expirationYear The expiration year in string form (if applicable).
|
||||
* @property securityCode The security code for the card (if applicable).
|
||||
* @property cardholderName The name on the card (if applicable).
|
||||
*/
|
||||
@Parcelize
|
||||
data class Card(
|
||||
val cardholderName: String?,
|
||||
val number: String?,
|
||||
val expirationMonth: String?,
|
||||
val expirationYear: String?,
|
||||
val securityCode: String?,
|
||||
val brand: String?,
|
||||
) : AutofillSaveItem()
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents data for a TOTP copying during the autofill flow via authentication intents.
|
||||
*
|
||||
* @property cipherId The cipher for which we are copying a TOTP to the clipboard.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AutofillTotpCopyData(
|
||||
val cipherId: String,
|
||||
) : Parcelable
|
||||
@@ -48,28 +48,10 @@ sealed class AutofillView {
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The expiration year [AutofillView] for the [Card] data partition. This implementation
|
||||
* also has its own [yearValue] because it can be present in lists, in which case there
|
||||
* is specialized logic for determining its [yearValue]. The [Data.textValue] is very
|
||||
* likely going to be a very different value.
|
||||
* The expiration year [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
data class ExpirationYear(
|
||||
override val data: Data,
|
||||
val yearValue: String?,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The expiration date [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
data class ExpirationDate(
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The cardholder name [AutofillView] for the [Card] data partition.
|
||||
*/
|
||||
data class CardholderName(
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
@@ -85,17 +67,6 @@ sealed class AutofillView {
|
||||
data class SecurityCode(
|
||||
override val data: Data,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
* The brand [AutofillView] for the [Card] data partition. This implementation also has its
|
||||
* own [brandValue] because it can be present in lists, in which case there is specialized
|
||||
* logic for determining its [brandValue]. The [Data.textValue] is very likely going to be
|
||||
* a very different value.
|
||||
*/
|
||||
data class Brand(
|
||||
override val data: Data,
|
||||
val brandValue: String?,
|
||||
) : Card()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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),
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
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),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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,7 +15,6 @@ 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.
|
||||
@@ -24,8 +23,6 @@ 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",
|
||||
)
|
||||
|
||||
@@ -73,7 +70,6 @@ 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.
|
||||
@@ -135,7 +131,6 @@ class AutofillParserImpl(
|
||||
|
||||
// Get inline information if available
|
||||
val isInlineAutofillEnabled = settingsRepository.isInlineAutofillEnabled
|
||||
Timber.d("Autofill request isInlineEnabled=$isInlineAutofillEnabled -- ${fillRequest?.id}")
|
||||
val maxInlineSuggestionsCount = fillRequest.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = isInlineAutofillEnabled,
|
||||
|
||||
@@ -53,12 +53,8 @@ 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 {
|
||||
Timber.d("Autofill job cancelled")
|
||||
job.cancel()
|
||||
}
|
||||
cancellationSignal.setOnCancelListener { job.cancel() }
|
||||
// Process the OS data and handle invoking the callback with the result.
|
||||
job.cancel()
|
||||
job = scope.launch {
|
||||
@@ -126,7 +122,6 @@ class AutofillProcessorImpl(
|
||||
)
|
||||
when (autofillRequest) {
|
||||
is AutofillRequest.Fillable -> {
|
||||
Timber.d("Autofill request is Fillable -- ${fillRequest.id}")
|
||||
// Fulfill the [autofillRequest].
|
||||
val filledData = filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
@@ -146,7 +141,6 @@ 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.
|
||||
@@ -159,7 +153,6 @@ 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,21 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.autofill.provider
|
||||
|
||||
import com.bitwarden.network.model.PolicyTypeJson
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CipherRepromptType
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
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 timber.log.Timber
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while waiting for the vault status to not be
|
||||
@@ -36,7 +31,6 @@ class AutofillCipherProviderImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val cipherMatchingManager: CipherMatchingManager,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val policyManager: PolicyManager,
|
||||
) : AutofillCipherProvider {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
@@ -55,40 +49,31 @@ class AutofillCipherProviderImpl(
|
||||
}
|
||||
|
||||
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
|
||||
val cipherListViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
|
||||
val organizationIdsWithCardTypeRestrictions = policyManager
|
||||
.getActivePolicies(PolicyTypeJson.RESTRICT_ITEM_TYPES)
|
||||
.map { it.organizationId }
|
||||
return cipherListViews
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
|
||||
return cipherViews
|
||||
.mapNotNull { cipherView ->
|
||||
cipherView
|
||||
// We only care about non-deleted card ciphers.
|
||||
.takeIf {
|
||||
// Must be card type.
|
||||
it.type is CipherListViewType.Card &&
|
||||
cipherView.type == CipherType.CARD &&
|
||||
// Must not be deleted.
|
||||
it.deletedDate == null &&
|
||||
cipherView.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
it.reprompt == CipherRepromptType.NONE &&
|
||||
// Must not be restricted by organization.
|
||||
it.organizationId !in organizationIdsWithCardTypeRestrictions
|
||||
it.reprompt == CipherRepromptType.NONE
|
||||
}
|
||||
?.let { nonNullCipherListView ->
|
||||
nonNullCipherListView.id?.let { cipherId ->
|
||||
decryptCipherOrNull(cipherId = cipherId)?.let { cipherView ->
|
||||
AutofillCipher.Card(
|
||||
cipherId = cipherView.id,
|
||||
name = cipherView.name,
|
||||
subtitle = cipherView.subtitle.orEmpty(),
|
||||
cardholderName = cipherView.card?.cardholderName.orEmpty(),
|
||||
code = cipherView.card?.code.orEmpty(),
|
||||
expirationMonth = cipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = cipherView.card?.expYear.orEmpty(),
|
||||
number = cipherView.card?.number.orEmpty(),
|
||||
brand = cipherView.card?.brand.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
?.let { nonNullCipherView ->
|
||||
AutofillCipher.Card(
|
||||
cipherId = cipherView.id,
|
||||
name = nonNullCipherView.name,
|
||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
||||
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
|
||||
code = nonNullCipherView.card?.code.orEmpty(),
|
||||
expirationMonth = nonNullCipherView.card?.expMonth.orEmpty(),
|
||||
expirationYear = nonNullCipherView.card?.expYear.orEmpty(),
|
||||
number = nonNullCipherView.card?.number.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,12 +81,12 @@ class AutofillCipherProviderImpl(
|
||||
override suspend fun getLoginAutofillCiphers(
|
||||
uri: String,
|
||||
): List<AutofillCipher.Login> {
|
||||
val cipherViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
|
||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||
// We only care about non-deleted login ciphers.
|
||||
val loginCiphers = cipherViews
|
||||
.filter {
|
||||
// Must be login type
|
||||
it.type is CipherListViewType.Login &&
|
||||
it.type == CipherType.LOGIN &&
|
||||
// Must not be deleted.
|
||||
it.deletedDate == null &&
|
||||
// Must not require a reprompt.
|
||||
@@ -111,12 +96,9 @@ class AutofillCipherProviderImpl(
|
||||
return cipherMatchingManager
|
||||
// Filter for ciphers that match the uri in some way.
|
||||
.filterCiphersForMatches(
|
||||
cipherListViews = loginCiphers,
|
||||
ciphers = loginCiphers,
|
||||
matchUri = uri,
|
||||
)
|
||||
.mapNotNull { cipherListView ->
|
||||
cipherListView.id?.let { decryptCipherOrNull(cipherId = it) }
|
||||
}
|
||||
.map { cipherView ->
|
||||
AutofillCipher.Login(
|
||||
cipherId = cipherView.id,
|
||||
@@ -132,26 +114,10 @@ class AutofillCipherProviderImpl(
|
||||
/**
|
||||
* Get available [CipherView]s if possible.
|
||||
*/
|
||||
private suspend fun getUnlockedCipherListViewsOrNull(): List<CipherListView>? =
|
||||
private suspend fun getUnlockedCiphersOrNull(): List<CipherView>? =
|
||||
vaultRepository
|
||||
.decryptCipherListResultStateFlow
|
||||
.ciphersStateFlow
|
||||
.takeUnless { isVaultLocked() }
|
||||
?.firstWithTimeoutOrNull(timeMillis = GET_CIPHERS_TIMEOUT_MS) { it.data != null }
|
||||
?.data
|
||||
?.successes
|
||||
|
||||
private suspend fun decryptCipherOrNull(cipherId: String): CipherView? =
|
||||
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
|
||||
GetCipherResult.CipherNotFound -> {
|
||||
Timber.e("Cipher not found for autofill.")
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Failure -> {
|
||||
Timber.e(result.error, "Failed to decrypt cipher for autofill.")
|
||||
null
|
||||
}
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,19 +12,18 @@ import android.service.autofill.Dataset
|
||||
import android.view.autofill.AutofillManager
|
||||
import androidx.core.os.bundleOf
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
|
||||
import com.x8bit.bitwarden.AutofillCallbackActivity
|
||||
import com.x8bit.bitwarden.AutofillTotpCopyActivity
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCallbackData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val AUTOFILL_SAVE_ITEM_DATA_KEY = "autofill-save-item-data"
|
||||
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
|
||||
private const val AUTOFILL_CALLBACK_DATA_KEY = "autofill-callback-data"
|
||||
private const val AUTOFILL_TOTP_COPY_DATA_KEY = "autofill-totp-copy-data"
|
||||
private const val AUTOFILL_BUNDLE_KEY = "autofill-bundle-key"
|
||||
|
||||
/**
|
||||
@@ -55,21 +54,21 @@ fun createAutofillSelectionIntent(
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [IntentSender] built with the data required for performing an Autofill callback
|
||||
* during the autofill flow.
|
||||
* Creates an [IntentSender] built with the data required for performing a TOTP copying during
|
||||
* the autofill flow.
|
||||
*/
|
||||
fun createAutofillCallbackIntentSender(
|
||||
fun createTotpCopyIntentSender(
|
||||
cipherId: String,
|
||||
context: Context,
|
||||
): IntentSender {
|
||||
val intent = Intent(
|
||||
context,
|
||||
AutofillCallbackActivity::class.java,
|
||||
AutofillTotpCopyActivity::class.java,
|
||||
)
|
||||
.putExtra(
|
||||
AUTOFILL_BUNDLE_KEY,
|
||||
bundleOf(
|
||||
AUTOFILL_CALLBACK_DATA_KEY to AutofillCallbackData(cipherId = cipherId),
|
||||
AUTOFILL_TOTP_COPY_DATA_KEY to AutofillTotpCopyData(cipherId = cipherId),
|
||||
),
|
||||
)
|
||||
return PendingIntent
|
||||
@@ -143,12 +142,12 @@ fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
|
||||
?.getSafeParcelableExtra(AUTOFILL_SELECTION_DATA_KEY)
|
||||
|
||||
/**
|
||||
* Checks if the given [Intent] contains Autofill callback data. The [AutofillCallbackData] will be
|
||||
* Checks if the given [Intent] contains data for TOTP copying. The [AutofillTotpCopyData] will be
|
||||
* returned when present.
|
||||
*/
|
||||
fun Intent.getAutofillCallbackIntentOrNull(): AutofillCallbackData? =
|
||||
fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? =
|
||||
getBundleExtra(AUTOFILL_BUNDLE_KEY)
|
||||
?.getSafeParcelableExtra(AUTOFILL_CALLBACK_DATA_KEY)
|
||||
?.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY)
|
||||
|
||||
/**
|
||||
* Checks if the given [Activity] was created for Autofill. This is useful to avoid locking the
|
||||
|
||||
@@ -9,19 +9,16 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
val AutofillPartition.Card.expirationMonthSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.filterIsInstance<AutofillView.Card.ExpirationMonth>()
|
||||
.firstOrNull { it.monthValue != null }
|
||||
?.monthValue
|
||||
.firstOrNull { it is AutofillView.Card.ExpirationMonth && it.monthValue != null }
|
||||
?.data
|
||||
?.textValue
|
||||
|
||||
/**
|
||||
* The text value representation of the year from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.expirationYearSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.filterIsInstance<AutofillView.Card.ExpirationYear>()
|
||||
.firstOrNull { it.yearValue != null }
|
||||
?.yearValue
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.ExpirationYear }
|
||||
|
||||
/**
|
||||
* The text value representation of the card number from the [AutofillPartition.Card].
|
||||
@@ -37,24 +34,6 @@ val AutofillPartition.Card.securityCodeSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.SecurityCode }
|
||||
|
||||
/**
|
||||
* The text value representation of the cardholder name from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.cardholderNameSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.CardholderName }
|
||||
|
||||
/**
|
||||
* The text value representation of the brand from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.brandSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.filterIsInstance<AutofillView.Card.Brand>()
|
||||
.firstOrNull { it.brandValue != null }
|
||||
?.brandValue
|
||||
?: this.extractNonNullTextValueOrNull { it is AutofillView.Card.Brand }
|
||||
|
||||
/**
|
||||
* The text value representation of the password from the [AutofillPartition.Login].
|
||||
*/
|
||||
|
||||
@@ -11,12 +11,10 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
when (this.partition) {
|
||||
is AutofillPartition.Card -> {
|
||||
AutofillSaveItem.Card(
|
||||
cardholderName = partition.cardholderNameSaveValue,
|
||||
number = partition.numberSaveValue,
|
||||
expirationMonth = partition.expirationMonthSaveValue,
|
||||
expirationYear = partition.expirationYearSaveValue,
|
||||
securityCode = partition.securityCodeSaveValue,
|
||||
brand = partition.brandSaveValue,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,39 +35,3 @@ fun AutofillValue.extractTextValue(): String? =
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a year value from this [AutofillValue].
|
||||
*/
|
||||
fun AutofillValue.extractYearValue(
|
||||
autofillOptions: List<String>,
|
||||
): String? =
|
||||
when {
|
||||
this.isList && autofillOptions.isNotEmpty() -> {
|
||||
autofillOptions.getOrNull(listValue)
|
||||
}
|
||||
|
||||
this.isText -> {
|
||||
this.textValue.toString()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a card brand value from this [AutofillValue].
|
||||
*/
|
||||
fun AutofillValue.extractCardBrandValue(
|
||||
autofillOptions: List<String>,
|
||||
): String? =
|
||||
when {
|
||||
this.isList && autofillOptions.isNotEmpty() -> {
|
||||
autofillOptions.getOrNull(listValue)
|
||||
}
|
||||
|
||||
this.isText -> {
|
||||
this.textValue.toString()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.view.View
|
||||
import android.view.autofill.AutofillValue
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
|
||||
/**
|
||||
* Convert this [AutofillView] into a [FilledItem]. Return null if not possible.
|
||||
@@ -13,34 +11,29 @@ import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
|
||||
fun AutofillView.buildFilledItemOrNull(
|
||||
value: String,
|
||||
): FilledItem? =
|
||||
// Do not try to autofill fields that are empty in the vault
|
||||
if (value.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
when (this.data.autofillType) {
|
||||
View.AUTOFILL_TYPE_DATE -> {
|
||||
value
|
||||
.toLongOrNull()
|
||||
?.let { AutofillValue.forDate(it) }
|
||||
}
|
||||
|
||||
View.AUTOFILL_TYPE_LIST -> this.buildListAutofillValueOrNull(value = value)
|
||||
View.AUTOFILL_TYPE_TEXT -> AutofillValue.forText(value)
|
||||
View.AUTOFILL_TYPE_TOGGLE -> {
|
||||
value
|
||||
.toBooleanStrictOrNull()
|
||||
?.let { AutofillValue.forToggle(it) }
|
||||
}
|
||||
|
||||
else -> null
|
||||
when (this.data.autofillType) {
|
||||
View.AUTOFILL_TYPE_DATE -> {
|
||||
value
|
||||
.toLongOrNull()
|
||||
?.let { AutofillValue.forDate(it) }
|
||||
}
|
||||
?.let { autofillValue ->
|
||||
FilledItem(
|
||||
autofillId = this.data.autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
}
|
||||
|
||||
View.AUTOFILL_TYPE_LIST -> this.buildListAutofillValueOrNull(value = value)
|
||||
View.AUTOFILL_TYPE_TEXT -> AutofillValue.forText(value)
|
||||
View.AUTOFILL_TYPE_TOGGLE -> {
|
||||
value
|
||||
.toBooleanStrictOrNull()
|
||||
?.let { AutofillValue.forToggle(it) }
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
?.let { autofillValue ->
|
||||
FilledItem(
|
||||
autofillId = this.data.autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a list [AutofillValue] out of [value] or return null if not possible.
|
||||
@@ -49,50 +42,22 @@ fun AutofillView.buildFilledItemOrNull(
|
||||
private fun AutofillView.buildListAutofillValueOrNull(
|
||||
value: String,
|
||||
): AutofillValue? =
|
||||
when (this) {
|
||||
is AutofillView.Card.ExpirationMonth -> {
|
||||
val autofillOptionsSize = this.data.autofillOptions.size
|
||||
// The idea here is that `value` is a numerical representation of a month.
|
||||
val monthIndex = value.toIntOrNull()
|
||||
when {
|
||||
monthIndex == null -> null
|
||||
// We expect there is some placeholder or empty space at the beginning of the list.
|
||||
autofillOptionsSize == 13 -> AutofillValue.forList(monthIndex)
|
||||
autofillOptionsSize >= monthIndex -> AutofillValue.forList(monthIndex - 1)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
is AutofillView.Card.ExpirationYear -> {
|
||||
val autofillOptions = this.data.autofillOptions
|
||||
autofillOptions
|
||||
.firstOrNull { it == value || it.takeLast(2) == value.takeLast(2) }
|
||||
?.let { AutofillValue.forList(autofillOptions.indexOf(it)) }
|
||||
}
|
||||
|
||||
is AutofillView.Card.Brand -> {
|
||||
value.findVaultCardBrandWithNameOrNull()
|
||||
?.takeUnless { it == VaultCardBrand.SELECT }
|
||||
?.let { vaultCardBrand ->
|
||||
this.data.autofillOptions
|
||||
.firstOrNull { it.findVaultCardBrandWithNameOrNull() == vaultCardBrand }
|
||||
?.let { AutofillValue.forList(this.data.autofillOptions.indexOf(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
is AutofillView.Card.CardholderName,
|
||||
is AutofillView.Card.ExpirationDate,
|
||||
is AutofillView.Card.Number,
|
||||
is AutofillView.Card.SecurityCode,
|
||||
is AutofillView.Login.Password,
|
||||
is AutofillView.Login.Username,
|
||||
is AutofillView.Unused,
|
||||
-> {
|
||||
this
|
||||
.data
|
||||
.autofillOptions
|
||||
.indexOfFirst { it == value }
|
||||
.takeIf { it != -1 }
|
||||
?.let { AutofillValue.forList(it) }
|
||||
if (this is AutofillView.Card.ExpirationMonth) {
|
||||
val autofillOptionsSize = this.data.autofillOptions.size
|
||||
// The idea here is that `value` is a numerical representation of a month.
|
||||
val monthIndex = value.toIntOrNull()
|
||||
when {
|
||||
monthIndex == null -> null
|
||||
// We expect there is some placeholder or empty space at the beginning of the list.
|
||||
autofillOptionsSize == 13 -> AutofillValue.forList(monthIndex)
|
||||
autofillOptionsSize >= monthIndex -> AutofillValue.forList(monthIndex - 1)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
this
|
||||
.data
|
||||
.autofillOptions
|
||||
.indexOfFirst { it == value }
|
||||
.takeIf { it != -1 }
|
||||
?.let { AutofillValue.forList(it) }
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import com.bitwarden.vault.CardListView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
import com.bitwarden.vault.CopyableCipherFields
|
||||
import com.bitwarden.vault.LoginListView
|
||||
|
||||
/**
|
||||
* Returns true when the cipher is not deleted and contains at least one FIDO 2 credential.
|
||||
*/
|
||||
val CipherListView.isActiveWithFido2Credentials: Boolean
|
||||
get() = deletedDate == null && login?.hasFido2 ?: false
|
||||
|
||||
/**
|
||||
* Returns true when the cipher type is not deleted and contains a copyable password.
|
||||
*/
|
||||
val CipherListView.isActiveWithCopyablePassword: Boolean
|
||||
get() = deletedDate == null && copyableFields.contains(CopyableCipherFields.LOGIN_PASSWORD)
|
||||
|
||||
/**
|
||||
* Returns the [LoginListView] if the cipher is of type [CipherListViewType.Login], otherwise null.
|
||||
*/
|
||||
val CipherListView.login: LoginListView?
|
||||
get() = (this.type as? CipherListViewType.Login)?.v1
|
||||
|
||||
/**
|
||||
* Returns the [CardListView] if the cipher is of type [CipherListViewType.Card], otherwise null.
|
||||
*/
|
||||
val CipherListView.card: CardListView?
|
||||
get() = (this.type as? CipherListViewType.Card)?.v1
|
||||
@@ -24,7 +24,6 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
expirationMonth = card.expMonth.orEmpty(),
|
||||
expirationYear = card.expYear.orEmpty(),
|
||||
number = card.number.orEmpty(),
|
||||
brand = card.brand.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -51,9 +50,3 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
*/
|
||||
val CipherView.isActiveWithFido2Credentials: Boolean
|
||||
get() = deletedDate == null && !(login?.fido2Credentials.isNullOrEmpty())
|
||||
|
||||
/**
|
||||
* Returns true when the cipher is not deleted and contains at least one Pasword credential.
|
||||
*/
|
||||
val CipherView.isActiveWithPasswordCredentials: Boolean
|
||||
get() = deletedDate == null && !(login?.password.isNullOrEmpty())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -8,11 +9,12 @@ 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.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
|
||||
if (autofillAppInfo.sdkInt < Build.VERSION_CODES.R) {
|
||||
// When SDK version is bellow 30, InlinePresentationSpec is not available and null
|
||||
// must be returned.
|
||||
null
|
||||
@@ -26,13 +28,14 @@ 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.isVersionAtLeast(version = Build.VERSION_CODES.R)
|
||||
autofillAppInfo.sdkInt >= Build.VERSION_CODES.R
|
||||
) {
|
||||
inlineSuggestionsRequest?.maxSuggestionCount ?: 0
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
@@ -9,7 +10,6 @@ import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
@@ -28,6 +28,7 @@ val FilledData.fillableAutofillIds: List<AutofillId>
|
||||
/**
|
||||
* Builds a [Dataset] for the Vault item.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FilledData.buildVaultItemDataset(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
): Dataset {
|
||||
@@ -69,7 +70,7 @@ fun FilledData.buildVaultItemDataset(
|
||||
return Dataset.Builder()
|
||||
.setAuthentication(pendingIntent.intentSender)
|
||||
.apply {
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||
addVaultItemDataPostTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
@@ -131,7 +132,8 @@ private fun Dataset.Builder.addVaultItemDataPostTiramisu(
|
||||
/**
|
||||
* Adds the Vault data to the given [Dataset.Builder] for pre-Tiramisu versions.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("DEPRECATION", "LongParameterList")
|
||||
@SuppressLint("NewApi")
|
||||
private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
pendingIntent: PendingIntent,
|
||||
@@ -140,7 +142,7 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
inlinePresentationSpec: InlinePresentationSpec?,
|
||||
isLocked: Boolean,
|
||||
): Dataset.Builder {
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
inlinePresentationSpec
|
||||
?.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
@@ -148,7 +150,6 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
isLocked = isLocked,
|
||||
)
|
||||
?.let { inlinePresentation ->
|
||||
@Suppress("DEPRECATION")
|
||||
this.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.os.Build
|
||||
import android.annotation.SuppressLint
|
||||
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.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
@SuppressLint("NewApi")
|
||||
fun FilledItem.applyToDatasetPostTiramisu(
|
||||
datasetBuilder: Dataset.Builder,
|
||||
presentations: Presentations,
|
||||
@@ -30,11 +29,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,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
@@ -15,6 +16,7 @@ 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,
|
||||
@@ -24,9 +26,13 @@ fun FilledPartition.buildDataset(
|
||||
autofillCipher = autofillCipher,
|
||||
)
|
||||
val datasetBuilder = Dataset.Builder()
|
||||
authIntentSender?.let { intentSender -> datasetBuilder.setAuthentication(intentSender) }
|
||||
|
||||
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
|
||||
authIntentSender
|
||||
?.let { intentSender ->
|
||||
datasetBuilder.setAuthentication(intentSender)
|
||||
}
|
||||
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||
applyToDatasetPostTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
datasetBuilder = datasetBuilder,
|
||||
@@ -79,19 +85,20 @@ 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.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
inlinePresentationSpec
|
||||
?.createCipherInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillCipher = autofillCipher,
|
||||
)
|
||||
?.let { inlinePresentation ->
|
||||
@Suppress("DEPRECATION")
|
||||
datasetBuilder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +1,53 @@
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a password field.
|
||||
*/
|
||||
fun HtmlInfo?.isPasswordField(): Boolean = isInputField &&
|
||||
hints().containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a username field.
|
||||
*/
|
||||
fun HtmlInfo?.isUsernameField(): Boolean = isInputField &&
|
||||
hints().containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a cardholder name field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardholderNameField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card number field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardNumberField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card expiration month field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardExpirationMonthField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card expiration year field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardExpirationYearField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card expiration date field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardExpirationDateField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card security code field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardSecurityCodeField(): Boolean = isInputField &&
|
||||
hints().containsAnyPatterns(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS)
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a card brand field.
|
||||
*/
|
||||
fun HtmlInfo?.isCardBrandField(): Boolean = isInputField &&
|
||||
hints().containsAnyTerms(SUPPORTED_RAW_CARD_BRAND_HINTS)
|
||||
|
||||
/**
|
||||
* Attributes that can be used as hints to determine the type of data the associated node expects.
|
||||
*
|
||||
* This function is untestable as [HtmlInfo] contains [android.util.Pair] which requires
|
||||
* instrumentation testing.
|
||||
*
|
||||
* @see IGNORED_RAW_HINTS
|
||||
* @see SUPPORTED_HTML_ATTRIBUTE_HINTS
|
||||
*/
|
||||
fun HtmlInfo?.hints(): List<String> = this
|
||||
?.let { htmlInfo ->
|
||||
htmlInfo
|
||||
.attributes
|
||||
// Filter out attributes with null values or values that match ignored raw hints
|
||||
?.filter { attribute ->
|
||||
attribute.second != null &&
|
||||
!attribute.second.containsAnyTerms(IGNORED_RAW_HINTS)
|
||||
@OmitFromCoverage
|
||||
fun HtmlInfo?.isPasswordField(): Boolean =
|
||||
this
|
||||
?.let { htmlInfo ->
|
||||
if (htmlInfo.isInputField) {
|
||||
htmlInfo
|
||||
.attributes
|
||||
?.any {
|
||||
it.first == "type" && it.second == "password"
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
// Filter attributes that match supported HTML attribute hints
|
||||
?.filter { attribute ->
|
||||
attribute.first.containsAnyTerms(
|
||||
terms = SUPPORTED_HTML_ATTRIBUTE_HINTS,
|
||||
ignoreCase = true,
|
||||
)
|
||||
}
|
||||
?: false
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents a username field.
|
||||
*
|
||||
* This function is untestable as [HtmlInfo] contains [android.util.Pair] which requires
|
||||
* instrumentation testing.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun HtmlInfo?.isUsernameField(): Boolean =
|
||||
this
|
||||
?.let { htmlInfo ->
|
||||
if (htmlInfo.isInputField) {
|
||||
htmlInfo
|
||||
.attributes
|
||||
?.any {
|
||||
it.first == "type" && it.second == "email"
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
.orEmpty()
|
||||
.mapNotNull { it.second }
|
||||
}
|
||||
.orEmpty()
|
||||
}
|
||||
?: false
|
||||
|
||||
/**
|
||||
* Whether this [HtmlInfo] represents an input field.
|
||||
*/
|
||||
val HtmlInfo?.isInputField: Boolean get() = this?.tag == "input"
|
||||
|
||||
/**
|
||||
* Checks if the list of strings contains any of the specified patterns.
|
||||
*/
|
||||
private fun List<String>.containsAnyPatterns(patterns: List<Regex>): Boolean = this
|
||||
.any { string -> patterns.any { pattern -> string.matches(pattern) } }
|
||||
|
||||
/**
|
||||
* Checks if the list of strings contains any of the specified terms.
|
||||
*/
|
||||
private fun List<String>.containsAnyTerms(terms: List<String>): Boolean =
|
||||
this.any { string ->
|
||||
string
|
||||
.toLowerCaseAndStripNonAlpha()
|
||||
.containsAnyTerms(terms)
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported attribute keys whose value can represent an autofill hint.
|
||||
*/
|
||||
private val SUPPORTED_HTML_ATTRIBUTE_HINTS: List<String> = listOf(
|
||||
"name",
|
||||
"label",
|
||||
"type",
|
||||
"hint",
|
||||
"autofill",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Whether this [Int] is a password [InputType].
|
||||
@@ -29,3 +32,15 @@ val Int.isUsernameInputType: Boolean
|
||||
* Whether this [Int] contains [flag].
|
||||
*/
|
||||
private fun Int.hasFlag(flag: Int): Boolean = (this and flag) == flag
|
||||
|
||||
/**
|
||||
* Starting from an initial pending intent flag. (ex: [PendingIntent.FLAG_CANCEL_CURRENT])
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun Int.toPendingIntentMutabilityFlag(): Int =
|
||||
// Mutable flag was added on API level 31
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
this or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -16,20 +16,3 @@ fun String.containsAnyTerms(
|
||||
ignoreCase = ignoreCase,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this string matches any of these [expressions].
|
||||
*/
|
||||
fun String.matchesAnyExpressions(
|
||||
expressions: List<Regex>,
|
||||
): Boolean =
|
||||
expressions.any {
|
||||
this.matches(regex = it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this [String] to lowercase and remove all non-alpha characters.
|
||||
*/
|
||||
fun String.toLowerCaseAndStripNonAlpha(): String = this
|
||||
.lowercase()
|
||||
.replace(Regex("[^a-z]"), "")
|
||||
|
||||
@@ -3,9 +3,7 @@ package com.x8bit.bitwarden.data.autofill.util
|
||||
import android.app.assist.AssistStructure
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillHint
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
|
||||
/**
|
||||
@@ -13,13 +11,39 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
*/
|
||||
private const val DEFAULT_SCHEME: String = "https"
|
||||
|
||||
/**
|
||||
* The set of raw autofill hints that should be ignored.
|
||||
*/
|
||||
private val IGNORED_RAW_HINTS: List<String> = listOf(
|
||||
"search",
|
||||
"find",
|
||||
"recipient",
|
||||
"edit",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported password autofill hints.
|
||||
*/
|
||||
private val SUPPORTED_RAW_PASSWORD_HINTS: List<String> = listOf(
|
||||
"password",
|
||||
"pswd",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported raw autofill hints.
|
||||
*/
|
||||
private val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
||||
"email",
|
||||
"phone",
|
||||
"username",
|
||||
)
|
||||
|
||||
/**
|
||||
* The supported autofill Android View hints.
|
||||
*/
|
||||
private val SUPPORTED_VIEW_HINTS: List<String> = listOf(
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
@@ -36,7 +60,7 @@ private val AssistStructure.ViewNode.isInputField: Boolean
|
||||
?.let {
|
||||
try {
|
||||
Class.forName(it)
|
||||
} catch (_: ClassNotFoundException) {
|
||||
} catch (e: ClassNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -54,7 +78,11 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
.autofillId
|
||||
// We only care about nodes with a valid `AutofillId`.
|
||||
?.let { nonNullAutofillId ->
|
||||
if (supportedAutofillHint != null || this.isInputField) {
|
||||
val supportedHint = this
|
||||
.autofillHints
|
||||
?.firstOrNull { SUPPORTED_VIEW_HINTS.contains(it) }
|
||||
|
||||
if (supportedHint != null || this.isInputField) {
|
||||
val autofillOptions = this
|
||||
.autofillOptions
|
||||
.orEmpty()
|
||||
@@ -71,65 +99,22 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
buildAutofillView(
|
||||
autofillOptions = autofillOptions,
|
||||
autofillViewData = autofillViewData,
|
||||
autofillHint = supportedAutofillHint,
|
||||
supportedHint = supportedHint,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first supported autofill hint for this view node, or null if none are found.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.supportedAutofillHint: AutofillHint?
|
||||
get() = firstSupportedAutofillHintOrNull()
|
||||
?: when {
|
||||
this.isUsernameField -> AutofillHint.USERNAME
|
||||
this.isPasswordField -> AutofillHint.PASSWORD
|
||||
this.isCardExpirationMonthField -> AutofillHint.CARD_EXPIRATION_MONTH
|
||||
this.isCardExpirationYearField -> AutofillHint.CARD_EXPIRATION_YEAR
|
||||
this.isCardExpirationDateField -> AutofillHint.CARD_EXPIRATION_DATE
|
||||
this.isCardNumberField -> AutofillHint.CARD_NUMBER
|
||||
this.isCardSecurityCodeField -> AutofillHint.CARD_SECURITY_CODE
|
||||
this.isCardholderNameField -> AutofillHint.CARD_CARDHOLDER
|
||||
this.isCardBrandField -> AutofillHint.CARD_BRAND
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first supported autofill hint from the view node's autofillHints, or null if none are
|
||||
* found.
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.firstSupportedAutofillHintOrNull(): AutofillHint? =
|
||||
autofillHints
|
||||
?.firstOrNull { SUPPORTED_VIEW_HINTS.contains(it) }
|
||||
?.toBitwardenAutofillHintOrNull()
|
||||
|
||||
private fun String.toBitwardenAutofillHintOrNull(): AutofillHint? =
|
||||
when (this) {
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> AutofillHint.CARD_EXPIRATION_MONTH
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> AutofillHint.CARD_EXPIRATION_YEAR
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE -> AutofillHint.CARD_EXPIRATION_DATE
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> AutofillHint.CARD_NUMBER
|
||||
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> AutofillHint.CARD_SECURITY_CODE
|
||||
View.AUTOFILL_HINT_PASSWORD -> AutofillHint.PASSWORD
|
||||
View.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||
View.AUTOFILL_HINT_USERNAME,
|
||||
-> AutofillHint.USERNAME
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] and [autofillViewData] into an [AutofillView].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
autofillOptions: List<String>,
|
||||
autofillViewData: AutofillView.Data,
|
||||
autofillHint: AutofillHint?,
|
||||
): AutofillView = when (autofillHint) {
|
||||
AutofillHint.CARD_EXPIRATION_MONTH -> {
|
||||
supportedHint: String?,
|
||||
): AutofillView = when {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
val monthValue = this
|
||||
.autofillValue
|
||||
?.extractMonthValue(
|
||||
@@ -142,67 +127,37 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_EXPIRATION_YEAR -> {
|
||||
val yearValue = this
|
||||
.autofillValue
|
||||
?.extractYearValue(
|
||||
autofillOptions = autofillOptions,
|
||||
)
|
||||
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
yearValue = yearValue,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_EXPIRATION_DATE -> {
|
||||
AutofillView.Card.ExpirationDate(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_NUMBER -> {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
|
||||
AutofillView.Card.Number(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_SECURITY_CODE -> {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_CARDHOLDER -> {
|
||||
AutofillView.Card.CardholderName(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.PASSWORD -> {
|
||||
this.isPasswordField(supportedHint) -> {
|
||||
AutofillView.Login.Password(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.USERNAME -> {
|
||||
this.isUsernameField(supportedHint) -> {
|
||||
AutofillView.Login.Username(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
|
||||
AutofillHint.CARD_BRAND -> {
|
||||
val brandValue = this.autofillValue
|
||||
?.extractCardBrandValue(
|
||||
autofillOptions = autofillOptions,
|
||||
)
|
||||
AutofillView.Card.Brand(
|
||||
data = autofillViewData,
|
||||
brandValue = brandValue,
|
||||
)
|
||||
}
|
||||
|
||||
null -> {
|
||||
else -> {
|
||||
AutofillView.Unused(
|
||||
data = autofillViewData,
|
||||
)
|
||||
@@ -212,117 +167,41 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a password field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isPasswordField: Boolean
|
||||
get() {
|
||||
val isUsernameField = this.isUsernameField
|
||||
if (
|
||||
this.inputType.isPasswordInputType &&
|
||||
!this.containsIgnoredHintTerms() &&
|
||||
!isUsernameField
|
||||
) {
|
||||
return true
|
||||
}
|
||||
fun AssistStructure.ViewNode.isPasswordField(
|
||||
supportedHint: String?,
|
||||
): Boolean {
|
||||
if (supportedHint == View.AUTOFILL_HINT_PASSWORD) return true
|
||||
|
||||
return hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
htmlInfo.isPasswordField()
|
||||
}
|
||||
val isInvalidField = this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true
|
||||
val isUsernameField = this.isUsernameField(supportedHint)
|
||||
if (this.inputType.isPasswordInputType && !isInvalidField && !isUsernameField) return true
|
||||
|
||||
return this
|
||||
.htmlInfo
|
||||
.isPasswordField()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] includes any password specific terms.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun AssistStructure.ViewNode.hasPasswordTerms(): Boolean =
|
||||
fun AssistStructure.ViewNode.hasPasswordTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
this.htmlInfo.hints().any { it.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) }
|
||||
this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a username field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isUsernameField: Boolean
|
||||
get() = inputType.isUsernameInputType ||
|
||||
fun AssistStructure.ViewNode.isUsernameField(
|
||||
supportedHint: String?,
|
||||
): Boolean =
|
||||
supportedHint == View.AUTOFILL_HINT_USERNAME ||
|
||||
supportedHint == View.AUTOFILL_HINT_EMAIL_ADDRESS ||
|
||||
inputType.isUsernameInputType ||
|
||||
idEntry?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
hint?.containsAnyTerms(SUPPORTED_RAW_USERNAME_HINTS) == true ||
|
||||
htmlInfo.isUsernameField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration month field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardExpirationMonthField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_MONTH_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationMonthField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration year field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardExpirationYearField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_YEAR_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationYearField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card expiration date field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardExpirationDateField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_EXP_DATE_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardExpirationDateField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card number field based.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardNumberField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_NUMBER_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardNumberField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card security code field based.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardSecurityCodeField: Boolean
|
||||
get() =
|
||||
idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARD_SECURITY_CODE_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardSecurityCodeField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a cardholder name field based.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardholderNameField: Boolean
|
||||
get() = idEntry?.matchesAnyExpressions(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS) == true ||
|
||||
hint?.matchesAnyExpressions(SUPPORTED_RAW_CARDHOLDER_NAME_HINT_PATTERNS) == true ||
|
||||
htmlInfo.isCardholderNameField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a card brand field.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal val AssistStructure.ViewNode.isCardBrandField: Boolean
|
||||
get() = idEntry
|
||||
?.toLowerCaseAndStripNonAlpha()
|
||||
?.containsAnyTerms(SUPPORTED_RAW_CARD_BRAND_HINTS) == true ||
|
||||
hint
|
||||
?.toLowerCaseAndStripNonAlpha()
|
||||
?.containsAnyTerms(SUPPORTED_RAW_CARD_BRAND_HINTS) == true ||
|
||||
htmlInfo.isCardBrandField()
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] contains any ignored hint terms.
|
||||
*/
|
||||
private fun AssistStructure.ViewNode.containsIgnoredHintTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.htmlInfo.hints().any { it.containsAnyTerms(IGNORED_RAW_HINTS) }
|
||||
|
||||
/**
|
||||
* The website that this [AssistStructure.ViewNode] is a part of representing.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user