diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7f35d2cb1..c3847e761 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "dotnet-format" ] + }, + "cake.tool": { + "version": "2.2.0", + "commands": [ + "dotnet-cake" + ] } } } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2ff9e650c..b24500bc9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,7 +22,7 @@ ## Before you submit -- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required) -- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required) -- [ ] This change requires a **documentation update** (notify the documentation team) -- [ ] This change has particular **deployment requirements** (notify the DevOps team) +- Please check for formatting errors (`dotnet format --verify-no-changes`) (required) +- Please add **unit tests** where it makes sense to do so (encouraged but not required) +- If this change requires a **documentation update** - notify the documentation team +- If this change has particular **deployment requirements** - notify the DevOps team diff --git a/renovate.json b/.github/renovate.json similarity index 100% rename from renovate.json rename to .github/renovate.json diff --git a/.github/resources/export-options-app-store.plist b/.github/resources/export-options-app-store.plist index df3ed635b..dc991f2a8 100644 --- a/.github/resources/export-options-app-store.plist +++ b/.github/resources/export-options-app-store.plist @@ -14,6 +14,10 @@ Dist: Extension 2021 com.8bit.bitwarden.share-extension Dist: Share Extension 2021 + com.8bit.bitwarden.watchkitapp + Dist: Bitwarden Watch App + com.8bit.bitwarden.watchkitapp.watchkitextension + Dist: Bitwarden Watch App Extension diff --git a/.github/secrets/dist_watch_app.mobileprovision.gpg b/.github/secrets/dist_watch_app.mobileprovision.gpg new file mode 100644 index 000000000..8981acabe Binary files /dev/null and b/.github/secrets/dist_watch_app.mobileprovision.gpg differ diff --git a/.github/secrets/dist_watch_app_extension.mobileprovision.gpg b/.github/secrets/dist_watch_app_extension.mobileprovision.gpg new file mode 100644 index 000000000..57b1bf2dd Binary files /dev/null and b/.github/secrets/dist_watch_app_extension.mobileprovision.gpg differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a47d79835..1087f9404 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,15 +42,15 @@ jobs: id: branch-check run: | if [[ $(git ls-remote --heads origin rc) ]]; then - echo "::set-output name=rc_branch_exists::1" + echo "rc_branch_exists=1" >> $GITHUB_OUTPUT else - echo "::set-output name=rc_branch_exists::0" + echo "rc_branch_exists=0" >> $GITHUB_OUTPUT fi if [[ $(git ls-remote --heads origin hotfix-rc) ]]; then - echo "::set-output name=hotfix_branch_exists::1" + echo "hotfix_branch_exists=1" >> $GITHUB_OUTPUT else - echo "::set-output name=hotfix_branch_exists::0" + echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT fi shell: bash @@ -59,6 +59,10 @@ jobs: name: Android runs-on: windows-2022 needs: setup + strategy: + fail-fast: false + matrix: + variant: ['prod', 'qa'] steps: - name: Setup NuGet uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6 @@ -67,7 +71,7 @@ jobs: - name: Set up MSBuild uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab - + - name: Work Around for broken Windows 2022 Runner Image run: | Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\" @@ -87,7 +91,6 @@ jobs: Write-Host "components were not installed" exit 1 } - - name: Print environment run: | nuget help | grep Version @@ -98,7 +101,8 @@ jobs: - name: Checkout repo uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 - + with: + fetch-depth: 0 - name: Decrypt secrets env: DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} @@ -109,12 +113,17 @@ jobs: --output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ --output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg - gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ - --output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ --output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg shell: bash - + - name: Decrypt secrets - Google Services + if: ${{ matrix.variant == 'prod' }} + env: + DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} + run: | + gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ + --output ./src/Android/google-services.json ./.github/secrets/google-services.json.gpg + shell: bash - name: Increment version run: | BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER)) @@ -142,26 +151,35 @@ jobs: run: dotnet test test/Core.Test/Core.Test.csproj - name: Build Play Store publisher + if: ${{ matrix.variant == 'prod' }} run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release - - name: Build for Play Store + - name: Setup Android build (${{ matrix.variant }}) + run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }} + + - name: Build Android run: | $configuration = "Release"; Write-Output "########################################" Write-Output "##### Build $configuration Configuration" Write-Output "########################################" - msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration" + shell: pwsh - - name: Sign for Play Store + - name: Sign Android Build env: PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} run: | $androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj"); - + $packageName = "com.x8bit.bitwarden"; + + if ("${{ matrix.variant }}" -ne "prod") + { + $packageName = "com.x8bit.bitwarden.${{ matrix.variant }}"; + } Write-Output "########################################" Write-Output "##### Sign Google Play Bundle Release Configuration" Write-Output "########################################" @@ -175,9 +193,8 @@ jobs: Write-Output "##### Copy Google Play Bundle to project root" Write-Output "########################################" - $signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab"); - $signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab"); - + $signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab"); + $signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab"); Copy-Item $signedAabPath $signedAabDestPath Write-Output "########################################" @@ -193,33 +210,41 @@ jobs: Write-Output "##### Copy Release APK to project root" Write-Output "########################################" - $signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk"); - $signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk"); + $signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk"); + $signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk"); Copy-Item $signedApkPath $signedApkDestPath shell: pwsh - - - name: Upload Play Store .aab artifact + - name: Upload Prod .aab artifact + if: ${{ matrix.variant == 'prod' }} uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden.aab path: ./com.x8bit.bitwarden.aab if-no-files-found: error - - name: Upload Play Store .apk artifact + - name: Upload Prod .apk artifact + if: ${{ matrix.variant == 'prod' }} uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 with: name: com.x8bit.bitwarden.apk path: ./com.x8bit.bitwarden.apk if-no-files-found: error + - name: Upload Other .apk artifact + if: ${{ matrix.variant != 'prod' }} + uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 + with: + name: com.x8bit.bitwarden.${{ matrix.variant }}.apk + path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk + if-no-files-found: error + - name: Deploy to Play Store - if: | - (github.ref == 'refs/heads/master' - && needs.setup.outputs.rc_branch_exists == 0 - && needs.setup.outputs.hotfix_branch_exists == 0) - || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) - || github.ref == 'refs/heads/hotfix-rc' + if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master' + && needs.setup.outputs.rc_branch_exists == 0 + && needs.setup.outputs.hotfix_branch_exists == 0) + || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) + || github.ref == 'refs/heads/hotfix-rc' ) }} run: | PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll" CREDS_PATH="$HOME/secrets/play_creds.json" @@ -441,10 +466,17 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f - with: - keyvault: "bitwarden-prod-kv" - secrets: "appcenter-ios-token" + env: + KEYVAULT: bitwarden-prod-kv + SECRETS: | + appcenter-ios-token + run: | + for i in ${SECRETS//,/ } + do + VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) + echo "::add-mask::$VALUE" + echo "$i=$VALUE" >> $GITHUB_OUTPUT + done - name: Decrypt secrets env: @@ -465,6 +497,12 @@ jobs: gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ --output $HOME/secrets/dist_share_extension.mobileprovision \ ./.github/secrets/dist_share_extension.mobileprovision.gpg + gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ + --output $HOME/secrets/dist_watch_app.mobileprovision \ + ./.github/secrets/dist_watch_app.mobileprovision.gpg + gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ + --output $HOME/secrets/dist_watch_app_extension.mobileprovision \ + ./.github/secrets/dist_watch_app_extension.mobileprovision.gpg shell: bash - name: Increment version @@ -479,6 +517,9 @@ jobs: perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist perl -0777 -pi.bak -e 's/CFBundleVersion<\/key>\s*1<\/string>/CFBundleVersion<\/key>\n\t'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist + cd src/watchOS/bitwarden + agvtool new-version -all $BUILD_NUMBER + cd ../../.. shell: bash - name: Update Entitlements @@ -513,6 +554,8 @@ jobs: BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision + WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_watch_app.mobileprovision + WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_extension.mobileprovision PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles mkdir -p "$PROFILES_DIR_PATH" @@ -528,6 +571,28 @@ jobs: SHARE_EXTENSION_UUID=$(grep UUID -A1 -a $SHARE_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") cp $SHARE_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$SHARE_EXTENSION_UUID.mobileprovision" + + WATCH_APP_UUID=$(grep UUID -A1 -a $WATCH_APP_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") + cp $WATCH_APP_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_UUID.mobileprovision" + + WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}") + cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision" + shell: bash + + - name: Bulid WatchApp + run: | + echo "########################################" + echo "##### Build WatchApp with Release Configuration" + echo "########################################" + + xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden + + echo "########################################" + echo "##### Done" + echo "########################################" + cd src/watchOS + ls -R + cd ../.. shell: bash - name: Restore packages @@ -635,10 +700,17 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f - with: - keyvault: "bitwarden-prod-kv" - secrets: "crowdin-api-token" + env: + KEYVAULT: bitwarden-prod-kv + SECRETS: | + crowdin-api-token + run: | + for i in ${SECRETS//,/ } + do + VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) + echo "::add-mask::$VALUE" + echo "$i=$VALUE" >> $GITHUB_OUTPUT + done - name: Upload Sources uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415 @@ -695,11 +767,18 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f if: failure() - with: - keyvault: "bitwarden-prod-kv" - secrets: "devops-alerts-slack-webhook-url" + env: + KEYVAULT: bitwarden-prod-kv + SECRETS: | + devops-alerts-slack-webhook-url + run: | + for i in ${SECRETS//,/ } + do + VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) + echo "::add-mask::$VALUE" + echo "$i=$VALUE" >> $GITHUB_OUTPUT + done - name: Notify Slack on failure uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33 diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 90fb1f826..6dd2e3c4f 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -24,10 +24,10 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403 + uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" - secrets: "crowdin-api-token" + keyvault: "bitwarden-prod-kv" + secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" - name: Download translations uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face @@ -40,10 +40,12 @@ jobs: upload_sources: false upload_translations: false download_translations: true - github_user_name: "github-actions" - github_user_email: "<>" + github_user_name: "bitwarden-devops-bot" + github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com" commit_message: "Autosync the updated translations" localization_branch_name: crowdin-auto-sync create_pull_request: true pull_request_title: "Autosync Crowdin Translations" pull_request_body: "Autosync the updated translations" + gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} + gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6a054d25..60c5398f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,6 @@ --- name: Release +run-name: Release ${{ inputs.release_type }} on: workflow_dispatch: @@ -51,9 +52,10 @@ jobs: id: branch run: | BRANCH_NAME=$(basename ${{ github.ref }}) - echo "::set-output name=branch-name::$BRANCH_NAME" + echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT - name: Create GitHub deployment + if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48 id: deployment with: @@ -72,7 +74,7 @@ jobs: workflow_conclusion: success branch: ${{ steps.branch.outputs.branch-name }} - - name: Download all artifacts + - name: Dry Run - Download all artifacts if: ${{ github.event.inputs.release_type == 'Dry Run' }} uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10 with: @@ -99,7 +101,7 @@ jobs: draft: true - name: Update deployment status to Success - if: ${{ success() }} + if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86 with: token: '${{ secrets.GITHUB_TOKEN }}' @@ -107,7 +109,7 @@ jobs: deployment-id: ${{ steps.deployment.outputs.deployment_id }} - name: Update deployment status to Failure - if: ${{ failure() }} + if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86 with: token: '${{ secrets.GITHUB_TOKEN }}' @@ -133,7 +135,7 @@ jobs: branch: ${{ needs.release.outputs.branch-name }} name: com.x8bit.bitwarden-fdroid.apk - - name: Download F-Droid .apk artifact + - name: Dry Run - Download F-Droid .apk artifact if: ${{ github.event.inputs.release_type == 'Dry Run' }} uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10 with: diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index fefb1f191..2af6da884 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -2,39 +2,38 @@ name: Version Auto Bump on: - release: - types: [published] + push: + tags: + - v** jobs: - setup: name: "Setup" - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 outputs: version_number: ${{ steps.version.outputs.new-version }} steps: - name: Checkout Branch uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - - name: Get version to bump + - name: Calculate bumped version id: version env: - RELEASE_TAG: ${{ github.event.release.tag }} + RELEASE_TAG: ${{ github.ref }} run: | - CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/') - CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/') - echo $CURR_VER + CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/') + CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/') + echo "Current Major: $CURR_MAJOR" + echo "Current Patch: $CURR_PATCH" - ((CURR_VER++)) - NEW_VER=$CURR_MAJOR$CURR_VER - - echo $NEW_VER - - echo "::set-output name=new-version::$NEW_VER" + NEW_PATCH=$((CURR_PATCH+1)) + NEW_VER=$CURR_MAJOR.$NEW_PATCH + echo "New Version: $NEW_VER" + echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT trigger_version_bump: name: "Trigger version bump workflow" - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - setup steps: @@ -45,13 +44,10 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - env: - KEYVAULT: bitwarden-prod-kv - SECRET: "github-pat-bitwarden-devops-bot-repo-scope" - run: | - VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv) - echo "::add-mask::$VALUE" - echo "::set-output name=$SECRET::$VALUE" + uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af + with: + keyvault: "bitwarden-prod-kv" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Call GitHub API to trigger workflow bump env: diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index b3c5a58ef..9347255f7 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -16,9 +16,28 @@ jobs: - name: Checkout Branch uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 + - name: Login to Azure - Prod Subscription + uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af + with: + keyvault: "bitwarden-prod-kv" + secrets: "github-gpg-private-key, github-gpg-private-key-passphrase" + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1 + with: + gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} + passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} + git_user_signingkey: true + git_commit_gpgsign: true + - name: Create Version Branch - run: | - git switch -c version_bump_${{ github.event.inputs.version_number }} + run: git switch -c version_bump_${{ github.event.inputs.version_number }} - name: Bump Version - Android XML uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945 @@ -52,23 +71,22 @@ jobs: - name: Setup git run: | - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" + git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" + git config --local user.name "bitwarden-devops-bot" - name: Check if version changed id: version-changed run: | if [ -n "$(git status --porcelain)" ]; then - echo "::set-output name=changes_to_commit::TRUE" + echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT else - echo "::set-output name=changes_to_commit::FALSE" + echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT echo "No changes to commit!"; fi - name: Commit files if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - run: | - git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a + run: git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a - name: Push changes if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} diff --git a/.gitignore b/.gitignore index 383caf2b4..107fad1df 100644 --- a/.gitignore +++ b/.gitignore @@ -208,4 +208,130 @@ FakesAssemblies/ # Other project.lock.json .DS_Store -src/App/Css \ No newline at end of file +src/App/Css +tools + +# Created by https://www.toptal.com/developers/gitignore/api/swift,objective-c +# Edit at https://www.toptal.com/developers/gitignore?templates=swift,objective-c + +### Objective-C ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Objective-C Patch ### + +### Swift ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + + + + + + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + + +# End of https://www.toptal.com/developers/gitignore/api/swift,objective-c diff --git a/README.md b/README.md index 3a268ac0b..0e9534d08 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin # Build/Run -Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. +Please refer to the [Mobile section](https://contributing.bitwarden.com/getting-started/clients/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. # We're Hiring! diff --git a/build.cake b/build.cake new file mode 100644 index 000000000..f30c8404b --- /dev/null +++ b/build.cake @@ -0,0 +1,346 @@ +#addin nuget:?package=Cake.FileHelpers&version=5.0.0 +#addin nuget:?package=Cake.AndroidAppManifest&version=1.1.2 +#addin nuget:?package=Cake.Plist&version=0.7.0 +#addin nuget:?package=Cake.Incubator&version=7.0.0 +#tool dotnet:?package=GitVersion.Tool&version=5.10.3 +using Path = System.IO.Path; + +var debugScript = Argument("debugScript", false); +var target = Argument("target", "Default"); +var configuration = Argument("configuration", "Release"); +var variant = Argument("variant", "dev"); + +abstract record VariantConfig( + string AppName, + string AndroidPackageName, + string iOSBundleId, + string ApsEnvironment + ); + +const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden"; +const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden"; + +record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development"); +record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development"); +record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production"); +record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production"); + +VariantConfig GetVariant() => variant.ToLower() switch{ + "qa" => new QA(), + "beta" => new Beta(), + "prod" => new Prod(), + _ => new Dev() +}; + +GitVersion _gitVersion; //will be set by GetGitInfo task +var _slnPath = Path.Combine(""); //base path used to access files. If build.cake file is moved, just update this +string _androidPackageName = string.Empty; //will be set by UpdateAndroidManifest task +string CreateFeatureBranch(string prevVersionName, GitVersion git) => $"{prevVersionName}-{git.BranchName.Replace("/","-")}"; +string GetVersionName(string prevVersionName, VariantConfig buildVariant, GitVersion git) => buildVariant is Prod? prevVersionName : CreateFeatureBranch(prevVersionName, git); +int CreateBuildNumber(int previousNumber) => ++previousNumber; + +Task("GetGitInfo") + .Does(()=> { + _gitVersion = GitVersion(new GitVersionSettings()); + + if(debugScript) + { + Information($"GitVersion Dump:\n{_gitVersion.Dump()}"); + } + + Information("Git data Load successfully."); + }); + +#region Android +Task("UpdateAndroidAppIcon") + .Does(()=>{ + //TODO we'll implement variant icons later + //manifest.ApplicationIcon = "@mipmap/ic_launcher"; + Information($"Updated Androix App Icon with success"); + }); + + +Task("UpdateAndroidManifest") + .IsDependentOn("GetGitInfo") + .Does(()=> + { + var buildVariant = GetVariant(); + var manifestPath = Path.Combine(_slnPath, "src", "Android", "Properties", "AndroidManifest.xml"); + + // Cake.AndroidAppManifest doesn't currently enable us to access nested items so, quick (not ideal) fix: + var manifestText = FileReadText(manifestPath); + manifestText = manifestText.Replace("com.x8bit.bitwarden.", buildVariant.AndroidPackageName + "."); + manifestText = manifestText.Replace("android:label=\"Bitwarden\"", $"android:label=\"{buildVariant.AppName}\""); + FileWriteText(manifestPath, manifestText); + + var manifest = DeserializeAppManifest(manifestPath); + + var prevVersionCode = manifest.VersionCode; + var prevVersionName = manifest.VersionName; + _androidPackageName = manifest.PackageName; + + //manifest.VersionCode = CreateBuildNumber(prevVersionCode); + manifest.VersionName = GetVersionName(prevVersionName, buildVariant, _gitVersion); + manifest.PackageName = buildVariant.AndroidPackageName; + manifest.ApplicationLabel = buildVariant.AppName; + + //Information($"AndroidManigest.xml VersionCode from {prevVersionCode} to {manifest.VersionCode}"); + Information($"AndroidManigest.xml VersionName from {prevVersionName} to {manifest.VersionName}"); + Information($"AndroidManigest.xml PackageName from {_androidPackageName} to {buildVariant.AndroidPackageName}"); + Information($"AndroidManigest.xml ApplicationLabel to {buildVariant.AppName}"); + + SerializeAppManifest(manifestPath, manifest); + + Information("AndroidManifest updated with success!"); + }); + +void ReplaceInFile(string filePath, string oldtext, string newtext) +{ + var fileText = FileReadText(filePath); + + if(string.IsNullOrEmpty(fileText) || !fileText.Contains(oldtext)) + { + throw new Exception($"Couldn't find {filePath} or it didn't contain: {oldtext}"); + } + + fileText = fileText.Replace(oldtext, newtext); + + FileWriteText(filePath, fileText); + Information($"{filePath} modified successfully."); +} + +Task("UpdateAndroidCodeFiles") + .IsDependentOn("UpdateAndroidManifest") + .Does(()=> { + var buildVariant = GetVariant(); + + //We're not using _androidPackageName here because the codefile is currently slightly different string than the one in AndroidManifest.xml + var keyName = "com.8bit.bitwarden"; + var fixedPackageName = buildVariant.AndroidPackageName.Replace("x8bit", "8bit"); + var filePath = Path.Combine(_slnPath, "src", "Android", "Services", "BiometricService.cs"); + ReplaceInFile(filePath, keyName, fixedPackageName); + + var packageFileList = new string[] { + Path.Combine(_slnPath, "src", "Android", "MainActivity.cs"), + Path.Combine(_slnPath, "src", "Android", "MainApplication.cs"), + Path.Combine(_slnPath, "src", "Android", "Constants.cs"), + Path.Combine(_slnPath, "src", "Android", "Accessibility", "AccessibilityService.cs"), + Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillHelpers.cs"), + Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "ClearClipboardAlarmReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "EventUploadReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "PackageReplacedReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Receivers", "RestrictionsChangedReceiver.cs"), + Path.Combine(_slnPath, "src", "Android", "Services", "DeviceActionService.cs"), + Path.Combine(_slnPath, "src", "Android", "Services", "FileService.cs"), + Path.Combine(_slnPath, "src", "Android", "Tiles", "AutofillTileService.cs"), + Path.Combine(_slnPath, "src", "Android", "Tiles", "GeneratorTileService.cs"), + Path.Combine(_slnPath, "src", "Android", "Tiles", "MyVaultTileService.cs"), + Path.Combine(_slnPath, "src", "Android", "google-services.json"), + Path.Combine(_slnPath, "store", "google", "Publisher", "Program.cs"), + }; + + foreach(string path in packageFileList) + { + ReplaceInFile(path, "com.x8bit.bitwarden", buildVariant.AndroidPackageName); + } + + var labelFileList = new string[] { + Path.Combine(_slnPath, "src", "Android", "Autofill", "AutofillService.cs"), + }; + + foreach(string path in labelFileList) + { + ReplaceInFile(path, "Bitwarden\"", $"{buildVariant.AppName}\""); + } + }); +#endregion Android + +#region iOS +enum iOSProjectType +{ + Null, + MainApp, + Autofill, + Extension, + ShareExtension +} + +string GetiOSBundleId(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch +{ + iOSProjectType.Autofill => $"{buildVariant.iOSBundleId}.autofill", + iOSProjectType.Extension => $"{buildVariant.iOSBundleId}.find-login-action-extension", + iOSProjectType.ShareExtension => $"{buildVariant.iOSBundleId}.share-extension", + _ => buildVariant.iOSBundleId +}; + +string GetiOSBundleName(VariantConfig buildVariant, iOSProjectType projectType) => projectType switch +{ + iOSProjectType.Autofill => $"{buildVariant.AppName} Autofill", + iOSProjectType.Extension => $"{buildVariant.AppName} Extension", + iOSProjectType.ShareExtension => $"{buildVariant.AppName} Share Extension", + _ => buildVariant.AppName +}; + +private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, GitVersion git, iOSProjectType projectType = iOSProjectType.MainApp) +{ + var plistFile = File(plistPath); + dynamic plist = DeserializePlist(plistFile); + + var prevVersionName = plist["CFBundleShortVersionString"]; + var prevVersionString = plist["CFBundleVersion"]; + var prevVersion = int.Parse(plist["CFBundleVersion"]); + var prevBundleId = plist["CFBundleIdentifier"]; + var prevBundleName = plist["CFBundleName"]; + //var newVersion = CreateBuildNumber(prevVersion).ToString(); + var newVersionName = GetVersionName(prevVersionName, buildVariant, git); + var newBundleId = GetiOSBundleId(buildVariant, projectType); + var newBundleName = GetiOSBundleName(buildVariant, projectType); + + plist["CFBundleName"] = newBundleName; + plist["CFBundleDisplayName"] = newBundleName; + //plist["CFBundleVersion"] = newVersion; + plist["CFBundleShortVersionString"] = newVersionName; + plist["CFBundleIdentifier"] = newBundleId; + + if(projectType == iOSProjectType.MainApp) + { + plist["CFBundleURLTypes"][0]["CFBundleURLName"] = $"{buildVariant.iOSBundleId}.url"; + } + + if(projectType == iOSProjectType.Extension) + { + var keyText = plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"]; + plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId); + } + + SerializePlist(plistFile, plist); + + Information($"Changed app name from {prevBundleName} to {newBundleName}"); + //Information($"Changed Bundle Version from {prevVersion} to {newVersion}"); + Information($"Changed Bundle Short Version name from {prevVersionName} to {newVersionName}"); + Information($"Changed Bundle Identifier from {prevBundleId} to {newBundleId}"); + Information($"{plistPath} updated with success!"); +} + +private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant) +{ + var EntitlementlistFile = File(entitlementsPath); + dynamic Entitlements = DeserializePlist(EntitlementlistFile); + + Entitlements["aps-environment"] = buildVariant.ApsEnvironment; + Entitlements["keychain-access-groups"] = new List() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId }; + Entitlements["com.apple.security.application-groups"] = new List() { $"group.{buildVariant.iOSBundleId}" };; + + Information($"Changed ApsEnvironment name to {buildVariant.ApsEnvironment}"); + Information($"Changed keychain-access-groups bundleID to {buildVariant.iOSBundleId}"); + + SerializePlist(EntitlementlistFile, Entitlements); + + Information($"{entitlementsPath} updated with success!"); +} + +Task("UpdateiOSIcon") + .Does(()=>{ + //TODO we'll implement variant icons later + Information($"Updating IOS App Icon"); + }); + +Task("UpdateiOSPlist") + .IsDependentOn("GetGitInfo") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSAutofillPlist") + .IsDependentOn("GetGitInfo") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSExtensionPlist") + .IsDependentOn("GetGitInfo") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSShareExtensionPlist") + .IsDependentOn("GetGitInfo") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist"); + var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist"); + UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension); + UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); + }); + +Task("UpdateiOSCodeFiles") + .IsDependentOn("UpdateiOSPlist") + .Does(()=> { + var buildVariant = GetVariant(); + var fileList = new string[] { + Path.Combine(_slnPath, "src", "iOS.Core", "Utilities", "iOSCoreHelpers.cs"), + Path.Combine(_slnPath, "src", "iOS.Core", "Constants.cs"), + Path.Combine(".github", "resources", "export-options-ad-hoc.plist"), + Path.Combine(".github", "resources", "export-options-app-store.plist"), + }; + + foreach(string path in fileList) + { + ReplaceInFile(path, "com.8bit.bitwarden", buildVariant.iOSBundleId); + } + }); +#endregion iOS + +#region Main Tasks +Task("Android") + //.IsDependentOn("UpdateAndroidAppIcon") + .IsDependentOn("UpdateAndroidManifest") + .IsDependentOn("UpdateAndroidCodeFiles") + .Does(()=> + { + Information("Android app updated"); + }); + +Task("iOS") + //.IsDependentOn("UpdateiOSIcon") + .IsDependentOn("UpdateiOSPlist") + .IsDependentOn("UpdateiOSAutofillPlist") + .IsDependentOn("UpdateiOSExtensionPlist") + .IsDependentOn("UpdateiOSShareExtensionPlist") + .IsDependentOn("UpdateiOSCodeFiles") + .Does(()=> + { + Information("iOS app updated"); + }); + +Task("Default") + .Does(() => { + var usage = @"Missing target. + +Usage: + dotnet cake build.cake --target (Android | iOS) --variant (dev | qa | beta | prod) + +Options: + --debugScript= Script debug mode. +"; + Information(usage); + }); +#endregion Main Tasks + +RunTarget(target); \ No newline at end of file diff --git a/src/Android/Accessibility/AccessibilityHelpers.cs b/src/Android/Accessibility/AccessibilityHelpers.cs index 81d7fa65f..2f0cd4ce6 100644 --- a/src/Android/Accessibility/AccessibilityHelpers.cs +++ b/src/Android/Accessibility/AccessibilityHelpers.cs @@ -33,6 +33,7 @@ namespace Bit.Droid.Accessibility // - Resources/xml/autofillservice.xml new Browser("alook.browser", "search_fragment_input_view"), new Browser("alook.browser.google", "search_fragment_input_view"), + new Browser("app.vanadium.browser", "url_bar"), new Browser("com.amazon.cloud9", "url"), new Browser("com.android.browser", "url"), new Browser("com.android.chrome", "url_bar"), @@ -66,6 +67,7 @@ namespace Bit.Droid.Accessibility new Browser("com.mmbox.xbrowser", "search_box"), new Browser("com.mycompany.app.soulbrowser", "edit_text"), new Browser("com.naver.whale", "url_bar"), + new Browser("com.neeva.app", "full_url_text_view"), new Browser("com.opera.browser", "url_field"), new Browser("com.opera.browser.beta", "url_field"), new Browser("com.opera.gx", "addressbarEdit"), @@ -90,6 +92,7 @@ namespace Bit.Droid.Accessibility new Browser("io.github.forkmaintainers.iceraven", "mozac_browser_toolbar_url_view"), new Browser("mark.via", "am,an"), new Browser("mark.via.gp", "as"), + new Browser("net.dezor.browser", "url_bar"), new Browser("net.slions.fulguris.full.download", "search"), new Browser("net.slions.fulguris.full.download.debug", "search"), new Browser("net.slions.fulguris.full.playstore", "search"), @@ -367,7 +370,7 @@ namespace Bit.Droid.Accessibility public static string GetUri(AccessibilityNodeInfo root) { - var uri = string.Concat(Constants.AndroidAppProtocol, root.PackageName); + var uri = string.Concat(Core.Constants.AndroidAppProtocol, root.PackageName); if (SupportedBrowsers.ContainsKey(root.PackageName)) { var browser = SupportedBrowsers[root.PackageName]; diff --git a/src/Android/Accessibility/AccessibilityService.cs b/src/Android/Accessibility/AccessibilityService.cs index 1345a5cef..d57ae56a9 100644 --- a/src/Android/Accessibility/AccessibilityService.cs +++ b/src/Android/Accessibility/AccessibilityService.cs @@ -15,7 +15,7 @@ using Bit.Core.Utilities; namespace Bit.Droid.Accessibility { - [Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden")] + [Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden", Exported = true)] [IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })] [MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")] [Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")] diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index f85cd5c15..8d01e1091 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -15,7 +15,7 @@ Properties\AndroidManifest.xml Resources Assets - v11.0 + v13.0 Xamarin.Android.Net.AndroidClientHandler @@ -75,24 +75,24 @@ 2.1.0.4 - 1.8.10 + 1.9.0 - - - - - - + + + + + + 1.7.3 - 122.0.0 + 123.0.8 - - + + - 117.0.1 + 118.0.1.2 @@ -103,8 +103,10 @@ + + @@ -152,6 +154,12 @@ + + + + + + @@ -176,6 +184,7 @@ + @@ -213,6 +222,13 @@ + + + + + + + @@ -280,6 +296,8 @@ + + \ No newline at end of file diff --git a/src/Android/Assets/bwi-font.ttf b/src/Android/Assets/bwi-font.ttf index 358fca186..7c7afd4cd 100644 Binary files a/src/Android/Assets/bwi-font.ttf and b/src/Android/Assets/bwi-font.ttf differ diff --git a/src/Android/Autofill/AutofillConstants.cs b/src/Android/Autofill/AutofillConstants.cs new file mode 100644 index 000000000..9bb1782f7 --- /dev/null +++ b/src/Android/Autofill/AutofillConstants.cs @@ -0,0 +1,10 @@ +namespace Bit.Droid.Autofill +{ + public class AutofillConstants + { + public const string AutofillFramework = "autofillFramework"; + public const string AutofillFrameworkFillType = "autofillFrameworkFillType"; + public const string AutofillFrameworkUri = "autofillFrameworkUri"; + public const string AutofillFrameworkCipherId = "autofillFrameworkCipherId"; + } +} diff --git a/src/Android/Autofill/AutofillExternalSelectionActivity.cs b/src/Android/Autofill/AutofillExternalSelectionActivity.cs new file mode 100644 index 000000000..c75d150be --- /dev/null +++ b/src/Android/Autofill/AutofillExternalSelectionActivity.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Android.App; +using Android.Content.PM; +using Android.OS; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Bit.Droid.Utilities; + +namespace Bit.Droid.Autofill +{ + [Activity( + NoHistory = true, + LaunchMode = LaunchMode.SingleTop)] + public class AutofillExternalSelectionActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity + { + protected override void OnCreate(Bundle bundle) + { + Intent?.Validate(); + base.OnCreate(bundle); + + var cipherId = Intent?.GetStringExtra(AutofillConstants.AutofillFrameworkCipherId); + if (string.IsNullOrEmpty(cipherId)) + { + SetResult(Result.Canceled); + Finish(); + return; + } + + GetCipherAndPerformAutofillAsync(cipherId).FireAndForget(); + } + + private async Task GetCipherAndPerformAutofillAsync(string cipherId) + { + var cipherService = ServiceContainer.Resolve(); + var cipher = await cipherService.GetAsync(cipherId); + var decCipher = await cipher.DecryptAsync(); + + var autofillHandler = ServiceContainer.Resolve(); + autofillHandler.Autofill(decCipher); + } + } +} diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index 8ec3416bd..924836941 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -19,6 +19,7 @@ using AndroidX.AutoFill.Inline; using AndroidX.AutoFill.Inline.V1; using Bit.Core.Abstractions; using SaveFlags = Android.Service.Autofill.SaveFlags; +using Bit.Droid.Utilities; namespace Bit.Droid.Autofill { @@ -53,6 +54,7 @@ namespace Bit.Droid.Autofill { "alook.browser", "alook.browser.google", + "app.vanadium.browser", "com.amazon.cloud9", "com.android.browser", "com.android.chrome", @@ -85,6 +87,7 @@ namespace Bit.Droid.Autofill "com.mmbox.xbrowser", "com.mycompany.app.soulbrowser", "com.naver.whale", + "com.neeva.app", "com.opera.browser", "com.opera.browser.beta", "com.opera.gx", @@ -108,6 +111,7 @@ namespace Bit.Droid.Autofill "io.github.forkmaintainers.iceraven", "mark.via", "mark.via.gp", + "net.dezor.browser", "net.slions.fulguris.full.download", "net.slions.fulguris.full.download.debug", "net.slions.fulguris.full.playstore", @@ -206,7 +210,7 @@ namespace Bit.Droid.Autofill } } var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i], - inlinePresentationSpec); + true, inlinePresentationSpec); if (dataset != null) { responseBuilder.AddDataset(dataset); @@ -220,7 +224,7 @@ namespace Bit.Droid.Autofill } public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem, - InlinePresentationSpec inlinePresentationSpec = null) + bool includeAuthIntent, InlinePresentationSpec inlinePresentationSpec = null) { var overlayPresentation = BuildOverlayPresentation( filledItem.Name, @@ -241,6 +245,15 @@ namespace Bit.Droid.Autofill { datasetBuilder.SetInlinePresentation(inlinePresentation); } + if (includeAuthIntent) + { + var intent = new Intent(context, typeof(AutofillExternalSelectionActivity)); + intent.PutExtra(AutofillConstants.AutofillFramework, true); + intent.PutExtra(AutofillConstants.AutofillFrameworkCipherId, filledItem.Id); + var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, + AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true)); + datasetBuilder.SetAuthentication(pendingIntent?.IntentSender); + } if (filledItem.ApplyToFields(fields, datasetBuilder)) { return datasetBuilder.Build(); @@ -252,26 +265,26 @@ namespace Bit.Droid.Autofill IList inlinePresentationSpecs = null) { var intent = new Intent(context, typeof(MainActivity)); - intent.PutExtra("autofillFramework", true); + intent.PutExtra(AutofillConstants.AutofillFramework, true); if (fields.FillableForLogin) { - intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login); + intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Login); } else if (fields.FillableForCard) { - intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card); + intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Card); } else if (fields.FillableForIdentity) { - intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity); + intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Identity); } else { return null; } - intent.PutExtra("autofillFrameworkUri", uri); + intent.PutExtra(AutofillConstants.AutofillFrameworkUri, uri); var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, - PendingIntentFlags.CancelCurrent); + AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true)); var overlayPresentation = BuildOverlayPresentation( AppResources.AutofillWithBitwarden, @@ -324,7 +337,7 @@ namespace Bit.Droid.Autofill // InlinePresentation requires nonNull pending intent (even though we only utilize one for the // "my vault" presentation) so we're including an empty one here pendingIntent = PendingIntent.GetService(context, 0, new Intent(), - PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent); + AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true)); } var slice = CreateInlinePresentationSlice( inlinePresentationSpec, diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index 07171eb9e..3becfe4ac 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -15,7 +15,7 @@ using Bit.Core.Utilities; namespace Bit.Droid.Autofill { - [Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden")] + [Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden", Exported = true)] [IntentFilter(new string[] { "android.service.autofill.AutofillService" })] [MetaData("android.autofill", Resource = "@xml/autofillservice")] [Register("com.x8bit.bitwarden.Autofill.AutofillService")] @@ -134,7 +134,7 @@ namespace Bit.Droid.Autofill { case CipherType.Login: intent.PutExtra("autofillFrameworkName", parser.Uri - .Replace(Constants.AndroidAppProtocol, string.Empty) + .Replace(Core.Constants.AndroidAppProtocol, string.Empty) .Replace("https://", string.Empty) .Replace("http://", string.Empty)); intent.PutExtra("autofillFrameworkUri", parser.Uri); diff --git a/src/Android/Autofill/FilledItem.cs b/src/Android/Autofill/FilledItem.cs index 3964f37bd..46b0436fc 100644 --- a/src/Android/Autofill/FilledItem.cs +++ b/src/Android/Autofill/FilledItem.cs @@ -23,6 +23,7 @@ namespace Bit.Droid.Autofill public FilledItem(CipherView cipher) { + Id = cipher.Id; Name = cipher.Name; Type = cipher.Type; Subtitle = cipher.SubTitle; @@ -55,6 +56,7 @@ namespace Bit.Droid.Autofill } } + public string Id { get; set; } public string Name { get; set; } public string Subtitle { get; set; } = string.Empty; public int Icon { get; set; } = Resource.Drawable.login; diff --git a/src/Android/Autofill/Parser.cs b/src/Android/Autofill/Parser.cs index 45a2fdb0b..4bc2ebca5 100644 --- a/src/Android/Autofill/Parser.cs +++ b/src/Android/Autofill/Parser.cs @@ -48,7 +48,7 @@ namespace Bit.Droid.Autofill } else { - _uri = string.Concat(Constants.AndroidAppProtocol, PackageName); + _uri = string.Concat(Core.Constants.AndroidAppProtocol, PackageName); } return _uri; } diff --git a/src/Android/Constants.cs b/src/Android/Constants.cs new file mode 100644 index 000000000..398bfb708 --- /dev/null +++ b/src/Android/Constants.cs @@ -0,0 +1,7 @@ +namespace Bit.Droid +{ + public static class Constants + { + public const string PACKAGE_NAME = "com.x8bit.bitwarden"; + } +} diff --git a/src/Android/Effects/RemoveFontPaddingEffect.cs b/src/Android/Effects/RemoveFontPaddingEffect.cs new file mode 100644 index 000000000..1f7cf1297 --- /dev/null +++ b/src/Android/Effects/RemoveFontPaddingEffect.cs @@ -0,0 +1,23 @@ +using Android.Widget; +using Bit.Droid.Effects; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportEffect(typeof(RemoveFontPaddingEffect), nameof(RemoveFontPaddingEffect))] +namespace Bit.Droid.Effects +{ + public class RemoveFontPaddingEffect : PlatformEffect + { + protected override void OnAttached() + { + if (Control is TextView textView) + { + textView.SetIncludeFontPadding(false); + } + } + + protected override void OnDetached() + { + } + } +} \ No newline at end of file diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 39fdfa6f1..2e25bc1cc 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -5,20 +5,27 @@ using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; +using Android.Content.Res; using Android.Nfc; using Android.OS; using Android.Runtime; -using AndroidX.Core.Content; +using Android.Views; using Bit.App.Abstractions; using Bit.App.Models; +using Bit.App.Resources; using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Utilities; +using Bit.Droid.Autofill; using Bit.Droid.Receivers; using Bit.Droid.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xamarin.Essentials; using ZXing.Net.Mobile.Android; +using FileProvider = AndroidX.Core.Content.FileProvider; namespace Bit.Droid { @@ -30,11 +37,14 @@ namespace Bit.Droid public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity { private IDeviceActionService _deviceActionService; + private IFileService _fileService; private IMessagingService _messagingService; private IBroadcasterService _broadcasterService; private IStateService _stateService; private IAppIdService _appIdService; private IEventService _eventService; + private IPushNotificationListenerService _pushNotificationListenerService; + private ILogger _logger; private PendingIntent _eventUploadPendingIntent; private AppOptions _appOptions; private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}"; @@ -45,17 +55,20 @@ namespace Bit.Droid { var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver)); _eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent, - PendingIntentFlags.UpdateCurrent); + AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)); var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build(); StrictMode.SetThreadPolicy(policy); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _fileService = ServiceContainer.Resolve(); _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _stateService = ServiceContainer.Resolve("stateService"); _appIdService = ServiceContainer.Resolve("appIdService"); _eventService = ServiceContainer.Resolve("eventService"); + _pushNotificationListenerService = ServiceContainer.Resolve(); + _logger = ServiceContainer.Resolve("logger"); TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; @@ -70,7 +83,7 @@ namespace Bit.Droid Window.AddFlags(Android.Views.WindowManagerFlags.Secure); }); - ServiceContainer.Resolve("logger").InitAsync(); + _logger.InitAsync(); var toplayout = Window?.DecorView?.RootView; if (toplayout != null) @@ -81,8 +94,9 @@ namespace Bit.Droid Xamarin.Essentials.Platform.Init(this, savedInstanceState); Xamarin.Forms.Forms.Init(this, savedInstanceState); _appOptions = GetOptions(); + CreateNotificationChannel(); LoadApplication(new App.App(_appOptions)); - + DisableAndroidFontScale(); _broadcasterService.Subscribe(_activityKey, (message) => { @@ -138,6 +152,15 @@ namespace Bit.Droid AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this) .GetAwaiter() .GetResult(); + + if (Intent?.GetStringExtra(Core.Constants.NotificationData) is string notificationDataJson) + { + var notificationType = JToken.Parse(notificationDataJson).SelectToken(Core.Constants.NotificationDataType); + if (notificationType.ToString() == PasswordlessNotificationData.TYPE) + { + _pushNotificationListenerService.OnNotificationTapped(JsonConvert.DeserializeObject(notificationDataJson)).FireAndForget(); + } + } } protected override void OnNewIntent(Intent intent) @@ -191,13 +214,13 @@ namespace Bit.Droid public async override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults) { - if (requestCode == Constants.SelectFilePermissionRequestCode) + if (requestCode == Core.Constants.SelectFilePermissionRequestCode) { if (grantResults.Any(r => r != Permission.Granted)) { _messagingService.Send("selectFileCameraPermissionDenied"); } - await _deviceActionService.SelectFileAsync(); + await _fileService.SelectFileAsync(); } else { @@ -210,7 +233,7 @@ namespace Bit.Droid protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { if (resultCode == Result.Ok && - (requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode)) + (requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode)) { Android.Net.Uri uri = null; string fileName = null; @@ -232,7 +255,7 @@ namespace Bit.Droid return; } - if (requestCode == Constants.SaveFileRequestCode) + if (requestCode == Core.Constants.SaveFileRequestCode) { _messagingService.Send("selectSaveFileResult", new Tuple(uri.ToString(), fileName)); @@ -273,7 +296,7 @@ namespace Bit.Droid { var intent = new Intent(this, Class); intent.AddFlags(ActivityFlags.SingleTop); - var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0); + var pendingIntent = PendingIntent.GetActivity(this, 0, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(0, true)); // register for all NDEF tags starting with http och https var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered); ndef.AddDataScheme("http"); @@ -300,13 +323,13 @@ namespace Bit.Droid { var options = new AppOptions { - Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"), + Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri), MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false), GeneratorTile = Intent.GetBooleanExtra("generatorTile", false), - FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false), + FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false), CreateSend = GetCreateSendRequest(Intent) }; - var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0); + var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0); if (fillType > 0) { options.FillType = (CipherType)fillType; @@ -401,5 +424,38 @@ namespace Bit.Droid alarmManager.Cancel(_eventUploadPendingIntent); await _eventService.UploadEventsAsync(); } + + private void CreateNotificationChannel() + { +#if !FDROID + if (Build.VERSION.SdkInt < BuildVersionCodes.O) + { + // Notification channels are new in API 26 (and not a part of the + // support library). There is no need to create a notification + // channel on older versions of Android. + return; + } + + var channel = new NotificationChannel(Core.Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default); + if(GetSystemService(NotificationService) is NotificationManager notificationManager) + { + notificationManager.CreateNotificationChannel(channel); + } +#endif + } + + private void DisableAndroidFontScale() + { + try + { + //As we are using NamedSizes the xamarin will change the font size. So we are disabling the Android scaling. + Resources.Configuration.FontScale = 1f; + BaseContext.Resources.DisplayMetrics.ScaledDensity = Resources.Configuration.FontScale * (float)DeviceDisplay.MainDisplayInfo.Density; + } + catch (Exception e) + { + _logger.Exception(e); + } + } } } diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index bea0569ff..db042bcf7 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -45,9 +45,17 @@ namespace Bit.Droid if (ServiceContainer.RegisteredServices.Count == 0) { RegisterLocalServices(); + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); - ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey, - Constants.AndroidAllClearCipherCacheKeys); + ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey, + Core.Constants.AndroidAllClearCipherCacheKeys); + + ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve())); + + InitializeAppSetup(); // TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner( @@ -71,7 +79,9 @@ namespace Bit.Droid ServiceContainer.Resolve("stateService"), ServiceContainer.Resolve("platformUtilsService"), ServiceContainer.Resolve("authService"), - ServiceContainer.Resolve("logger")); + ServiceContainer.Resolve("logger"), + ServiceContainer.Resolve("messagingService"), + ServiceContainer.Resolve()); ServiceContainer.Register("accountsManager", accountsManager); } #if !FDROID @@ -137,10 +147,12 @@ namespace Bit.Droid var stateMigrationService = new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); var clipboardService = new ClipboardService(stateService); - var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService, - broadcasterService, () => ServiceContainer.Resolve("eventService")); + var deviceActionService = new DeviceActionService(stateService, messagingService); + var fileService = new FileService(stateService, broadcasterService); var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService, messagingService, broadcasterService); + var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, + platformUtilsService, new LazyResolve()); var biometricService = new BiometricService(); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); var cryptoService = new CryptoService(stateService, cryptoFunctionService); @@ -157,6 +169,8 @@ namespace Bit.Droid ServiceContainer.Register("stateMigrationService", stateMigrationService); ServiceContainer.Register("clipboardService", clipboardService); ServiceContainer.Register("deviceActionService", deviceActionService); + ServiceContainer.Register(fileService); + ServiceContainer.Register(autofillHandler); ServiceContainer.Register("platformUtilsService", platformUtilsService); ServiceContainer.Register("biometricService", biometricService); ServiceContainer.Register("cryptoFunctionService", cryptoFunctionService); @@ -193,5 +207,12 @@ namespace Bit.Droid { await ServiceContainer.Resolve("environmentService").SetUrlsFromStorageAsync(); } + + private void InitializeAppSetup() + { + var appSetup = new AppSetup(); + appSetup.InitializeServicesLastChance(); + ServiceContainer.Register("appSetup", appSetup); + } } } diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml index 6dd07923a..da8e1ddb3 100644 --- a/src/Android/Properties/AndroidManifest.xml +++ b/src/Android/Properties/AndroidManifest.xml @@ -1,6 +1,6 @@  - - + + @@ -40,10 +40,4 @@ - - - - - - \ No newline at end of file diff --git a/src/Android/Push/FirebaseMessagingService.cs b/src/Android/Push/FirebaseMessagingService.cs index 676390aef..887c8ac44 100644 --- a/src/Android/Push/FirebaseMessagingService.cs +++ b/src/Android/Push/FirebaseMessagingService.cs @@ -1,7 +1,9 @@ #if !FDROID +using System; using Android.App; using Bit.App.Abstractions; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; using Firebase.Messaging; using Newtonsoft.Json; @@ -16,34 +18,41 @@ namespace Bit.Droid.Push { public async override void OnNewToken(string token) { - var stateService = ServiceContainer.Resolve("stateService"); - var pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); + try { + var stateService = ServiceContainer.Resolve("stateService"); + var pushNotificationService = ServiceContainer.Resolve("pushNotificationService"); - await stateService.SetPushRegisteredTokenAsync(token); - await pushNotificationService.RegisterAsync(); + await stateService.SetPushRegisteredTokenAsync(token); + await pushNotificationService.RegisterAsync(); + } + catch (Exception ex) + { + Logger.Instance.Exception(ex); + } } public async override void OnMessageReceived(RemoteMessage message) { - if (message?.Data == null) - { - return; - } - var data = message.Data.ContainsKey("data") ? message.Data["data"] : null; - if (data == null) - { - return; - } try { + if (message?.Data == null) + { + return; + } + var data = message.Data.ContainsKey("data") ? message.Data["data"] : null; + if (data == null) + { + return; + } + var obj = JObject.Parse(data); var listener = ServiceContainer.Resolve( "pushNotificationListenerService"); await listener.OnMessageAsync(obj, Device.Android); } - catch (JsonReaderException ex) + catch (Exception ex) { - System.Diagnostics.Debug.WriteLine(ex.ToString()); + Logger.Instance.Exception(ex); } } } diff --git a/src/Android/Receivers/ClearClipboardAlarmReceiver.cs b/src/Android/Receivers/ClearClipboardAlarmReceiver.cs index ff1566134..6e57cedb7 100644 --- a/src/Android/Receivers/ClearClipboardAlarmReceiver.cs +++ b/src/Android/Receivers/ClearClipboardAlarmReceiver.cs @@ -1,4 +1,5 @@ using Android.Content; +using Android.OS; namespace Bit.Droid.Receivers { @@ -8,7 +9,17 @@ namespace Bit.Droid.Receivers public override void OnReceive(Context context, Intent intent) { var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager; - clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " "); + if (clipboardManager == null) + { + return; + } + // ClearPrimaryClip is supported down to API 28 with mixed results, so we're requiring 33+ instead + if ((int)Build.VERSION.SdkInt < 33) + { + clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " "); + return; + } + clipboardManager.ClearPrimaryClip(); } } } diff --git a/src/Android/Receivers/NotificationDismissReceiver.cs b/src/Android/Receivers/NotificationDismissReceiver.cs new file mode 100644 index 000000000..43f69ea62 --- /dev/null +++ b/src/Android/Receivers/NotificationDismissReceiver.cs @@ -0,0 +1,41 @@ +using Android.Content; +using Bit.App.Abstractions; +using Bit.App.Models; +using Bit.App.Services; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using CoreConstants = Bit.Core.Constants; + +namespace Bit.Droid.Receivers +{ + [BroadcastReceiver(Name = Constants.PACKAGE_NAME + "." + nameof(NotificationDismissReceiver), Exported = false)] + public class NotificationDismissReceiver : BroadcastReceiver + { + private readonly LazyResolve _pushNotificationListenerService = new LazyResolve(); + private readonly LazyResolve _logger = new LazyResolve(); + + public override void OnReceive(Context context, Intent intent) + { + try + { + if (intent?.GetStringExtra(CoreConstants.NotificationData) is string notificationDataJson) + { + var notificationType = JToken.Parse(notificationDataJson).SelectToken(CoreConstants.NotificationDataType); + if (notificationType.ToString() == PasswordlessNotificationData.TYPE) + { + _pushNotificationListenerService.Value.OnNotificationDismissed(JsonConvert.DeserializeObject(notificationDataJson)).FireAndForget(); + } + } + } + catch (System.Exception ex) + { + _logger.Value.Exception(ex); + } + } + } +} + diff --git a/src/Android/Renderers/CustomSwitchRenderer.cs b/src/Android/Renderers/CustomSwitchRenderer.cs index 00caa6290..f937b6e77 100644 --- a/src/Android/Renderers/CustomSwitchRenderer.cs +++ b/src/Android/Renderers/CustomSwitchRenderer.cs @@ -44,6 +44,7 @@ namespace Bit.Droid.Renderers } if (Control != null) { + Control.SetHintTextColor(ThemeHelpers.MutedColor); var t = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.switch_thumb, null); if (t is GradientDrawable thumb) { diff --git a/src/Android/Resources/drawable-night-v26/splash_screen_round.xml b/src/Android/Resources/drawable-night-v26/splash_screen_round.xml new file mode 100644 index 000000000..dc4c7209e --- /dev/null +++ b/src/Android/Resources/drawable-night-v26/splash_screen_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Android/Resources/drawable-v26/splash_screen_round.xml b/src/Android/Resources/drawable-v26/splash_screen_round.xml new file mode 100644 index 000000000..602f055dd --- /dev/null +++ b/src/Android/Resources/drawable-v26/splash_screen_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Android/Resources/drawable/ic_launcher_monochrome.xml b/src/Android/Resources/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000..5a54380d9 --- /dev/null +++ b/src/Android/Resources/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/src/Android/Resources/drawable/ic_notification.xml b/src/Android/Resources/drawable/ic_notification.xml new file mode 100644 index 000000000..ed92d0ce8 --- /dev/null +++ b/src/Android/Resources/drawable/ic_notification.xml @@ -0,0 +1,4 @@ + + + diff --git a/src/Android/Resources/drawable/logo_rounded.xml b/src/Android/Resources/drawable/logo_rounded.xml new file mode 100644 index 000000000..860d4c963 --- /dev/null +++ b/src/Android/Resources/drawable/logo_rounded.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/src/Android/Resources/mipmap-anydpi-v26/ic_launcher.xml b/src/Android/Resources/mipmap-anydpi-v26/ic_launcher.xml index ef49c9917..1084c2408 100644 --- a/src/Android/Resources/mipmap-anydpi-v26/ic_launcher.xml +++ b/src/Android/Resources/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/src/Android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml b/src/Android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml index ef49c9917..1084c2408 100644 --- a/src/Android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/src/Android/Resources/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/src/Android/Resources/values-night/styles.xml b/src/Android/Resources/values-night/styles.xml index 7e5a4074e..edfaaa455 100644 --- a/src/Android/Resources/values-night/styles.xml +++ b/src/Android/Resources/values-night/styles.xml @@ -4,6 +4,7 @@ - + + + + diff --git a/src/App/Styles/Black.xaml b/src/App/Styles/Black.xaml index 7c64d5ea3..b2027f114 100644 --- a/src/App/Styles/Black.xaml +++ b/src/App/Styles/Black.xaml @@ -71,6 +71,6 @@ #ffffff #52bdfb - + #F08DC7 #80BDFF diff --git a/src/App/Styles/Dark.xaml b/src/App/Styles/Dark.xaml index 778a2b5a1..570e9ca2e 100644 --- a/src/App/Styles/Dark.xaml +++ b/src/App/Styles/Dark.xaml @@ -71,6 +71,6 @@ #ffffff #52bdfb - + #F08DC7 #80BDFF diff --git a/src/App/Styles/Light.xaml b/src/App/Styles/Light.xaml index fef760bf0..59a424953 100644 --- a/src/App/Styles/Light.xaml +++ b/src/App/Styles/Light.xaml @@ -71,6 +71,6 @@ #ffffff #175DDC - + #C01176 #80BDFF diff --git a/src/App/Styles/Nord.xaml b/src/App/Styles/Nord.xaml index 4b83815e1..d4690c2de 100644 --- a/src/App/Styles/Nord.xaml +++ b/src/App/Styles/Nord.xaml @@ -71,6 +71,6 @@ #e5e9f0 #81a1c1 - + #F08DC7 #80BDFF diff --git a/src/App/Styles/SolarizedDark.xaml b/src/App/Styles/SolarizedDark.xaml new file mode 100644 index 000000000..e8402a0d2 --- /dev/null +++ b/src/App/Styles/SolarizedDark.xaml @@ -0,0 +1,76 @@ + + + #eee8d5 + #859900 + #dc322f + #859900 + #859900 + #b58900 + #839496 + #2aa198 + #b58900 + #93a1a1 + #657b83 + #cb4b16 + + #002b36 + #073642 + #073642 + #839496 + #073642 + #859900 + + #073642 + #859900 + + #eee8d5 + #eee8d5 + #657b83 + + #073642 + #073642 + #073642 + #859900 + #073642 + + #eee8d5 + #073642 + #859900 + #073642 + + #859900 + #eee8d5 + + #657b83 + #eee8d5 + + #859900 + #667500 + #4d541c + #e5e9f0 + #ACB5C5 + + #657b83 + #3A4251 + #454951 + #073642 + #eee8d5 + #aaaaaa + #99eee8d5 + + #859900 + #859900 + + #073642 + #eee8d5 + #859900 + + #073642 + #eee8d5 + + #859900 + #F08DC7 + #80BDFF + diff --git a/src/App/Styles/SolarizedDark.xaml.cs b/src/App/Styles/SolarizedDark.xaml.cs new file mode 100644 index 000000000..a8dc19768 --- /dev/null +++ b/src/App/Styles/SolarizedDark.xaml.cs @@ -0,0 +1,12 @@ +using Xamarin.Forms; + +namespace Bit.App.Styles +{ + public partial class SolarizedDark : ResourceDictionary, IThemeResourceDictionary + { + public SolarizedDark() + { + InitializeComponent(); + } + } +} diff --git a/src/App/Styles/iOS.xaml b/src/App/Styles/iOS.xaml index f6bd66a4c..b6041bcce 100644 --- a/src/App/Styles/iOS.xaml +++ b/src/App/Styles/iOS.xaml @@ -93,6 +93,7 @@ Value="{DynamicResource StepperForegroundColor}" />