mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
168 Commits
v2022.9.1
...
community/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95f5a1ce09 | ||
|
|
c4790eb78e | ||
|
|
8577765668 | ||
|
|
f63918aa4e | ||
|
|
f42c677d5a | ||
|
|
5a56d64211 | ||
|
|
0bd1b3f45f | ||
|
|
3780587991 | ||
|
|
6875389948 | ||
|
|
0e5d6e79c5 | ||
|
|
490b74dd26 | ||
|
|
66d05e1b00 | ||
|
|
3d3101c3ab | ||
|
|
ccde4270d0 | ||
|
|
a1f799302e | ||
|
|
4619e257e8 | ||
|
|
c54a14cd3f | ||
|
|
a96d95c95b | ||
|
|
e61ca489ce | ||
|
|
c3ad5f0580 | ||
|
|
8b08f906bd | ||
|
|
4ad5f5ae37 | ||
|
|
68a6449339 | ||
|
|
b8d53b0f81 | ||
|
|
dbfd15b819 | ||
|
|
acd0cb119d | ||
|
|
d61bc4b5c1 | ||
|
|
5aa1146657 | ||
|
|
f15fd246a8 | ||
|
|
2b8547878a | ||
|
|
942d5d29d2 | ||
|
|
fde63a836d | ||
|
|
6102a0c115 | ||
|
|
4f4953206e | ||
|
|
f772ee7068 | ||
|
|
8f93e6bf5f | ||
|
|
64fefac194 | ||
|
|
d784b1290b | ||
|
|
0f2bc2fa25 | ||
|
|
0e856d2add | ||
|
|
acc587ce45 | ||
|
|
66180397d9 | ||
|
|
571c4b8d22 | ||
|
|
660ba3d722 | ||
|
|
414cb9bd7e | ||
|
|
e588efd0a1 | ||
|
|
1cdba5f73d | ||
|
|
cbccd10271 | ||
|
|
6de6b19944 | ||
|
|
5493b00957 | ||
|
|
81988a7fe7 | ||
|
|
b91ff09e27 | ||
|
|
945627d649 | ||
|
|
307c71ee07 | ||
|
|
e72932cbaa | ||
|
|
4347c2f81d | ||
|
|
619acfe0fe | ||
|
|
05765c2af9 | ||
|
|
728182de6c | ||
|
|
a19b5c4e05 | ||
|
|
f6895a0733 | ||
|
|
3e48b7a968 | ||
|
|
ebf65ecb96 | ||
|
|
37dab0928b | ||
|
|
aa0544cd3d | ||
|
|
f4b4cfc9de | ||
|
|
7785b2dbf9 | ||
|
|
e3dafb502b | ||
|
|
b30fc12135 | ||
|
|
28d204f2b1 | ||
|
|
1a3ff5ec41 | ||
|
|
2f6fd476a2 | ||
|
|
7a08452fa8 | ||
|
|
fa6bac3b43 | ||
|
|
2a60ff62d8 | ||
|
|
eaa4f193ce | ||
|
|
6973a0b71c | ||
|
|
bafd9ff85d | ||
|
|
4580033477 | ||
|
|
a4673021a9 | ||
|
|
8e2e143e6f | ||
|
|
f43f9171e5 | ||
|
|
3c8a8862ca | ||
|
|
969b89bb4d | ||
|
|
164b8970d3 | ||
|
|
34fd30e157 | ||
|
|
8e09f0cc15 | ||
|
|
693a4ef776 | ||
|
|
0992a989d4 | ||
|
|
1b137a8a8a | ||
|
|
7e8e86a77a | ||
|
|
20c1e2d7f2 | ||
|
|
7870c2706b | ||
|
|
5a5f50605b | ||
|
|
0106732cbd | ||
|
|
9ae269dd57 | ||
|
|
04ed47d545 | ||
|
|
6160535c03 | ||
|
|
8d92373c88 | ||
|
|
6bfc8f7d49 | ||
|
|
69f02eef82 | ||
|
|
02b8e54988 | ||
|
|
99472d2548 | ||
|
|
5a4c81bd75 | ||
|
|
ee09c0abda | ||
|
|
9baa79e10b | ||
|
|
a8909a3ce6 | ||
|
|
b9b9c2e5ff | ||
|
|
89adab6784 | ||
|
|
77d8afcfe6 | ||
|
|
4fcb063190 | ||
|
|
5deba15373 | ||
|
|
505426cd6a | ||
|
|
3cb9f37997 | ||
|
|
4c2f5c05e5 | ||
|
|
eefc9bd239 | ||
|
|
d18efdea73 | ||
|
|
a72a0ec0ce | ||
|
|
c7e9f30a9a | ||
|
|
6c404c8229 | ||
|
|
e5de530c2c | ||
|
|
3a87378847 | ||
|
|
6048a10d6d | ||
|
|
a5ad43b134 | ||
|
|
539f8fe5b5 | ||
|
|
569922805f | ||
|
|
3972e3de8a | ||
|
|
ba677a96aa | ||
|
|
d800e9a43e | ||
|
|
2d35a00caa | ||
|
|
dc5698b353 | ||
|
|
abada481b7 | ||
|
|
1e5eab0574 | ||
|
|
c1101af582 | ||
|
|
1db4c4fc8b | ||
|
|
bc949fe87a | ||
|
|
a890ee6612 | ||
|
|
90e0b5dcf0 | ||
|
|
9631988fc2 | ||
|
|
b5f80da28d | ||
|
|
7e0b943b70 | ||
|
|
425be32c15 | ||
|
|
f9a32e4abc | ||
|
|
2f4cd36595 | ||
|
|
70ee24d82a | ||
|
|
28576bbf49 | ||
|
|
7f9dfd3dae | ||
|
|
115aee2026 | ||
|
|
86fee6f04e | ||
|
|
2deab59b35 | ||
|
|
b4737457a8 | ||
|
|
305c770c58 | ||
|
|
afa9e23707 | ||
|
|
87fb5cf2ae | ||
|
|
3f8e00985c | ||
|
|
533928a4f1 | ||
|
|
b7048de2a1 | ||
|
|
2016eadb0d | ||
|
|
68b5bc0964 | ||
|
|
119fc5812b | ||
|
|
b628c1990e | ||
|
|
183bfa0ab2 | ||
|
|
b1fb867b6e | ||
|
|
673ba9f3cc | ||
|
|
cdd9a5ff4d | ||
|
|
d204e812e1 | ||
|
|
9163b9e4de | ||
|
|
ecd4da08ee |
@@ -7,6 +7,12 @@
|
|||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-format"
|
"dotnet-format"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"cake.tool": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-cake"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## Before you submit
|
## Before you submit
|
||||||
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
|
- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
|
||||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
|
||||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
- If this change requires a **documentation update** - notify the documentation team
|
||||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
- If this change has particular **deployment requirements** - notify the DevOps team
|
||||||
|
|||||||
0
renovate.json → .github/renovate.json
vendored
0
renovate.json → .github/renovate.json
vendored
@@ -14,6 +14,10 @@
|
|||||||
<string>Dist: Extension 2021</string>
|
<string>Dist: Extension 2021</string>
|
||||||
<key>com.8bit.bitwarden.share-extension</key>
|
<key>com.8bit.bitwarden.share-extension</key>
|
||||||
<string>Dist: Share Extension 2021</string>
|
<string>Dist: Share Extension 2021</string>
|
||||||
|
<key>com.8bit.bitwarden.watchkitapp</key>
|
||||||
|
<string>Dist: Bitwarden Watch App</string>
|
||||||
|
<key>com.8bit.bitwarden.watchkitapp.watchkitextension</key>
|
||||||
|
<string>Dist: Bitwarden Watch App Extension</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
Normal file
BIN
.github/secrets/GoogleService-Info.plist.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_watch_app.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
BIN
.github/secrets/dist_watch_app_extension.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_watch_app_extension.mobileprovision.gpg
vendored
Normal file
Binary file not shown.
237
.github/workflows/build.yml
vendored
237
.github/workflows/build.yml
vendored
@@ -4,10 +4,10 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'l10n_master'
|
- "l10n_master"
|
||||||
- 'gh-pages'
|
- "gh-pages"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '.github/workflows/**'
|
- ".github/workflows/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs: {}
|
||||||
|
|
||||||
@@ -42,15 +42,15 @@ jobs:
|
|||||||
id: branch-check
|
id: branch-check
|
||||||
run: |
|
run: |
|
||||||
if [[ $(git ls-remote --heads origin rc) ]]; then
|
if [[ $(git ls-remote --heads origin rc) ]]; then
|
||||||
echo "::set-output name=rc_branch_exists::1"
|
echo "rc_branch_exists=1" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "::set-output name=rc_branch_exists::0"
|
echo "rc_branch_exists=0" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $(git ls-remote --heads origin hotfix-rc) ]]; then
|
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
|
else
|
||||||
echo "::set-output name=hotfix_branch_exists::0"
|
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
@@ -59,6 +59,10 @@ jobs:
|
|||||||
name: Android
|
name: Android
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
needs: setup
|
needs: setup
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
variant: ["prod", "qa"]
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
@@ -68,6 +72,9 @@ jobs:
|
|||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Setup Windows builder
|
||||||
|
run: choco install checksum --no-progress
|
||||||
|
|
||||||
- name: Work Around for broken Windows 2022 Runner Image
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
run: |
|
run: |
|
||||||
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
@@ -87,7 +94,6 @@ jobs:
|
|||||||
Write-Host "components were not installed"
|
Write-Host "components were not installed"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -98,7 +104,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||||
@@ -109,12 +116,17 @@ jobs:
|
|||||||
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
--output ./src/Android/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg
|
||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output ./src/Android/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg
|
--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" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg
|
||||||
shell: bash
|
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
|
- name: Increment version
|
||||||
run: |
|
run: |
|
||||||
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
|
||||||
@@ -139,29 +151,48 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Run Core tests
|
- name: Run Core tests
|
||||||
run: dotnet test test/Core.Test/Core.Test.csproj
|
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx"
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Report test results
|
||||||
|
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Test Results
|
||||||
|
path: "**/test-results.trx"
|
||||||
|
reporter: dotnet-trx
|
||||||
|
fail-on-error: true
|
||||||
|
|
||||||
- name: Build Play Store publisher
|
- name: Build Play Store publisher
|
||||||
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
|
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: |
|
run: |
|
||||||
$configuration = "Release";
|
$configuration = "Release";
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
Write-Output "##### Build $configuration Configuration"
|
Write-Output "##### Build $configuration Configuration"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
msbuild "$($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj")" "/p:Configuration=$configuration"
|
||||||
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Sign for Play Store
|
- name: Sign Android Build
|
||||||
env:
|
env:
|
||||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
$androidPath = $($env:GITHUB_WORKSPACE + "/src/Android/Android.csproj");
|
$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 "########################################"
|
||||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -175,9 +206,8 @@ jobs:
|
|||||||
Write-Output "##### Copy Google Play Bundle to project root"
|
Write-Output "##### Copy Google Play Bundle to project root"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab");
|
$signedAabPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.aab");
|
||||||
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.aab");
|
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab");
|
||||||
|
|
||||||
Copy-Item $signedAabPath $signedAabDestPath
|
Copy-Item $signedAabPath $signedAabDestPath
|
||||||
|
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
@@ -193,33 +223,69 @@ jobs:
|
|||||||
Write-Output "##### Copy Release APK to project root"
|
Write-Output "##### Copy Release APK to project root"
|
||||||
Write-Output "########################################"
|
Write-Output "########################################"
|
||||||
|
|
||||||
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk");
|
$signedApkPath = $($env:GITHUB_WORKSPACE + "/src/Android/bin/Release/$($packageName)-Signed.apk");
|
||||||
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden.apk");
|
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
|
||||||
|
|
||||||
Copy-Item $signedApkPath $signedApkDestPath
|
Copy-Item $signedApkPath $signedApkDestPath
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
- name: Upload Prod .aab artifact
|
||||||
- name: Upload Play Store .aab artifact
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.aab
|
name: com.x8bit.bitwarden.aab
|
||||||
path: ./com.x8bit.bitwarden.aab
|
path: ./com.x8bit.bitwarden.aab
|
||||||
if-no-files-found: error
|
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
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
with:
|
with:
|
||||||
name: com.x8bit.bitwarden.apk
|
name: com.x8bit.bitwarden.apk
|
||||||
path: ./com.x8bit.bitwarden.apk
|
path: ./com.x8bit.bitwarden.apk
|
||||||
if-no-files-found: error
|
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: Create checksum for Prod .apk artifact
|
||||||
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
|
run: |
|
||||||
|
checksum -f="./com.x8bit.bitwarden.apk" `
|
||||||
|
-t sha256 | Out-File -Encoding ASCII ./bw-android-apk-sha256.txt
|
||||||
|
|
||||||
|
- name: Create checksum for Other .apk artifact
|
||||||
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
|
run: |
|
||||||
|
checksum -f="./com.x8bit.bitwarden.${{ matrix.variant }}.apk" `
|
||||||
|
-t sha256 | Out-File -Encoding ASCII ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
|
|
||||||
|
- name: Upload .apk sha file for prod
|
||||||
|
if: ${{ matrix.variant == 'prod' }}
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: bw-android-apk-sha256.txt
|
||||||
|
path: ./bw-android-apk-sha256.txt
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload .apk sha file for other
|
||||||
|
if: ${{ matrix.variant != 'prod' }}
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
|
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Deploy to Play Store
|
- name: Deploy to Play Store
|
||||||
if: |
|
if: ${{ matrix.variant == 'prod' && (( github.ref == 'refs/heads/master'
|
||||||
(github.ref == 'refs/heads/master'
|
|
||||||
&& needs.setup.outputs.rc_branch_exists == 0
|
&& needs.setup.outputs.rc_branch_exists == 0
|
||||||
&& needs.setup.outputs.hotfix_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/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc' ) }}
|
||||||
run: |
|
run: |
|
||||||
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
|
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/netcoreapp3.1/Publisher.dll"
|
||||||
CREDS_PATH="$HOME/secrets/play_creds.json"
|
CREDS_PATH="$HOME/secrets/play_creds.json"
|
||||||
@@ -242,6 +308,9 @@ jobs:
|
|||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Setup Windows builder
|
||||||
|
run: choco install checksum --no-progress
|
||||||
|
|
||||||
- name: Work Around for broken Windows 2022 Runner Image
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
run: |
|
run: |
|
||||||
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
@@ -412,10 +481,22 @@ jobs:
|
|||||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Create checksum for F-Droid artifact
|
||||||
|
run: |
|
||||||
|
checksum -f="./com.x8bit.bitwarden-fdroid.apk" `
|
||||||
|
-t sha256 | Out-File -Encoding ASCII ./bw-fdroid-apk-sha256.txt
|
||||||
|
|
||||||
|
- name: Upload F-Droid sha file
|
||||||
|
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
|
||||||
|
with:
|
||||||
|
name: bw-fdroid-apk-sha256.txt
|
||||||
|
path: ./bw-fdroid-apk-sha256.txt
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
ios:
|
ios:
|
||||||
name: Apple iOS
|
name: Apple iOS
|
||||||
runs-on: macos-11
|
runs-on: macos-12
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
- name: Setup NuGet
|
- name: Setup NuGet
|
||||||
@@ -441,10 +522,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
env:
|
||||||
with:
|
KEYVAULT: bitwarden-prod-kv
|
||||||
keyvault: "bitwarden-prod-kv"
|
SECRETS: |
|
||||||
secrets: "appcenter-ios-token"
|
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
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
@@ -465,6 +553,14 @@ jobs:
|
|||||||
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
--output $HOME/secrets/dist_share_extension.mobileprovision \
|
--output $HOME/secrets/dist_share_extension.mobileprovision \
|
||||||
./.github/secrets/dist_share_extension.mobileprovision.gpg
|
./.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
|
||||||
|
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
|
||||||
|
--output ./src/watchOS/bitwarden/GoogleService-Info.plist ./.github/secrets/GoogleService-Info.plist.gpg
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Increment version
|
- name: Increment version
|
||||||
@@ -479,6 +575,9 @@ jobs:
|
|||||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist
|
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist
|
||||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist
|
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist
|
||||||
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
|
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
|
||||||
|
cd src/watchOS/bitwarden
|
||||||
|
agvtool new-version -all $BUILD_NUMBER
|
||||||
|
cd ../../..
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Update Entitlements
|
- name: Update Entitlements
|
||||||
@@ -513,6 +612,8 @@ jobs:
|
|||||||
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
|
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
|
||||||
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
|
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
|
||||||
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_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
|
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
|
||||||
mkdir -p "$PROFILES_DIR_PATH"
|
mkdir -p "$PROFILES_DIR_PATH"
|
||||||
@@ -528,6 +629,25 @@ jobs:
|
|||||||
|
|
||||||
SHARE_EXTENSION_UUID=$(grep UUID -A1 -a $SHARE_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
|
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"
|
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 "########################################"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Restore packages
|
- name: Restore packages
|
||||||
@@ -565,7 +685,12 @@ jobs:
|
|||||||
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
|
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
|
||||||
EXPORT_PATH="./bitwarden-export"
|
EXPORT_PATH="./bitwarden-export"
|
||||||
|
|
||||||
cp -r $ARCHIVE_DSYMS_PATH $EXPORT_PATH
|
WATCH_ARCHIVE_DSYMS_PATH="./src/watchOS/bitwarden.xcarchive/dSYMs/"
|
||||||
|
WATCH_DSYMS_EXPORT_PATH="$EXPORT_PATH/Watch_dSYMs"
|
||||||
|
|
||||||
|
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
|
||||||
|
mkdir $WATCH_DSYMS_EXPORT_PATH
|
||||||
|
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Upload App Store .ipa & dSYMs artifacts
|
- name: Upload App Store .ipa & dSYMs artifacts
|
||||||
@@ -598,6 +723,22 @@ jobs:
|
|||||||
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
|
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload Watch dSYMs to Firebase Crashlytics
|
||||||
|
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'
|
||||||
|
run: |
|
||||||
|
|
||||||
|
echo "########################################"
|
||||||
|
echo "##### Uploading Watch dSYMs to Firebase"
|
||||||
|
echo "########################################"
|
||||||
|
|
||||||
|
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Deploy to App Store
|
- name: Deploy to App Store
|
||||||
if: |
|
if: |
|
||||||
(github.ref == 'refs/heads/master'
|
(github.ref == 'refs/heads/master'
|
||||||
@@ -635,10 +776,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
env:
|
||||||
with:
|
KEYVAULT: bitwarden-prod-kv
|
||||||
keyvault: "bitwarden-prod-kv"
|
SECRETS: |
|
||||||
secrets: "crowdin-api-token"
|
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
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
||||||
@@ -695,11 +843,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
env:
|
||||||
keyvault: "bitwarden-prod-kv"
|
KEYVAULT: bitwarden-prod-kv
|
||||||
secrets: "devops-alerts-slack-webhook-url"
|
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
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||||
|
|||||||
10
.github/workflows/crowdin-pull.yml
vendored
10
.github/workflows/crowdin-pull.yml
vendored
@@ -24,10 +24,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-prod-kv"
|
keyvault: "bitwarden-prod-kv"
|
||||||
secrets: "crowdin-api-token"
|
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
||||||
@@ -40,10 +40,12 @@ jobs:
|
|||||||
upload_sources: false
|
upload_sources: false
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
github_user_name: "github-actions"
|
github_user_name: "bitwarden-devops-bot"
|
||||||
github_user_email: "<>"
|
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
commit_message: "Autosync the updated translations"
|
commit_message: "Autosync the updated translations"
|
||||||
localization_branch_name: crowdin-auto-sync
|
localization_branch_name: crowdin-auto-sync
|
||||||
create_pull_request: true
|
create_pull_request: true
|
||||||
pull_request_title: "Autosync Crowdin Translations"
|
pull_request_title: "Autosync Crowdin Translations"
|
||||||
pull_request_body: "Autosync the updated 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 }}
|
||||||
|
|||||||
4
.github/workflows/enforce-labels.yml
vendored
4
.github/workflows/enforce-labels.yml
vendored
@@ -12,5 +12,5 @@ jobs:
|
|||||||
- name: Enforce Label
|
- name: Enforce Label
|
||||||
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
|
||||||
with:
|
with:
|
||||||
BANNED_LABELS: "hold"
|
BANNED_LABELS: "hold,needs-qa"
|
||||||
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"
|
BANNED_LABELS_DESCRIPTION: "PRs with the hold or needs-qa labels cannot be merged"
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Release
|
name: Release
|
||||||
|
run-name: Release ${{ inputs.release_type }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -51,9 +52,10 @@ jobs:
|
|||||||
id: branch
|
id: branch
|
||||||
run: |
|
run: |
|
||||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create GitHub deployment
|
- name: Create GitHub deployment
|
||||||
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
||||||
id: deployment
|
id: deployment
|
||||||
with:
|
with:
|
||||||
@@ -72,7 +74,7 @@ jobs:
|
|||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ steps.branch.outputs.branch-name }}
|
branch: ${{ steps.branch.outputs.branch-name }}
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Dry Run - Download all artifacts
|
||||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
with:
|
with:
|
||||||
@@ -90,7 +92,9 @@ jobs:
|
|||||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||||
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
./com.x8bit.bitwarden.apk/com.x8bit.bitwarden.apk,
|
||||||
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
|
./com.x8bit.bitwarden-fdroid.apk/com.x8bit.bitwarden-fdroid.apk,
|
||||||
./Bitwarden iOS.zip"
|
./Bitwarden iOS.zip,
|
||||||
|
./bw-android-apk-sha256.txt,
|
||||||
|
./bw-fdroid-apk-sha256.txt"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
tag: v${{ steps.version.outputs.version }}
|
tag: v${{ steps.version.outputs.version }}
|
||||||
name: Version ${{ steps.version.outputs.version }}
|
name: Version ${{ steps.version.outputs.version }}
|
||||||
@@ -99,7 +103,7 @@ jobs:
|
|||||||
draft: true
|
draft: true
|
||||||
|
|
||||||
- name: Update deployment status to Success
|
- name: Update deployment status to Success
|
||||||
if: ${{ success() }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }}
|
||||||
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
@@ -107,7 +111,7 @@ jobs:
|
|||||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
- name: Update deployment status to Failure
|
- name: Update deployment status to Failure
|
||||||
if: ${{ failure() }}
|
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }}
|
||||||
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
with:
|
with:
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
@@ -133,7 +137,7 @@ jobs:
|
|||||||
branch: ${{ needs.release.outputs.branch-name }}
|
branch: ${{ needs.release.outputs.branch-name }}
|
||||||
name: com.x8bit.bitwarden-fdroid.apk
|
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' }}
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
with:
|
with:
|
||||||
|
|||||||
42
.github/workflows/version-auto-bump.yml
vendored
42
.github/workflows/version-auto-bump.yml
vendored
@@ -2,39 +2,38 @@
|
|||||||
name: Version Auto Bump
|
name: Version Auto Bump
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
push:
|
||||||
types: [published]
|
tags:
|
||||||
|
- v**
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
name: "Setup"
|
name: "Setup"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
version_number: ${{ steps.version.outputs.new-version }}
|
version_number: ${{ steps.version.outputs.new-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
- name: Get version to bump
|
- name: Calculate bumped version
|
||||||
id: version
|
id: version
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ github.event.release.tag }}
|
RELEASE_TAG: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/')
|
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
|
||||||
CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/')
|
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
|
||||||
echo $CURR_VER
|
echo "Current Major: $CURR_MAJOR"
|
||||||
|
echo "Current Patch: $CURR_PATCH"
|
||||||
|
|
||||||
((CURR_VER++))
|
NEW_PATCH=$((CURR_PATCH+1))
|
||||||
NEW_VER=$CURR_MAJOR$CURR_VER
|
NEW_VER=$CURR_MAJOR.$NEW_PATCH
|
||||||
|
echo "New Version: $NEW_VER"
|
||||||
echo $NEW_VER
|
echo "new-version=$NEW_VER" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
echo "::set-output name=new-version::$NEW_VER"
|
|
||||||
|
|
||||||
trigger_version_bump:
|
trigger_version_bump:
|
||||||
name: "Trigger version bump workflow"
|
name: "Trigger version bump workflow"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- setup
|
- setup
|
||||||
steps:
|
steps:
|
||||||
@@ -45,13 +44,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve secrets
|
- name: Retrieve secrets
|
||||||
id: retrieve-secrets
|
id: retrieve-secrets
|
||||||
env:
|
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||||
KEYVAULT: bitwarden-prod-kv
|
with:
|
||||||
SECRET: "github-pat-bitwarden-devops-bot-repo-scope"
|
keyvault: "bitwarden-prod-kv"
|
||||||
run: |
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv)
|
|
||||||
echo "::add-mask::$VALUE"
|
|
||||||
echo "::set-output name=$SECRET::$VALUE"
|
|
||||||
|
|
||||||
- name: Call GitHub API to trigger workflow bump
|
- name: Call GitHub API to trigger workflow bump
|
||||||
env:
|
env:
|
||||||
|
|||||||
34
.github/workflows/version-bump.yml
vendored
34
.github/workflows/version-bump.yml
vendored
@@ -16,9 +16,28 @@ jobs:
|
|||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
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
|
- name: Create Version Branch
|
||||||
run: |
|
run: git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
|
||||||
|
|
||||||
- name: Bump Version - Android XML
|
- name: Bump Version - Android XML
|
||||||
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
|
||||||
@@ -52,23 +71,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup git
|
- name: Setup git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
git config --local user.name "github-actions[bot]"
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
|
|
||||||
- name: Check if version changed
|
- name: Check if version changed
|
||||||
id: version-changed
|
id: version-changed
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
echo "::set-output name=changes_to_commit::TRUE"
|
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "::set-output name=changes_to_commit::FALSE"
|
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
|
||||||
echo "No changes to commit!";
|
echo "No changes to commit!";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: |
|
run: git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
||||||
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
|
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
|
|||||||
127
.gitignore
vendored
127
.gitignore
vendored
@@ -30,6 +30,7 @@ Components/
|
|||||||
[Rr]eleases/
|
[Rr]eleases/
|
||||||
x64/
|
x64/
|
||||||
x86/
|
x86/
|
||||||
|
!src/lib/x86/
|
||||||
build/
|
build/
|
||||||
bld/
|
bld/
|
||||||
[Bb]in/
|
[Bb]in/
|
||||||
@@ -209,3 +210,129 @@ FakesAssemblies/
|
|||||||
project.lock.json
|
project.lock.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
src/App/Css
|
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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
|
|||||||
|
|
||||||
# Build/Run
|
# 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!
|
# We're Hiring!
|
||||||
|
|
||||||
|
|||||||
346
build.cake
Normal file
346
build.cake
Normal file
@@ -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<bool>("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<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
|
||||||
|
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"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=<bool> Script debug mode.
|
||||||
|
";
|
||||||
|
Information(usage);
|
||||||
|
});
|
||||||
|
#endregion Main Tasks
|
||||||
|
|
||||||
|
RunTarget(target);
|
||||||
BIN
lib/ios/libargon2.a
Normal file
BIN
lib/ios/libargon2.a
Normal file
Binary file not shown.
@@ -33,6 +33,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
// - Resources/xml/autofillservice.xml
|
// - Resources/xml/autofillservice.xml
|
||||||
new Browser("alook.browser", "search_fragment_input_view"),
|
new Browser("alook.browser", "search_fragment_input_view"),
|
||||||
new Browser("alook.browser.google", "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.amazon.cloud9", "url"),
|
||||||
new Browser("com.android.browser", "url"),
|
new Browser("com.android.browser", "url"),
|
||||||
new Browser("com.android.chrome", "url_bar"),
|
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.mmbox.xbrowser", "search_box"),
|
||||||
new Browser("com.mycompany.app.soulbrowser", "edit_text"),
|
new Browser("com.mycompany.app.soulbrowser", "edit_text"),
|
||||||
new Browser("com.naver.whale", "url_bar"),
|
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", "url_field"),
|
||||||
new Browser("com.opera.browser.beta", "url_field"),
|
new Browser("com.opera.browser.beta", "url_field"),
|
||||||
new Browser("com.opera.gx", "addressbarEdit"),
|
new Browser("com.opera.gx", "addressbarEdit"),
|
||||||
@@ -74,6 +76,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.opera.touch", "addressbarEdit"),
|
new Browser("com.opera.touch", "addressbarEdit"),
|
||||||
new Browser("com.qflair.browserq", "url"),
|
new Browser("com.qflair.browserq", "url"),
|
||||||
new Browser("com.qwant.liberty", "mozac_browser_toolbar_url_view,url_bar_title"), // 2nd = Legacy (before v4)
|
new Browser("com.qwant.liberty", "mozac_browser_toolbar_url_view,url_bar_title"), // 2nd = Legacy (before v4)
|
||||||
|
new Browser("com.rainsee.create", "search_box"),
|
||||||
new Browser("com.sec.android.app.sbrowser", "location_bar_edit_text"),
|
new Browser("com.sec.android.app.sbrowser", "location_bar_edit_text"),
|
||||||
new Browser("com.sec.android.app.sbrowser.beta", "location_bar_edit_text"),
|
new Browser("com.sec.android.app.sbrowser.beta", "location_bar_edit_text"),
|
||||||
new Browser("com.stoutner.privacybrowser.free", "url_edittext"),
|
new Browser("com.stoutner.privacybrowser.free", "url_edittext"),
|
||||||
@@ -83,6 +86,9 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.vivaldi.browser.sopranos", "url_bar"),
|
new Browser("com.vivaldi.browser.sopranos", "url_bar"),
|
||||||
new Browser("com.yandex.browser", "bro_omnibar_address_title_text,bro_omnibox_collapsed_title",
|
new Browser("com.yandex.browser", "bro_omnibar_address_title_text,bro_omnibox_collapsed_title",
|
||||||
(s) => s.Split(new char[]{' ', ' '}).FirstOrDefault()), // 0 = Regular Space, 1 = No-break space (00A0)
|
(s) => s.Split(new char[]{' ', ' '}).FirstOrDefault()), // 0 = Regular Space, 1 = No-break space (00A0)
|
||||||
|
new Browser("com.yjllq.internet", "search_box"),
|
||||||
|
new Browser("com.yjllq.kito", "search_box"),
|
||||||
|
new Browser("com.yujian.ResideMenuDemo", "search_box"),
|
||||||
new Browser("com.z28j.feel", "g2"),
|
new Browser("com.z28j.feel", "g2"),
|
||||||
new Browser("idm.internet.download.manager", "search"),
|
new Browser("idm.internet.download.manager", "search"),
|
||||||
new Browser("idm.internet.download.manager.adm.lite", "search"),
|
new Browser("idm.internet.download.manager.adm.lite", "search"),
|
||||||
@@ -90,6 +96,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("io.github.forkmaintainers.iceraven", "mozac_browser_toolbar_url_view"),
|
new Browser("io.github.forkmaintainers.iceraven", "mozac_browser_toolbar_url_view"),
|
||||||
new Browser("mark.via", "am,an"),
|
new Browser("mark.via", "am,an"),
|
||||||
new Browser("mark.via.gp", "as"),
|
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", "search"),
|
||||||
new Browser("net.slions.fulguris.full.download.debug", "search"),
|
new Browser("net.slions.fulguris.full.download.debug", "search"),
|
||||||
new Browser("net.slions.fulguris.full.playstore", "search"),
|
new Browser("net.slions.fulguris.full.playstore", "search"),
|
||||||
@@ -129,6 +136,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.htc.sense.browser", "title"),
|
new Browser("com.htc.sense.browser", "title"),
|
||||||
new Browser("com.jerky.browser2", "enterUrl"),
|
new Browser("com.jerky.browser2", "enterUrl"),
|
||||||
new Browser("com.ksmobile.cb", "address_bar_edit_text"),
|
new Browser("com.ksmobile.cb", "address_bar_edit_text"),
|
||||||
|
new Browser("com.lemurbrowser.exts","url_bar"),
|
||||||
new Browser("com.linkbubble.playstore", "url_text"),
|
new Browser("com.linkbubble.playstore", "url_text"),
|
||||||
new Browser("com.mx.browser", "address_editor_with_progress"),
|
new Browser("com.mx.browser", "address_editor_with_progress"),
|
||||||
new Browser("com.mx.browser.tablet", "address_editor_with_progress"),
|
new Browser("com.mx.browser.tablet", "address_editor_with_progress"),
|
||||||
@@ -367,7 +375,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
|
|
||||||
public static string GetUri(AccessibilityNodeInfo root)
|
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))
|
if (SupportedBrowsers.ContainsKey(root.PackageName))
|
||||||
{
|
{
|
||||||
var browser = SupportedBrowsers[root.PackageName];
|
var browser = SupportedBrowsers[root.PackageName];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Droid.Accessibility
|
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" })]
|
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
|
||||||
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
|
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
|
||||||
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
|
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
||||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
||||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||||
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v13.0</TargetFrameworkVersion>
|
||||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
||||||
<NuGetPackageImportStamp>
|
<NuGetPackageImportStamp>
|
||||||
</NuGetPackageImportStamp>
|
</NuGetPackageImportStamp>
|
||||||
@@ -75,24 +75,24 @@
|
|||||||
<Version>2.1.0.4</Version>
|
<Version>2.1.0.4</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Portable.BouncyCastle">
|
<PackageReference Include="Portable.BouncyCastle">
|
||||||
<Version>1.8.10</Version>
|
<Version>1.9.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
|
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1.1" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.9" />
|
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.14" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.11" />
|
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.17" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.10" />
|
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0.1" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
|
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.15" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
|
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1.1" />
|
||||||
<PackageReference Include="Xamarin.Essentials">
|
<PackageReference Include="Xamarin.Essentials">
|
||||||
<Version>1.7.3</Version>
|
<Version>1.7.3</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||||
<Version>122.0.0</Version>
|
<Version>123.0.8</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.4.0.4" />
|
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
|
||||||
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
|
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||||
<Version>117.0.1</Version>
|
<Version>118.0.1.2</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -103,8 +103,10 @@
|
|||||||
<Compile Include="Accessibility\Browser.cs" />
|
<Compile Include="Accessibility\Browser.cs" />
|
||||||
<Compile Include="Accessibility\NodeList.cs" />
|
<Compile Include="Accessibility\NodeList.cs" />
|
||||||
<Compile Include="Accessibility\KnownUsernameField.cs" />
|
<Compile Include="Accessibility\KnownUsernameField.cs" />
|
||||||
|
<Compile Include="Autofill\AutofillConstants.cs" />
|
||||||
<Compile Include="Autofill\AutofillHelpers.cs" />
|
<Compile Include="Autofill\AutofillHelpers.cs" />
|
||||||
<Compile Include="Autofill\AutofillService.cs" />
|
<Compile Include="Autofill\AutofillService.cs" />
|
||||||
|
<Compile Include="Autofill\AutofillExternalSelectionActivity.cs" />
|
||||||
<Compile Include="Autofill\Field.cs" />
|
<Compile Include="Autofill\Field.cs" />
|
||||||
<Compile Include="Autofill\FieldCollection.cs" />
|
<Compile Include="Autofill\FieldCollection.cs" />
|
||||||
<Compile Include="Autofill\FilledItem.cs" />
|
<Compile Include="Autofill\FilledItem.cs" />
|
||||||
@@ -152,6 +154,12 @@
|
|||||||
<Compile Include="Utilities\IntentExtensions.cs" />
|
<Compile Include="Utilities\IntentExtensions.cs" />
|
||||||
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
||||||
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
||||||
|
<Compile Include="Receivers\NotificationDismissReceiver.cs" />
|
||||||
|
<Compile Include="Services\FileService.cs" />
|
||||||
|
<Compile Include="Services\AutofillHandler.cs" />
|
||||||
|
<Compile Include="Constants.cs" />
|
||||||
|
<Compile Include="Effects\RemoveFontPaddingEffect.cs" />
|
||||||
|
<Compile Include="Services\WatchDeviceService.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
@@ -161,6 +169,10 @@
|
|||||||
<GoogleServicesJson Include="google-services.json" />
|
<GoogleServicesJson Include="google-services.json" />
|
||||||
<GoogleServicesJson Include="google-services.json.enc" />
|
<GoogleServicesJson Include="google-services.json.enc" />
|
||||||
<None Include="fdroid-keystore.jks.enc" />
|
<None Include="fdroid-keystore.jks.enc" />
|
||||||
|
<AndroidNativeLibrary Include="lib\arm64-v8a\libargon2.so" />
|
||||||
|
<AndroidNativeLibrary Include="lib\armeabi-v7a\libargon2.so" />
|
||||||
|
<AndroidNativeLibrary Include="lib\x86\libargon2.so" />
|
||||||
|
<AndroidNativeLibrary Include="lib\x86_64\libargon2.so" />
|
||||||
<None Include="Properties\AndroidManifest.xml" />
|
<None Include="Properties\AndroidManifest.xml" />
|
||||||
<None Include="upload-keystore.jks.enc" />
|
<None Include="upload-keystore.jks.enc" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -176,6 +188,7 @@
|
|||||||
<AndroidResource Include="Resources\drawable\cog_settings.xml" />
|
<AndroidResource Include="Resources\drawable\cog_settings.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\icon.xml" />
|
<AndroidResource Include="Resources\drawable\icon.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\ic_launcher_foreground.xml" />
|
<AndroidResource Include="Resources\drawable\ic_launcher_foreground.xml" />
|
||||||
|
<AndroidResource Include="Resources\drawable\ic_launcher_monochrome.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\ic_warning.xml" />
|
<AndroidResource Include="Resources\drawable\ic_warning.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\id.xml" />
|
<AndroidResource Include="Resources\drawable\id.xml" />
|
||||||
<AndroidResource Include="Resources\drawable\info.xml" />
|
<AndroidResource Include="Resources\drawable\info.xml" />
|
||||||
@@ -213,6 +226,13 @@
|
|||||||
<AndroidResource Include="Resources\values\colors.xml" />
|
<AndroidResource Include="Resources\values\colors.xml" />
|
||||||
<AndroidResource Include="Resources\values\manifest.xml" />
|
<AndroidResource Include="Resources\values\manifest.xml" />
|
||||||
<AndroidResource Include="Resources\values-v30\manifest.xml" />
|
<AndroidResource Include="Resources\values-v30\manifest.xml" />
|
||||||
|
<AndroidResource Include="Resources\drawable-v26\splash_screen_round.xml" />
|
||||||
|
<AndroidResource Include="Resources\drawable\logo_rounded.xml" />
|
||||||
|
<AndroidResource Include="Resources\drawable-night-v26\splash_screen_round.xml" />
|
||||||
|
<AndroidResource Include="Resources\drawable\ic_notification.xml">
|
||||||
|
<SubType></SubType>
|
||||||
|
<Generator></Generator>
|
||||||
|
</AndroidResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
|
||||||
@@ -280,6 +300,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\values-v30\" />
|
<Folder Include="Resources\values-v30\" />
|
||||||
|
<Folder Include="Resources\drawable-v26\" />
|
||||||
|
<Folder Include="Resources\drawable-night-v26\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
Binary file not shown.
10
src/Android/Autofill/AutofillConstants.cs
Normal file
10
src/Android/Autofill/AutofillConstants.cs
Normal file
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/Android/Autofill/AutofillExternalSelectionActivity.cs
Normal file
42
src/Android/Autofill/AutofillExternalSelectionActivity.cs
Normal file
@@ -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<ICipherService>();
|
||||||
|
var cipher = await cipherService.GetAsync(cipherId);
|
||||||
|
var decCipher = await cipher.DecryptAsync();
|
||||||
|
|
||||||
|
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
|
autofillHandler.Autofill(decCipher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ using AndroidX.AutoFill.Inline;
|
|||||||
using AndroidX.AutoFill.Inline.V1;
|
using AndroidX.AutoFill.Inline.V1;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
using SaveFlags = Android.Service.Autofill.SaveFlags;
|
||||||
|
using Bit.Droid.Utilities;
|
||||||
|
|
||||||
namespace Bit.Droid.Autofill
|
namespace Bit.Droid.Autofill
|
||||||
{
|
{
|
||||||
@@ -53,6 +54,7 @@ namespace Bit.Droid.Autofill
|
|||||||
{
|
{
|
||||||
"alook.browser",
|
"alook.browser",
|
||||||
"alook.browser.google",
|
"alook.browser.google",
|
||||||
|
"app.vanadium.browser",
|
||||||
"com.amazon.cloud9",
|
"com.amazon.cloud9",
|
||||||
"com.android.browser",
|
"com.android.browser",
|
||||||
"com.android.chrome",
|
"com.android.chrome",
|
||||||
@@ -77,6 +79,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.jamal2367.styx",
|
"com.jamal2367.styx",
|
||||||
"com.kiwibrowser.browser",
|
"com.kiwibrowser.browser",
|
||||||
"com.kiwibrowser.browser.dev",
|
"com.kiwibrowser.browser.dev",
|
||||||
|
"com.lemurbrowser.exts",
|
||||||
"com.microsoft.emmx",
|
"com.microsoft.emmx",
|
||||||
"com.microsoft.emmx.beta",
|
"com.microsoft.emmx.beta",
|
||||||
"com.microsoft.emmx.canary",
|
"com.microsoft.emmx.canary",
|
||||||
@@ -85,6 +88,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.mmbox.xbrowser",
|
"com.mmbox.xbrowser",
|
||||||
"com.mycompany.app.soulbrowser",
|
"com.mycompany.app.soulbrowser",
|
||||||
"com.naver.whale",
|
"com.naver.whale",
|
||||||
|
"com.neeva.app",
|
||||||
"com.opera.browser",
|
"com.opera.browser",
|
||||||
"com.opera.browser.beta",
|
"com.opera.browser.beta",
|
||||||
"com.opera.gx",
|
"com.opera.gx",
|
||||||
@@ -93,6 +97,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.opera.touch",
|
"com.opera.touch",
|
||||||
"com.qflair.browserq",
|
"com.qflair.browserq",
|
||||||
"com.qwant.liberty",
|
"com.qwant.liberty",
|
||||||
|
"com.rainsee.create",
|
||||||
"com.sec.android.app.sbrowser",
|
"com.sec.android.app.sbrowser",
|
||||||
"com.sec.android.app.sbrowser.beta",
|
"com.sec.android.app.sbrowser.beta",
|
||||||
"com.stoutner.privacybrowser.free",
|
"com.stoutner.privacybrowser.free",
|
||||||
@@ -101,6 +106,9 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.vivaldi.browser.snapshot",
|
"com.vivaldi.browser.snapshot",
|
||||||
"com.vivaldi.browser.sopranos",
|
"com.vivaldi.browser.sopranos",
|
||||||
"com.yandex.browser",
|
"com.yandex.browser",
|
||||||
|
"com.yjllq.internet",
|
||||||
|
"com.yjllq.kito",
|
||||||
|
"com.yujian.ResideMenuDemo",
|
||||||
"com.z28j.feel",
|
"com.z28j.feel",
|
||||||
"idm.internet.download.manager",
|
"idm.internet.download.manager",
|
||||||
"idm.internet.download.manager.adm.lite",
|
"idm.internet.download.manager.adm.lite",
|
||||||
@@ -108,6 +116,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"io.github.forkmaintainers.iceraven",
|
"io.github.forkmaintainers.iceraven",
|
||||||
"mark.via",
|
"mark.via",
|
||||||
"mark.via.gp",
|
"mark.via.gp",
|
||||||
|
"net.dezor.browser",
|
||||||
"net.slions.fulguris.full.download",
|
"net.slions.fulguris.full.download",
|
||||||
"net.slions.fulguris.full.download.debug",
|
"net.slions.fulguris.full.download.debug",
|
||||||
"net.slions.fulguris.full.playstore",
|
"net.slions.fulguris.full.playstore",
|
||||||
@@ -206,7 +215,7 @@ namespace Bit.Droid.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i],
|
var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, items[i],
|
||||||
inlinePresentationSpec);
|
true, inlinePresentationSpec);
|
||||||
if (dataset != null)
|
if (dataset != null)
|
||||||
{
|
{
|
||||||
responseBuilder.AddDataset(dataset);
|
responseBuilder.AddDataset(dataset);
|
||||||
@@ -220,7 +229,7 @@ namespace Bit.Droid.Autofill
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem,
|
public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem,
|
||||||
InlinePresentationSpec inlinePresentationSpec = null)
|
bool includeAuthIntent, InlinePresentationSpec inlinePresentationSpec = null)
|
||||||
{
|
{
|
||||||
var overlayPresentation = BuildOverlayPresentation(
|
var overlayPresentation = BuildOverlayPresentation(
|
||||||
filledItem.Name,
|
filledItem.Name,
|
||||||
@@ -241,6 +250,15 @@ namespace Bit.Droid.Autofill
|
|||||||
{
|
{
|
||||||
datasetBuilder.SetInlinePresentation(inlinePresentation);
|
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))
|
if (filledItem.ApplyToFields(fields, datasetBuilder))
|
||||||
{
|
{
|
||||||
return datasetBuilder.Build();
|
return datasetBuilder.Build();
|
||||||
@@ -252,26 +270,26 @@ namespace Bit.Droid.Autofill
|
|||||||
IList<InlinePresentationSpec> inlinePresentationSpecs = null)
|
IList<InlinePresentationSpec> inlinePresentationSpecs = null)
|
||||||
{
|
{
|
||||||
var intent = new Intent(context, typeof(MainActivity));
|
var intent = new Intent(context, typeof(MainActivity));
|
||||||
intent.PutExtra("autofillFramework", true);
|
intent.PutExtra(AutofillConstants.AutofillFramework, true);
|
||||||
if (fields.FillableForLogin)
|
if (fields.FillableForLogin)
|
||||||
{
|
{
|
||||||
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login);
|
intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Login);
|
||||||
}
|
}
|
||||||
else if (fields.FillableForCard)
|
else if (fields.FillableForCard)
|
||||||
{
|
{
|
||||||
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card);
|
intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Card);
|
||||||
}
|
}
|
||||||
else if (fields.FillableForIdentity)
|
else if (fields.FillableForIdentity)
|
||||||
{
|
{
|
||||||
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity);
|
intent.PutExtra(AutofillConstants.AutofillFrameworkFillType, (int)CipherType.Identity);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
intent.PutExtra("autofillFrameworkUri", uri);
|
intent.PutExtra(AutofillConstants.AutofillFrameworkUri, uri);
|
||||||
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
|
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
|
||||||
PendingIntentFlags.CancelCurrent);
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
|
||||||
|
|
||||||
var overlayPresentation = BuildOverlayPresentation(
|
var overlayPresentation = BuildOverlayPresentation(
|
||||||
AppResources.AutofillWithBitwarden,
|
AppResources.AutofillWithBitwarden,
|
||||||
@@ -324,7 +342,7 @@ namespace Bit.Droid.Autofill
|
|||||||
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
|
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
|
||||||
// "my vault" presentation) so we're including an empty one here
|
// "my vault" presentation) so we're including an empty one here
|
||||||
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
|
||||||
PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent);
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
|
||||||
}
|
}
|
||||||
var slice = CreateInlinePresentationSlice(
|
var slice = CreateInlinePresentationSlice(
|
||||||
inlinePresentationSpec,
|
inlinePresentationSpec,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Droid.Autofill
|
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" })]
|
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
|
||||||
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
|
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
|
||||||
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
|
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
|
||||||
@@ -134,7 +134,7 @@ namespace Bit.Droid.Autofill
|
|||||||
{
|
{
|
||||||
case CipherType.Login:
|
case CipherType.Login:
|
||||||
intent.PutExtra("autofillFrameworkName", parser.Uri
|
intent.PutExtra("autofillFrameworkName", parser.Uri
|
||||||
.Replace(Constants.AndroidAppProtocol, string.Empty)
|
.Replace(Core.Constants.AndroidAppProtocol, string.Empty)
|
||||||
.Replace("https://", string.Empty)
|
.Replace("https://", string.Empty)
|
||||||
.Replace("http://", string.Empty));
|
.Replace("http://", string.Empty));
|
||||||
intent.PutExtra("autofillFrameworkUri", parser.Uri);
|
intent.PutExtra("autofillFrameworkUri", parser.Uri);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace Bit.Droid.Autofill
|
|||||||
private List<Field> _passwordFields = null;
|
private List<Field> _passwordFields = null;
|
||||||
private List<Field> _usernameFields = null;
|
private List<Field> _usernameFields = null;
|
||||||
private HashSet<string> _ignoreSearchTerms = new HashSet<string> { "search", "find", "recipient", "edit" };
|
private HashSet<string> _ignoreSearchTerms = new HashSet<string> { "search", "find", "recipient", "edit" };
|
||||||
|
private HashSet<string> _usernameTerms = new HashSet<string> { "email", "phone", "username"};
|
||||||
private HashSet<string> _passwordTerms = new HashSet<string> { "password", "pswd" };
|
private HashSet<string> _passwordTerms = new HashSet<string> { "password", "pswd" };
|
||||||
|
|
||||||
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
|
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
|
||||||
@@ -98,6 +99,11 @@ namespace Bit.Droid.Autofill
|
|||||||
_usernameFields.Add(usernameField);
|
_usernameFields.Add(usernameField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_usernameFields.Any())
|
||||||
|
{
|
||||||
|
_usernameFields = Fields.Where(f => FieldIsUsername(f)).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _usernameFields;
|
return _usernameFields;
|
||||||
}
|
}
|
||||||
@@ -321,7 +327,7 @@ namespace Bit.Droid.Autofill
|
|||||||
}
|
}
|
||||||
|
|
||||||
return inputTypePassword && !ValueContainsAnyTerms(f.IdEntry, _ignoreSearchTerms) &&
|
return inputTypePassword && !ValueContainsAnyTerms(f.IdEntry, _ignoreSearchTerms) &&
|
||||||
!ValueContainsAnyTerms(f.Hint, _ignoreSearchTerms);
|
!ValueContainsAnyTerms(f.Hint, _ignoreSearchTerms) && !FieldIsUsername(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool FieldHasPasswordTerms(Field f)
|
private bool FieldHasPasswordTerms(Field f)
|
||||||
@@ -329,6 +335,16 @@ namespace Bit.Droid.Autofill
|
|||||||
return ValueContainsAnyTerms(f.IdEntry, _passwordTerms) || ValueContainsAnyTerms(f.Hint, _passwordTerms);
|
return ValueContainsAnyTerms(f.IdEntry, _passwordTerms) || ValueContainsAnyTerms(f.Hint, _passwordTerms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool FieldIsUsername(Field f)
|
||||||
|
{
|
||||||
|
return f.InputType.HasFlag(InputTypes.TextVariationWebEmailAddress) || FieldHasUsernameTerms(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FieldHasUsernameTerms(Field f)
|
||||||
|
{
|
||||||
|
return ValueContainsAnyTerms(f.IdEntry, _usernameTerms) || ValueContainsAnyTerms(f.Hint, _usernameTerms);
|
||||||
|
}
|
||||||
|
|
||||||
private bool ValueContainsAnyTerms(string value, HashSet<string> terms)
|
private bool ValueContainsAnyTerms(string value, HashSet<string> terms)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace Bit.Droid.Autofill
|
|||||||
|
|
||||||
public FilledItem(CipherView cipher)
|
public FilledItem(CipherView cipher)
|
||||||
{
|
{
|
||||||
|
Id = cipher.Id;
|
||||||
Name = cipher.Name;
|
Name = cipher.Name;
|
||||||
Type = cipher.Type;
|
Type = cipher.Type;
|
||||||
Subtitle = cipher.SubTitle;
|
Subtitle = cipher.SubTitle;
|
||||||
@@ -55,6 +56,7 @@ namespace Bit.Droid.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Subtitle { get; set; } = string.Empty;
|
public string Subtitle { get; set; } = string.Empty;
|
||||||
public int Icon { get; set; } = Resource.Drawable.login;
|
public int Icon { get; set; } = Resource.Drawable.login;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace Bit.Droid.Autofill
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_uri = string.Concat(Constants.AndroidAppProtocol, PackageName);
|
_uri = string.Concat(Core.Constants.AndroidAppProtocol, PackageName);
|
||||||
}
|
}
|
||||||
return _uri;
|
return _uri;
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/Android/Constants.cs
Normal file
7
src/Android/Constants.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Droid
|
||||||
|
{
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Android/Effects/RemoveFontPaddingEffect.cs
Normal file
23
src/Android/Effects/RemoveFontPaddingEffect.cs
Normal file
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,20 +5,27 @@ using System.Threading.Tasks;
|
|||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
|
using Android.Content.Res;
|
||||||
using Android.Nfc;
|
using Android.Nfc;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Runtime;
|
using Android.Runtime;
|
||||||
using AndroidX.Core.Content;
|
using Android.Views;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Droid.Autofill;
|
||||||
using Bit.Droid.Receivers;
|
using Bit.Droid.Receivers;
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Xamarin.Essentials;
|
||||||
using ZXing.Net.Mobile.Android;
|
using ZXing.Net.Mobile.Android;
|
||||||
|
using FileProvider = AndroidX.Core.Content.FileProvider;
|
||||||
|
|
||||||
namespace Bit.Droid
|
namespace Bit.Droid
|
||||||
{
|
{
|
||||||
@@ -30,11 +37,14 @@ namespace Bit.Droid
|
|||||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||||
{
|
{
|
||||||
private IDeviceActionService _deviceActionService;
|
private IDeviceActionService _deviceActionService;
|
||||||
|
private IFileService _fileService;
|
||||||
private IMessagingService _messagingService;
|
private IMessagingService _messagingService;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
private IStateService _stateService;
|
private IStateService _stateService;
|
||||||
private IAppIdService _appIdService;
|
private IAppIdService _appIdService;
|
||||||
private IEventService _eventService;
|
private IEventService _eventService;
|
||||||
|
private IPushNotificationListenerService _pushNotificationListenerService;
|
||||||
|
private ILogger _logger;
|
||||||
private PendingIntent _eventUploadPendingIntent;
|
private PendingIntent _eventUploadPendingIntent;
|
||||||
private AppOptions _appOptions;
|
private AppOptions _appOptions;
|
||||||
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
|
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
|
||||||
@@ -45,17 +55,20 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
|
||||||
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
|
||||||
PendingIntentFlags.UpdateCurrent);
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false));
|
||||||
|
|
||||||
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
|
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
|
||||||
StrictMode.SetThreadPolicy(policy);
|
StrictMode.SetThreadPolicy(policy);
|
||||||
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||||
|
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
||||||
TabLayoutResource = Resource.Layout.Tabbar;
|
TabLayoutResource = Resource.Layout.Tabbar;
|
||||||
ToolbarResource = Resource.Layout.Toolbar;
|
ToolbarResource = Resource.Layout.Toolbar;
|
||||||
@@ -70,7 +83,7 @@ namespace Bit.Droid
|
|||||||
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
|
||||||
});
|
});
|
||||||
|
|
||||||
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
|
_logger.InitAsync();
|
||||||
|
|
||||||
var toplayout = Window?.DecorView?.RootView;
|
var toplayout = Window?.DecorView?.RootView;
|
||||||
if (toplayout != null)
|
if (toplayout != null)
|
||||||
@@ -81,8 +94,9 @@ namespace Bit.Droid
|
|||||||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||||
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
||||||
_appOptions = GetOptions();
|
_appOptions = GetOptions();
|
||||||
|
CreateNotificationChannel();
|
||||||
LoadApplication(new App.App(_appOptions));
|
LoadApplication(new App.App(_appOptions));
|
||||||
|
DisableAndroidFontScale();
|
||||||
|
|
||||||
_broadcasterService.Subscribe(_activityKey, (message) =>
|
_broadcasterService.Subscribe(_activityKey, (message) =>
|
||||||
{
|
{
|
||||||
@@ -138,6 +152,15 @@ namespace Bit.Droid
|
|||||||
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
|
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
|
||||||
.GetAwaiter()
|
.GetAwaiter()
|
||||||
.GetResult();
|
.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<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNewIntent(Intent intent)
|
protected override void OnNewIntent(Intent intent)
|
||||||
@@ -191,13 +214,13 @@ namespace Bit.Droid
|
|||||||
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
|
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
|
||||||
[GeneratedEnum] Permission[] grantResults)
|
[GeneratedEnum] Permission[] grantResults)
|
||||||
{
|
{
|
||||||
if (requestCode == Constants.SelectFilePermissionRequestCode)
|
if (requestCode == Core.Constants.SelectFilePermissionRequestCode)
|
||||||
{
|
{
|
||||||
if (grantResults.Any(r => r != Permission.Granted))
|
if (grantResults.Any(r => r != Permission.Granted))
|
||||||
{
|
{
|
||||||
_messagingService.Send("selectFileCameraPermissionDenied");
|
_messagingService.Send("selectFileCameraPermissionDenied");
|
||||||
}
|
}
|
||||||
await _deviceActionService.SelectFileAsync();
|
await _fileService.SelectFileAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -210,7 +233,7 @@ namespace Bit.Droid
|
|||||||
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
|
||||||
{
|
{
|
||||||
if (resultCode == Result.Ok &&
|
if (resultCode == Result.Ok &&
|
||||||
(requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode))
|
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
|
||||||
{
|
{
|
||||||
Android.Net.Uri uri = null;
|
Android.Net.Uri uri = null;
|
||||||
string fileName = null;
|
string fileName = null;
|
||||||
@@ -232,7 +255,7 @@ namespace Bit.Droid
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestCode == Constants.SaveFileRequestCode)
|
if (requestCode == Core.Constants.SaveFileRequestCode)
|
||||||
{
|
{
|
||||||
_messagingService.Send("selectSaveFileResult",
|
_messagingService.Send("selectSaveFileResult",
|
||||||
new Tuple<string, string>(uri.ToString(), fileName));
|
new Tuple<string, string>(uri.ToString(), fileName));
|
||||||
@@ -273,7 +296,7 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
var intent = new Intent(this, Class);
|
var intent = new Intent(this, Class);
|
||||||
intent.AddFlags(ActivityFlags.SingleTop);
|
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
|
// register for all NDEF tags starting with http och https
|
||||||
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
|
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
|
||||||
ndef.AddDataScheme("http");
|
ndef.AddDataScheme("http");
|
||||||
@@ -300,13 +323,13 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
var options = new AppOptions
|
var options = new AppOptions
|
||||||
{
|
{
|
||||||
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"),
|
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
|
||||||
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
|
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
|
||||||
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
|
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
|
||||||
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false),
|
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
|
||||||
CreateSend = GetCreateSendRequest(Intent)
|
CreateSend = GetCreateSendRequest(Intent)
|
||||||
};
|
};
|
||||||
var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0);
|
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
|
||||||
if (fillType > 0)
|
if (fillType > 0)
|
||||||
{
|
{
|
||||||
options.FillType = (CipherType)fillType;
|
options.FillType = (CipherType)fillType;
|
||||||
@@ -401,5 +424,38 @@ namespace Bit.Droid
|
|||||||
alarmManager.Cancel(_eventUploadPendingIntent);
|
alarmManager.Cancel(_eventUploadPendingIntent);
|
||||||
await _eventService.UploadEventsAsync();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,17 @@ namespace Bit.Droid
|
|||||||
if (ServiceContainer.RegisteredServices.Count == 0)
|
if (ServiceContainer.RegisteredServices.Count == 0)
|
||||||
{
|
{
|
||||||
RegisterLocalServices();
|
RegisterLocalServices();
|
||||||
|
|
||||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey,
|
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey,
|
||||||
Constants.AndroidAllClearCipherCacheKeys);
|
Core.Constants.AndroidAllClearCipherCacheKeys);
|
||||||
|
|
||||||
|
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
|
||||||
|
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||||
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
ServiceContainer.Resolve<IVaultTimeoutService>()));
|
||||||
|
|
||||||
|
InitializeAppSetup();
|
||||||
|
|
||||||
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
|
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
|
||||||
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
|
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
|
||||||
@@ -71,7 +79,9 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IStateService>("stateService"),
|
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||||
ServiceContainer.Resolve<IAuthService>("authService"),
|
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||||
ServiceContainer.Resolve<ILogger>("logger"));
|
ServiceContainer.Resolve<ILogger>("logger"),
|
||||||
|
ServiceContainer.Resolve<IMessagingService>("messagingService"),
|
||||||
|
ServiceContainer.Resolve<IWatchDeviceService>());
|
||||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
@@ -137,10 +147,12 @@ namespace Bit.Droid
|
|||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var clipboardService = new ClipboardService(stateService);
|
var clipboardService = new ClipboardService(stateService);
|
||||||
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
|
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
||||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
var fileService = new FileService(stateService, broadcasterService);
|
||||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
||||||
messagingService, broadcasterService);
|
messagingService, broadcasterService);
|
||||||
|
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
|
||||||
|
platformUtilsService, new LazyResolve<IEventService>());
|
||||||
var biometricService = new BiometricService();
|
var biometricService = new BiometricService();
|
||||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
|
||||||
@@ -157,6 +169,8 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
||||||
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
||||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||||
|
ServiceContainer.Register<IFileService>(fileService);
|
||||||
|
ServiceContainer.Register<IAutofillHandler>(autofillHandler);
|
||||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||||
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
||||||
@@ -193,5 +207,12 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeAppSetup()
|
||||||
|
{
|
||||||
|
var appSetup = new AppSetup();
|
||||||
|
appSetup.InitializeServicesLastChance();
|
||||||
|
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.8.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2023.1.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
@@ -40,10 +40,16 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
<!-- Package visibility (for Android 11+) -->
|
<!-- Support for Xamarin.Essentials.Browser.OpenAsync (for Android > 11) -->
|
||||||
|
<!-- Related docs: https://learn.microsoft.com/en-us/xamarin/essentials/open-browser?tabs=android -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="*" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#if !FDROID
|
#if !FDROID
|
||||||
|
using System;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Firebase.Messaging;
|
using Firebase.Messaging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -16,14 +18,22 @@ namespace Bit.Droid.Push
|
|||||||
{
|
{
|
||||||
public async override void OnNewToken(string token)
|
public async override void OnNewToken(string token)
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||||
|
|
||||||
await stateService.SetPushRegisteredTokenAsync(token);
|
await stateService.SetPushRegisteredTokenAsync(token);
|
||||||
await pushNotificationService.RegisterAsync();
|
await pushNotificationService.RegisterAsync();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Instance.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async override void OnMessageReceived(RemoteMessage message)
|
public async override void OnMessageReceived(RemoteMessage message)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (message?.Data == null)
|
if (message?.Data == null)
|
||||||
{
|
{
|
||||||
@@ -34,16 +44,15 @@ namespace Bit.Droid.Push
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
|
||||||
var obj = JObject.Parse(data);
|
var obj = JObject.Parse(data);
|
||||||
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
||||||
"pushNotificationListenerService");
|
"pushNotificationListenerService");
|
||||||
await listener.OnMessageAsync(obj, Device.Android);
|
await listener.OnMessageAsync(obj, Device.Android);
|
||||||
}
|
}
|
||||||
catch (JsonReaderException ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine(ex.ToString());
|
Logger.Instance.Exception(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Android.Content;
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
|
|
||||||
namespace Bit.Droid.Receivers
|
namespace Bit.Droid.Receivers
|
||||||
{
|
{
|
||||||
@@ -8,7 +9,17 @@ namespace Bit.Droid.Receivers
|
|||||||
public override void OnReceive(Context context, Intent intent)
|
public override void OnReceive(Context context, Intent intent)
|
||||||
{
|
{
|
||||||
var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
|
var clipboardManager = context.GetSystemService(Context.ClipboardService) as ClipboardManager;
|
||||||
|
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", " ");
|
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", " ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clipboardManager.ClearPrimaryClip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/Android/Receivers/NotificationDismissReceiver.cs
Normal file
41
src/Android/Receivers/NotificationDismissReceiver.cs
Normal file
@@ -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<IPushNotificationListenerService> _pushNotificationListenerService = new LazyResolve<IPushNotificationListenerService>();
|
||||||
|
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
|
|
||||||
|
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<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ namespace Bit.Droid.Renderers
|
|||||||
}
|
}
|
||||||
if (Control != null)
|
if (Control != null)
|
||||||
{
|
{
|
||||||
|
Control.SetHintTextColor(ThemeHelpers.MutedColor);
|
||||||
var t = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.switch_thumb, null);
|
var t = ResourcesCompat.GetDrawable(Resources, Resource.Drawable.switch_thumb, null);
|
||||||
if (t is GradientDrawable thumb)
|
if (t is GradientDrawable thumb)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/darkgray"/>
|
||||||
|
<foreground android:drawable="@drawable/logo_rounded"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/logo_rounded"/>
|
||||||
|
</adaptive-icon>
|
||||||
15
src/Android/Resources/drawable/ic_launcher_monochrome.xml
Normal file
15
src/Android/Resources/drawable/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group
|
||||||
|
android:scaleX="0.099"
|
||||||
|
android:scaleY="0.099"
|
||||||
|
android:translateX="24.3"
|
||||||
|
android:translateY="24.3">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M481.4,102.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6L131.7,96.6c-5.1,0 -9.4,1.9 -13.1,5.6C114.9,105.9 113,110.2 113,115.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8L487.3,115.3C487,110.2 485.1,105.9 481.4,102.2zM438,341.8C438,423 300,493 300,493L300,144.6h138C438,144.6 438,260.6 438,341.8z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
4
src/Android/Resources/drawable/ic_notification.xml
Normal file
4
src/Android/Resources/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<vector android:height="24dp" android:viewportHeight="420"
|
||||||
|
android:viewportWidth="420" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M350.43,40.516C347.563,37.65 344.153,36.178 340.281,36.178L79.487,36.178C75.538,36.178 72.206,37.65 69.338,40.516C66.472,43.384 65,46.716 65,50.665L65,224.527C65,237.466 67.557,250.405 72.593,263.112C77.629,275.895 83.904,287.207 91.42,297.046C98.857,306.964 107.768,316.571 118.149,325.869C128.455,335.242 138.063,342.99 146.817,349.19C155.573,355.387 164.715,361.198 174.243,366.699C183.773,372.2 190.514,375.919 194.543,377.933C198.572,379.871 201.749,381.421 204.151,382.426C205.932,383.357 207.948,383.821 210.04,383.821C212.131,383.821 214.145,383.357 215.929,382.426C218.329,381.344 221.584,379.871 225.534,377.933C229.563,375.997 236.304,372.2 245.832,366.699C255.365,361.198 264.506,355.311 273.262,349.19C282.017,342.99 291.545,335.242 301.928,325.869C312.232,316.493 321.142,306.886 328.657,297.046C336.096,287.129 342.372,275.819 347.407,263.112C352.444,250.328 355,237.466 355,224.527L355,50.665C354.768,46.716 353.296,43.384 350.43,40.516ZM316.804,226.154C316.804,289.067 209.883,343.302 209.883,343.302L209.883,73.368L316.804,73.368C316.804,73.368 316.804,163.242 316.804,226.154Z"/>
|
||||||
|
</vector>
|
||||||
14
src/Android/Resources/drawable/logo_rounded.xml
Normal file
14
src/Android/Resources/drawable/logo_rounded.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<group android:scaleX="0.11454546"
|
||||||
|
android:scaleY="0.11454546"
|
||||||
|
android:translateX="31.663637"
|
||||||
|
android:translateY="27.54">
|
||||||
|
<path
|
||||||
|
android:pathData="M376.4,12.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6H26.7c-5.1,0 -9.4,1.9 -13.1,5.6C9.9,15.9 8,20.2 8,25.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8V25.3C382,20.2 380.1,15.9 376.4,12.2zM333,251.8C333,333 195,403 195,403V54.6h138C333,54.6 333,170.6 333,251.8z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<style name="LaunchTheme" parent="BaseTheme">
|
<style name="LaunchTheme" parent="BaseTheme">
|
||||||
<item name="android:windowBackground">@drawable/splash_screen_dark</item>
|
<item name="android:windowBackground">@drawable/splash_screen_dark</item>
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_round</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="BaseTheme" parent="Theme.AppCompat">
|
<style name="BaseTheme" parent="Theme.AppCompat">
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<style name="LaunchTheme" parent="BaseTheme">
|
<style name="LaunchTheme" parent="BaseTheme">
|
||||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/ic_launcher_background</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_round</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="BaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="BaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
android:accessibilityFeedbackType="feedbackGeneric"
|
android:accessibilityFeedbackType="feedbackGeneric"
|
||||||
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
|
||||||
android:notificationTimeout="100"
|
android:notificationTimeout="100"
|
||||||
android:canRetrieveWindowContent="true"/>
|
android:canRetrieveWindowContent="true"
|
||||||
|
android:isAccessibilityTool="false"/>
|
||||||
@@ -17,6 +17,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="alook.browser.google"
|
android:name="alook.browser.google"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="app.vanadium.browser"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.amazon.cloud9"
|
android:name="com.amazon.cloud9"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -89,6 +92,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.kiwibrowser.browser.dev"
|
android:name="com.kiwibrowser.browser.dev"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.lemurbrowser.exts"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.microsoft.emmx"
|
android:name="com.microsoft.emmx"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -113,6 +119,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.naver.whale"
|
android:name="com.naver.whale"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.neeva.app"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.opera.browser"
|
android:name="com.opera.browser"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -137,6 +146,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.qwant.liberty"
|
android:name="com.qwant.liberty"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.rainsee.create"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.sec.android.app.sbrowser"
|
android:name="com.sec.android.app.sbrowser"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -161,6 +173,15 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.yandex.browser"
|
android:name="com.yandex.browser"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.yjllq.internet"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.yjllq.kito"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.yujian.ResideMenuDemo"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.z28j.feel"
|
android:name="com.z28j.feel"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -182,6 +203,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="mark.via.gp"
|
android:name="mark.via.gp"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="net.dezor.browser"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="net.slions.fulguris.full.download"
|
android:name="net.slions.fulguris.full.download"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
#if !FDROID
|
#if !FDROID
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
using AndroidX.Core.App;
|
using AndroidX.Core.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Droid.Receivers;
|
||||||
|
using Bit.Droid.Utilities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
using static Xamarin.Essentials.Platform;
|
||||||
|
using Intent = Android.Content.Intent;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
@@ -23,6 +34,11 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
||||||
|
|
||||||
|
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(IsRegisteredForPush);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> GetTokenAsync()
|
public async Task<string> GetTokenAsync()
|
||||||
{
|
{
|
||||||
return await _stateService.GetPushCurrentTokenAsync();
|
return await _stateService.GetPushCurrentTokenAsync();
|
||||||
@@ -47,6 +63,50 @@ namespace Bit.Droid.Services
|
|||||||
// Do we ever need to unregister?
|
// Do we ever need to unregister?
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DismissLocalNotification(string notificationId)
|
||||||
|
{
|
||||||
|
if (int.TryParse(notificationId, out int intNotificationId))
|
||||||
|
{
|
||||||
|
var notificationManager = NotificationManagerCompat.From(Android.App.Application.Context);
|
||||||
|
notificationManager.Cancel(intNotificationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendLocalNotification(string title, string message, BaseNotificationData data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(data.Id))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("notificationId cannot be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = Android.App.Application.Context;
|
||||||
|
var intent = new Intent(context, typeof(MainActivity));
|
||||||
|
intent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
|
||||||
|
var pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true);
|
||||||
|
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags);
|
||||||
|
|
||||||
|
var deleteIntent = new Intent(context, typeof(NotificationDismissReceiver));
|
||||||
|
deleteIntent.PutExtra(Bit.Core.Constants.NotificationData, JsonConvert.SerializeObject(data));
|
||||||
|
var deletePendingIntent = PendingIntent.GetBroadcast(context, 20220802, deleteIntent, pendingIntentFlags);
|
||||||
|
|
||||||
|
var builder = new NotificationCompat.Builder(context, Bit.Core.Constants.AndroidNotificationChannelId)
|
||||||
|
.SetContentIntent(pendingIntent)
|
||||||
|
.SetContentTitle(title)
|
||||||
|
.SetContentText(message)
|
||||||
|
.SetSmallIcon(Resource.Drawable.ic_notification)
|
||||||
|
.SetColor((int)Android.Graphics.Color.White)
|
||||||
|
.SetDeleteIntent(deletePendingIntent)
|
||||||
|
.SetAutoCancel(true);
|
||||||
|
|
||||||
|
if (data is PasswordlessNotificationData passwordlessNotificationData && passwordlessNotificationData.TimeoutInMinutes > 0)
|
||||||
|
{
|
||||||
|
builder.SetTimeoutAfter(passwordlessNotificationData.TimeoutInMinutes * 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationManager = NotificationManagerCompat.From(context);
|
||||||
|
notificationManager.Notify(int.Parse(data.Id), builder.Build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
215
src/Android/Services/AutofillHandler.cs
Normal file
215
src/Android/Services/AutofillHandler.cs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Android.App;
|
||||||
|
using Android.App.Assist;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Provider;
|
||||||
|
using Android.Views.Autofill;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Droid.Autofill;
|
||||||
|
using Plugin.CurrentActivity;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Services
|
||||||
|
{
|
||||||
|
public class AutofillHandler : IAutofillHandler
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IMessagingService _messagingService;
|
||||||
|
private readonly IClipboardService _clipboardService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private readonly LazyResolve<IEventService> _eventService;
|
||||||
|
|
||||||
|
public AutofillHandler(IStateService stateService,
|
||||||
|
IMessagingService messagingService,
|
||||||
|
IClipboardService clipboardService,
|
||||||
|
IPlatformUtilsService platformUtilsService,
|
||||||
|
LazyResolve<IEventService> eventService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_messagingService = messagingService;
|
||||||
|
_clipboardService = clipboardService;
|
||||||
|
_platformUtilsService = platformUtilsService;
|
||||||
|
_eventService = eventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillServiceEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var afm = (AutofillManager)activity.GetSystemService(
|
||||||
|
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||||
|
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsAutofillService()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
||||||
|
var manager = activity.GetSystemService(type) as AutofillManager;
|
||||||
|
return manager.IsAutofillSupported;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Autofill(CipherView cipher)
|
||||||
|
{
|
||||||
|
var activity = CrossCurrentActivity.Current.Activity as Xamarin.Forms.Platform.Android.FormsAppCompatActivity;
|
||||||
|
if (activity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activity.Intent?.GetBooleanExtra(AutofillConstants.AutofillFramework, false) ?? false)
|
||||||
|
{
|
||||||
|
if (cipher == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var structure = activity.Intent.GetParcelableExtra(
|
||||||
|
AutofillManager.ExtraAssistStructure) as AssistStructure;
|
||||||
|
if (structure == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var parser = new Parser(structure, activity.ApplicationContext);
|
||||||
|
parser.Parse();
|
||||||
|
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var task = CopyTotpAsync(cipher);
|
||||||
|
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher), false);
|
||||||
|
var replyIntent = new Intent();
|
||||||
|
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
||||||
|
activity.SetResult(Result.Ok, replyIntent);
|
||||||
|
activity.Finish();
|
||||||
|
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var data = new Intent();
|
||||||
|
if (cipher?.Login == null)
|
||||||
|
{
|
||||||
|
data.PutExtra("canceled", "true");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var task = CopyTotpAsync(cipher);
|
||||||
|
data.PutExtra("uri", cipher.Login.Uri);
|
||||||
|
data.PutExtra("username", cipher.Login.Username);
|
||||||
|
data.PutExtra("password", cipher.Login.Password);
|
||||||
|
}
|
||||||
|
if (activity.Parent == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Ok, data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
activity.Parent.SetResult(Result.Ok, data);
|
||||||
|
}
|
||||||
|
activity.Finish();
|
||||||
|
_messagingService.Send("finishMainActivity");
|
||||||
|
if (cipher != null)
|
||||||
|
{
|
||||||
|
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseAutofill()
|
||||||
|
{
|
||||||
|
Autofill(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillAccessibilityServiceRunning()
|
||||||
|
{
|
||||||
|
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
|
||||||
|
Settings.Secure.EnabledAccessibilityServices);
|
||||||
|
return Application.Context.PackageName != null &&
|
||||||
|
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillAccessibilityOverlayPermitted()
|
||||||
|
{
|
||||||
|
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void DisableAutofillService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
||||||
|
var manager = activity.GetSystemService(type) as AutofillManager;
|
||||||
|
manager.DisableAutofillServices();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillServicesEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||||
|
{
|
||||||
|
// Android 5-6: Both accessibility & overlay are required or nothing happens
|
||||||
|
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
|
||||||
|
{
|
||||||
|
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
|
||||||
|
return AutofillAccessibilityServiceRunning();
|
||||||
|
}
|
||||||
|
// Android 8+: Either autofill or accessibility is required
|
||||||
|
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyTotpAsync(CipherView cipher)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
||||||
|
{
|
||||||
|
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
||||||
|
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||||
|
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||||
|
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
||||||
|
if (totp != null)
|
||||||
|
{
|
||||||
|
await _clipboardService.CopyTextAsync(totp);
|
||||||
|
_platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using Android.Content;
|
|||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Droid.Receivers;
|
using Bit.Droid.Receivers;
|
||||||
using Plugin.CurrentActivity;
|
using Bit.Droid.Utilities;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
@@ -20,10 +20,10 @@ namespace Bit.Droid.Services
|
|||||||
_stateService = stateService;
|
_stateService = stateService;
|
||||||
|
|
||||||
_clearClipboardPendingIntent = new Lazy<PendingIntent>(() =>
|
_clearClipboardPendingIntent = new Lazy<PendingIntent>(() =>
|
||||||
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
|
PendingIntent.GetBroadcast(Application.Context,
|
||||||
0,
|
0,
|
||||||
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
|
new Intent(Application.Context, typeof(ClearClipboardAlarmReceiver)),
|
||||||
PendingIntentFlags.UpdateCurrent));
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||||
@@ -44,7 +44,7 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
|
catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to"))
|
||||||
{
|
{
|
||||||
// #1962 Just ignore, the content is copied either way but there is some app interfiering in the process
|
// #1962 Just ignore, the content is copied either way but there is some app interfering in the process
|
||||||
// that the OS catches and just throws this exception.
|
// that the OS catches and just throws this exception.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,9 +57,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
private void CopyToClipboard(string text, bool isSensitive = true)
|
private void CopyToClipboard(string text, bool isSensitive = true)
|
||||||
{
|
{
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
var clipboardManager = Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;
|
||||||
var clipboardManager = activity.GetSystemService(
|
|
||||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
|
||||||
var clipData = ClipData.NewPlainText("bitwarden", text);
|
var clipData = ClipData.NewPlainText("bitwarden", text);
|
||||||
if (isSensitive)
|
if (isSensitive)
|
||||||
{
|
{
|
||||||
@@ -86,7 +84,7 @@ namespace Bit.Droid.Services
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs;
|
var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + clearMs;
|
||||||
var alarmManager = CrossCurrentActivity.Current.Activity.GetSystemService(Context.AlarmService) as AlarmManager;
|
var alarmManager = Application.Context.GetSystemService(Context.AlarmService) as AlarmManager;
|
||||||
alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent.Value);
|
alarmManager.Set(AlarmType.Rtc, triggerMs, _clearClipboardPendingIntent.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using Org.BouncyCastle.Crypto.Digests;
|
|||||||
using Org.BouncyCastle.Crypto.Generators;
|
using Org.BouncyCastle.Crypto.Generators;
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Java.Lang;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
@@ -33,5 +35,19 @@ namespace Bit.Droid.Services
|
|||||||
generator.Init(password, salt, iterations);
|
generator.Init(password, salt, iterations);
|
||||||
return ((KeyParameter)generator.GenerateDerivedMacParameters(keySize)).GetKey();
|
return ((KeyParameter)generator.GenerateDerivedMacParameters(keySize)).GetKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] Argon2id(byte[] password, byte[] salt, int iterations, int memory, int parallelism)
|
||||||
|
{
|
||||||
|
JavaSystem.LoadLibrary("argon2");
|
||||||
|
int keySize = 32;
|
||||||
|
var key = new byte[keySize];
|
||||||
|
argon2id_hash_raw(iterations, memory, parallelism,
|
||||||
|
password, password.Length, salt, salt.Length, key, key.Length);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("argon2", EntryPoint = "argon2id_hash_raw")]
|
||||||
|
private static extern int argon2id_hash_raw(int timeCost, int memoryCost, int parallelism,
|
||||||
|
byte[] pwd, int pwdlen, byte[] salt, int saltlen, byte[] hash, int hashlen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android;
|
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.App.Assist;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Nfc;
|
using Android.Nfc;
|
||||||
@@ -14,59 +9,35 @@ using Android.Provider;
|
|||||||
using Android.Text;
|
using Android.Text;
|
||||||
using Android.Text.Method;
|
using Android.Text.Method;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Views.Autofill;
|
|
||||||
using Android.Views.InputMethods;
|
using Android.Views.InputMethods;
|
||||||
using Android.Webkit;
|
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
using AndroidX.Core.App;
|
|
||||||
using AndroidX.Core.Content;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Droid.Autofill;
|
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
|
using static Bit.App.Pages.SettingsPageViewModel;
|
||||||
|
|
||||||
namespace Bit.Droid.Services
|
namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
public class DeviceActionService : IDeviceActionService
|
public class DeviceActionService : IDeviceActionService
|
||||||
{
|
{
|
||||||
private readonly IClipboardService _clipboardService;
|
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
|
||||||
private readonly Func<IEventService> _eventServiceFunc;
|
|
||||||
private AlertDialog _progressDialog;
|
private AlertDialog _progressDialog;
|
||||||
object _progressDialogLock = new object();
|
object _progressDialogLock = new object();
|
||||||
|
|
||||||
private bool _cameraPermissionsDenied;
|
|
||||||
private Toast _toast;
|
private Toast _toast;
|
||||||
private string _userAgent;
|
private string _userAgent;
|
||||||
|
|
||||||
public DeviceActionService(
|
public DeviceActionService(
|
||||||
IClipboardService clipboardService,
|
|
||||||
IStateService stateService,
|
IStateService stateService,
|
||||||
IMessagingService messagingService,
|
IMessagingService messagingService)
|
||||||
IBroadcasterService broadcasterService,
|
|
||||||
Func<IEventService> eventServiceFunc)
|
|
||||||
{
|
{
|
||||||
_clipboardService = clipboardService;
|
|
||||||
_stateService = stateService;
|
_stateService = stateService;
|
||||||
_messagingService = messagingService;
|
_messagingService = messagingService;
|
||||||
_broadcasterService = broadcasterService;
|
|
||||||
_eventServiceFunc = eventServiceFunc;
|
|
||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
|
|
||||||
{
|
|
||||||
if (message.Command == "selectFileCameraPermissionDenied")
|
|
||||||
{
|
|
||||||
_cameraPermissionsDenied = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DeviceUserAgent
|
public string DeviceUserAgent
|
||||||
@@ -99,14 +70,17 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
public bool LaunchApp(string appName)
|
public bool LaunchApp(string appName)
|
||||||
{
|
{
|
||||||
|
if ((int)Build.VERSION.SdkInt < 33)
|
||||||
|
{
|
||||||
|
// API 33 required to avoid using wildcard app visibility or dangerous permissions
|
||||||
|
// https://developer.android.com/reference/android/content/pm/PackageManager#getLaunchIntentSenderForPackage(java.lang.String)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var activity = CrossCurrentActivity.Current.Activity;
|
var activity = CrossCurrentActivity.Current.Activity;
|
||||||
appName = appName.Replace("androidapp://", string.Empty);
|
appName = appName.Replace("androidapp://", string.Empty);
|
||||||
var launchIntent = activity.PackageManager.GetLaunchIntentForPackage(appName);
|
var launchIntentSender = activity?.PackageManager?.GetLaunchIntentSenderForPackage(appName);
|
||||||
if (launchIntent != null)
|
launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
|
||||||
{
|
return launchIntentSender != null;
|
||||||
activity.StartActivity(launchIntent);
|
|
||||||
}
|
|
||||||
return launchIntent != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShowLoadingAsync(string text)
|
public async Task ShowLoadingAsync(string text)
|
||||||
@@ -212,184 +186,6 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var intent = BuildOpenFileIntent(fileData, fileName);
|
|
||||||
if (intent == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
activity.StartActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanOpenFile(string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
|
||||||
if (intent == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
|
||||||
PackageInfoFlags.MatchDefaultOnly);
|
|
||||||
return (activities?.Count ?? 0) > 0;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
|
||||||
{
|
|
||||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
|
||||||
if (extension == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var cachePath = activity.CacheDir;
|
|
||||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
|
||||||
File.WriteAllBytes(filePath, fileData);
|
|
||||||
var file = new Java.IO.File(cachePath, fileName);
|
|
||||||
if (!file.IsFile)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var intent = new Intent(Intent.ActionView);
|
|
||||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
|
||||||
"com.x8bit.bitwarden.fileprovider", file);
|
|
||||||
intent.SetDataAndType(uri, mimeType);
|
|
||||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
|
|
||||||
if (contentUri != null)
|
|
||||||
{
|
|
||||||
var uri = Android.Net.Uri.Parse(contentUri);
|
|
||||||
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
|
||||||
// Using java bufferedOutputStream due to this issue:
|
|
||||||
// https://github.com/xamarin/xamarin-android/issues/3498
|
|
||||||
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
|
||||||
javaStream.Write(fileData);
|
|
||||||
javaStream.Flush();
|
|
||||||
javaStream.Close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for location to save file
|
|
||||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
|
||||||
if (extension == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
// Unable to identify so fall back to generic "any" type
|
|
||||||
mimeType = "*/*";
|
|
||||||
}
|
|
||||||
|
|
||||||
var intent = new Intent(Intent.ActionCreateDocument);
|
|
||||||
intent.SetType(mimeType);
|
|
||||||
intent.AddCategory(Intent.CategoryOpenable);
|
|
||||||
intent.PutExtra(Intent.ExtraTitle, fileName);
|
|
||||||
|
|
||||||
activity.StartActivityForResult(intent, Constants.SaveFileRequestCode);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearCacheAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
|
||||||
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
|
||||||
}
|
|
||||||
catch (Exception) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SelectFileAsync()
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
|
||||||
HasPermission(Manifest.Permission.WriteExternalStorage);
|
|
||||||
var additionalIntents = new List<IParcelable>();
|
|
||||||
if (activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
|
||||||
{
|
|
||||||
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
|
|
||||||
if (!_cameraPermissionsDenied && !hasStorageWritePermission)
|
|
||||||
{
|
|
||||||
AskPermission(Manifest.Permission.WriteExternalStorage);
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
if (!_cameraPermissionsDenied && !hasCameraPermission)
|
|
||||||
{
|
|
||||||
AskPermission(Manifest.Permission.Camera);
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
if (!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
|
||||||
if (!file.Exists())
|
|
||||||
{
|
|
||||||
file.ParentFile.Mkdirs();
|
|
||||||
file.CreateNewFile();
|
|
||||||
}
|
|
||||||
var outputFileUri = FileProvider.GetUriForFile(activity,
|
|
||||||
"com.x8bit.bitwarden.fileprovider", file);
|
|
||||||
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
|
||||||
}
|
|
||||||
catch (Java.IO.IOException) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var docIntent = new Intent(Intent.ActionOpenDocument);
|
|
||||||
docIntent.AddCategory(Intent.CategoryOpenable);
|
|
||||||
docIntent.SetType("*/*");
|
|
||||||
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
|
|
||||||
if (additionalIntents.Count > 0)
|
|
||||||
{
|
|
||||||
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
|
|
||||||
}
|
|
||||||
activity.StartActivityForResult(chooserIntent, Constants.SelectFileRequestCode);
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<string> DisplayPromptAync(string title = null, string description = null,
|
public Task<string> DisplayPromptAync(string title = null, string description = null,
|
||||||
string text = null, string okButtonText = null, string cancelButtonText = null,
|
string text = null, string okButtonText = null, string cancelButtonText = null,
|
||||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||||
@@ -467,34 +263,6 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisableAutofillService()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
|
||||||
var manager = activity.GetSystemService(type) as AutofillManager;
|
|
||||||
manager.DisableAutofillServices();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillServicesEnabled()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
|
||||||
{
|
|
||||||
// Android 5-6: Both accessibility & overlay are required or nothing happens
|
|
||||||
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
|
|
||||||
{
|
|
||||||
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
|
|
||||||
return AutofillAccessibilityServiceRunning();
|
|
||||||
}
|
|
||||||
// Android 8+: Either autofill or accessibility is required
|
|
||||||
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetBuildNumber()
|
public string GetBuildNumber()
|
||||||
{
|
{
|
||||||
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
|
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
|
||||||
@@ -526,25 +294,6 @@ namespace Bit.Droid.Services
|
|||||||
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsAutofillService()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
|
||||||
var manager = activity.GetSystemService(type) as AutofillManager;
|
|
||||||
return manager.IsAutofillSupported;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SystemMajorVersion()
|
public int SystemMajorVersion()
|
||||||
{
|
{
|
||||||
return (int)Build.VERSION.SdkInt;
|
return (int)Build.VERSION.SdkInt;
|
||||||
@@ -635,112 +384,6 @@ namespace Bit.Droid.Services
|
|||||||
title, cancel, destruction, buttons);
|
title, cancel, destruction, buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Autofill(CipherView cipher)
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
if (activity == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
|
||||||
{
|
|
||||||
if (cipher == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var structure = activity.Intent.GetParcelableExtra(
|
|
||||||
AutofillManager.ExtraAssistStructure) as AssistStructure;
|
|
||||||
if (structure == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var parser = new Parser(structure, activity.ApplicationContext);
|
|
||||||
parser.Parse();
|
|
||||||
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var task = CopyTotpAsync(cipher);
|
|
||||||
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
|
|
||||||
var replyIntent = new Intent();
|
|
||||||
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
|
||||||
activity.SetResult(Result.Ok, replyIntent);
|
|
||||||
activity.Finish();
|
|
||||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var data = new Intent();
|
|
||||||
if (cipher?.Login == null)
|
|
||||||
{
|
|
||||||
data.PutExtra("canceled", "true");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var task = CopyTotpAsync(cipher);
|
|
||||||
data.PutExtra("uri", cipher.Login.Uri);
|
|
||||||
data.PutExtra("username", cipher.Login.Username);
|
|
||||||
data.PutExtra("password", cipher.Login.Password);
|
|
||||||
}
|
|
||||||
if (activity.Parent == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Ok, data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.Parent.SetResult(Result.Ok, data);
|
|
||||||
}
|
|
||||||
activity.Finish();
|
|
||||||
_messagingService.Send("finishMainActivity");
|
|
||||||
if (cipher != null)
|
|
||||||
{
|
|
||||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseAutofill()
|
|
||||||
{
|
|
||||||
Autofill(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Background()
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.MoveTaskToBack(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityServiceRunning()
|
|
||||||
{
|
|
||||||
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
|
|
||||||
Settings.Secure.EnabledAccessibilityServices);
|
|
||||||
return Application.Context.PackageName != null &&
|
|
||||||
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityOverlayPermitted()
|
|
||||||
{
|
|
||||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasAutofillService()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenAccessibilityOverlayPermissionSettings()
|
public void OpenAccessibilityOverlayPermissionSettings()
|
||||||
{
|
{
|
||||||
@@ -771,25 +414,6 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AutofillServiceEnabled()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var afm = (AutofillManager)activity.GetSystemService(
|
|
||||||
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
|
||||||
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenAccessibilitySettings()
|
public void OpenAccessibilitySettings()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -848,61 +472,6 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DeleteDir(Java.IO.File dir)
|
|
||||||
{
|
|
||||||
if (dir != null && dir.IsDirectory)
|
|
||||||
{
|
|
||||||
var children = dir.List();
|
|
||||||
for (int i = 0; i < children.Length; i++)
|
|
||||||
{
|
|
||||||
var success = DeleteDir(new Java.IO.File(dir, children[i]));
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dir.Delete();
|
|
||||||
}
|
|
||||||
else if (dir != null && dir.IsFile)
|
|
||||||
{
|
|
||||||
return dir.Delete();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HasPermission(string permission)
|
|
||||||
{
|
|
||||||
return ContextCompat.CheckSelfPermission(
|
|
||||||
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AskPermission(string permission)
|
|
||||||
{
|
|
||||||
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
|
|
||||||
Constants.SelectFilePermissionRequestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
|
|
||||||
{
|
|
||||||
var intents = new List<IParcelable>();
|
|
||||||
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
|
|
||||||
var captureIntent = new Intent(MediaStore.ActionImageCapture);
|
|
||||||
var listCam = pm.QueryIntentActivities(captureIntent, 0);
|
|
||||||
foreach (var res in listCam)
|
|
||||||
{
|
|
||||||
var packageName = res.ActivityInfo.PackageName;
|
|
||||||
var intent = new Intent(captureIntent);
|
|
||||||
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
|
|
||||||
intent.SetPackage(packageName);
|
|
||||||
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
|
||||||
intents.Add(intent);
|
|
||||||
}
|
|
||||||
return intents;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent RateIntentForUrl(string url, Activity activity)
|
private Intent RateIntentForUrl(string url, Activity activity)
|
||||||
{
|
{
|
||||||
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
||||||
@@ -920,24 +489,6 @@ namespace Bit.Droid.Services
|
|||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyTotpAsync(CipherView cipher)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
|
||||||
{
|
|
||||||
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
|
||||||
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
|
||||||
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
|
||||||
{
|
|
||||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
|
||||||
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
|
||||||
if (totp != null)
|
|
||||||
{
|
|
||||||
await _clipboardService.CopyTextAsync(totp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public float GetSystemFontSizeScale()
|
public float GetSystemFontSizeScale()
|
||||||
{
|
{
|
||||||
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
||||||
@@ -964,5 +515,20 @@ namespace Bit.Droid.Services
|
|||||||
}
|
}
|
||||||
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenAppSettings()
|
||||||
|
{
|
||||||
|
var intent = new Intent(Android.Provider.Settings.ActionApplicationDetailsSettings);
|
||||||
|
intent.AddFlags(ActivityFlags.NewTask);
|
||||||
|
var uri = Android.Net.Uri.FromParts("package", Application.Context.PackageName, null);
|
||||||
|
intent.SetData(uri);
|
||||||
|
Application.Context.StartActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseExtensionPopUp()
|
||||||
|
{
|
||||||
|
// only used by iOS
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
278
src/Android/Services/FileService.cs
Normal file
278
src/Android/Services/FileService.cs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Android;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Content.PM;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Provider;
|
||||||
|
using Android.Webkit;
|
||||||
|
using AndroidX.Core.App;
|
||||||
|
using AndroidX.Core.Content;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Plugin.CurrentActivity;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Services
|
||||||
|
{
|
||||||
|
public class FileService : IFileService
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
|
|
||||||
|
private bool _cameraPermissionsDenied;
|
||||||
|
|
||||||
|
public FileService(IStateService stateService, IBroadcasterService broadcasterService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_broadcasterService = broadcasterService;
|
||||||
|
|
||||||
|
_broadcasterService.Subscribe(nameof(FileService), (message) =>
|
||||||
|
{
|
||||||
|
if (message.Command == "selectFileCameraPermissionDenied")
|
||||||
|
{
|
||||||
|
_cameraPermissionsDenied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var intent = BuildOpenFileIntent(fileData, fileName);
|
||||||
|
if (intent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
activity.StartActivity(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanOpenFile(string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
||||||
|
if (intent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
||||||
|
PackageInfoFlags.MatchDefaultOnly);
|
||||||
|
return (activities?.Count ?? 0) > 0;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
||||||
|
{
|
||||||
|
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||||
|
if (mimeType == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var cachePath = activity.CacheDir;
|
||||||
|
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||||
|
File.WriteAllBytes(filePath, fileData);
|
||||||
|
var file = new Java.IO.File(cachePath, fileName);
|
||||||
|
if (!file.IsFile)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var intent = new Intent(Intent.ActionView);
|
||||||
|
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||||
|
"com.x8bit.bitwarden.fileprovider", file);
|
||||||
|
intent.SetDataAndType(uri, mimeType);
|
||||||
|
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
|
||||||
|
if (contentUri != null)
|
||||||
|
{
|
||||||
|
var uri = Android.Net.Uri.Parse(contentUri);
|
||||||
|
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
||||||
|
// Using java bufferedOutputStream due to this issue:
|
||||||
|
// https://github.com/xamarin/xamarin-android/issues/3498
|
||||||
|
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
||||||
|
javaStream.Write(fileData);
|
||||||
|
javaStream.Flush();
|
||||||
|
javaStream.Close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for location to save file
|
||||||
|
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||||
|
if (mimeType == null)
|
||||||
|
{
|
||||||
|
// Unable to identify so fall back to generic "any" type
|
||||||
|
mimeType = "*/*";
|
||||||
|
}
|
||||||
|
|
||||||
|
var intent = new Intent(Intent.ActionCreateDocument);
|
||||||
|
intent.SetType(mimeType);
|
||||||
|
intent.AddCategory(Intent.CategoryOpenable);
|
||||||
|
intent.PutExtra(Intent.ExtraTitle, fileName);
|
||||||
|
|
||||||
|
activity.StartActivityForResult(intent, Core.Constants.SaveFileRequestCode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearCacheAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
||||||
|
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SelectFileAsync()
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
||||||
|
HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||||
|
var additionalIntents = new List<IParcelable>();
|
||||||
|
if (activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
||||||
|
{
|
||||||
|
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
|
||||||
|
if (!_cameraPermissionsDenied && !hasStorageWritePermission)
|
||||||
|
{
|
||||||
|
AskPermission(Manifest.Permission.WriteExternalStorage);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
if (!_cameraPermissionsDenied && !hasCameraPermission)
|
||||||
|
{
|
||||||
|
AskPermission(Manifest.Permission.Camera);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
if (!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
||||||
|
if (!file.Exists())
|
||||||
|
{
|
||||||
|
file.ParentFile.Mkdirs();
|
||||||
|
file.CreateNewFile();
|
||||||
|
}
|
||||||
|
var outputFileUri = FileProvider.GetUriForFile(activity,
|
||||||
|
"com.x8bit.bitwarden.fileprovider", file);
|
||||||
|
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
|
||||||
|
}
|
||||||
|
catch (Java.IO.IOException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var docIntent = new Intent(Intent.ActionOpenDocument);
|
||||||
|
docIntent.AddCategory(Intent.CategoryOpenable);
|
||||||
|
docIntent.SetType("*/*");
|
||||||
|
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
|
||||||
|
if (additionalIntents.Count > 0)
|
||||||
|
{
|
||||||
|
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
|
||||||
|
}
|
||||||
|
activity.StartActivityForResult(chooserIntent, Core.Constants.SelectFileRequestCode);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DeleteDir(Java.IO.File dir)
|
||||||
|
{
|
||||||
|
if (dir is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir.IsDirectory)
|
||||||
|
{
|
||||||
|
var children = dir.List();
|
||||||
|
for (int i = 0; i < children.Length; i++)
|
||||||
|
{
|
||||||
|
var success = DeleteDir(new Java.IO.File(dir, children[i]));
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir.IsFile)
|
||||||
|
{
|
||||||
|
return dir.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasPermission(string permission)
|
||||||
|
{
|
||||||
|
return ContextCompat.CheckSelfPermission(
|
||||||
|
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AskPermission(string permission)
|
||||||
|
{
|
||||||
|
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
|
||||||
|
Core.Constants.SelectFilePermissionRequestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
|
||||||
|
{
|
||||||
|
var intents = new List<IParcelable>();
|
||||||
|
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
|
||||||
|
var captureIntent = new Intent(MediaStore.ActionImageCapture);
|
||||||
|
var listCam = pm.QueryIntentActivities(captureIntent, 0);
|
||||||
|
foreach (var res in listCam)
|
||||||
|
{
|
||||||
|
var packageName = res.ActivityInfo.PackageName;
|
||||||
|
var intent = new Intent(captureIntent);
|
||||||
|
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
|
||||||
|
intent.SetPackage(packageName);
|
||||||
|
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
|
||||||
|
intents.Add(intent);
|
||||||
|
}
|
||||||
|
return intents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
@@ -25,13 +26,13 @@ namespace Bit.Droid.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage));
|
var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage));
|
||||||
Console.WriteLine(netLanguage + " failed, trying " + fallback + " (" + e1.Message + ")");
|
Debug.WriteLine(netLanguage + " failed, trying " + fallback + " (" + e1.Message + ")");
|
||||||
ci = new CultureInfo(fallback);
|
ci = new CultureInfo(fallback);
|
||||||
}
|
}
|
||||||
catch (CultureNotFoundException e2)
|
catch (CultureNotFoundException e2)
|
||||||
{
|
{
|
||||||
// iOS language not valid .NET culture, falling back to English
|
// iOS language not valid .NET culture, falling back to English
|
||||||
Console.WriteLine(netLanguage + " couldn't be set, using 'en' (" + e2.Message + ")");
|
Debug.WriteLine(netLanguage + " couldn't be set, using 'en' (" + e2.Message + ")");
|
||||||
ci = new CultureInfo("en");
|
ci = new CultureInfo("en");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,7 @@ namespace Bit.Droid.Services
|
|||||||
|
|
||||||
private string AndroidToDotnetLanguage(string androidLanguage)
|
private string AndroidToDotnetLanguage(string androidLanguage)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Android Language:" + androidLanguage);
|
Debug.WriteLine("Android Language:" + androidLanguage);
|
||||||
var netLanguage = androidLanguage;
|
var netLanguage = androidLanguage;
|
||||||
if (androidLanguage.StartsWith("zh"))
|
if (androidLanguage.StartsWith("zh"))
|
||||||
{
|
{
|
||||||
@@ -79,13 +80,13 @@ namespace Bit.Droid.Services
|
|||||||
// ONLY use cultures that have been tested and known to work
|
// ONLY use cultures that have been tested and known to work
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Console.WriteLine(".NET Language/Locale:" + netLanguage);
|
Debug.WriteLine(".NET Language/Locale:" + netLanguage);
|
||||||
return netLanguage;
|
return netLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ToDotnetFallbackLanguage(PlatformCulture platCulture)
|
private string ToDotnetFallbackLanguage(PlatformCulture platCulture)
|
||||||
{
|
{
|
||||||
Console.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode);
|
Debug.WriteLine(".NET Fallback Language:" + platCulture.LanguageCode);
|
||||||
var netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually);
|
var netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually);
|
||||||
switch (platCulture.LanguageCode)
|
switch (platCulture.LanguageCode)
|
||||||
{
|
{
|
||||||
@@ -95,7 +96,7 @@ namespace Bit.Droid.Services
|
|||||||
// add more application-specific cases here (if required)
|
// add more application-specific cases here (if required)
|
||||||
// ONLY use cultures that have been tested and known to work
|
// ONLY use cultures that have been tested and known to work
|
||||||
}
|
}
|
||||||
Console.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)");
|
Debug.WriteLine(".NET Fallback Language/Locale:" + netLanguage + " (application-specific)");
|
||||||
return netLanguage;
|
return netLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/Android/Services/WatchDeviceService.cs
Normal file
29
src/Android/Services/WatchDeviceService.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Services;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Services
|
||||||
|
{
|
||||||
|
public class WatchDeviceService : BaseWatchDeviceService
|
||||||
|
{
|
||||||
|
public WatchDeviceService(ICipherService cipherService,
|
||||||
|
IEnvironmentService environmentService,
|
||||||
|
IStateService stateService,
|
||||||
|
IVaultTimeoutService vaultTimeoutService)
|
||||||
|
: base(cipherService, environmentService, stateService, vaultTimeoutService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsSupported => false;
|
||||||
|
|
||||||
|
public override bool IsConnected => false;
|
||||||
|
|
||||||
|
protected override bool CanSendData => false;
|
||||||
|
|
||||||
|
protected override Task SendDataToWatchAsync(WatchDTO watchDto) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
protected override void ConnectToWatch() => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ using Java.Lang;
|
|||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
|
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
|
||||||
Icon = "@drawable/shield")]
|
Icon = "@drawable/shield", Exported = true)]
|
||||||
[IntentFilter(new string[] { ActionQsTile })]
|
[IntentFilter(new string[] { ActionQsTile })]
|
||||||
[Register("com.x8bit.bitwarden.AutofillTileService")]
|
[Register("com.x8bit.bitwarden.AutofillTileService")]
|
||||||
public class AutofillTileService : TileService
|
public class AutofillTileService : TileService
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using Java.Lang;
|
|||||||
|
|
||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/PasswordGenerator",
|
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Exported = true, Label = "@string/PasswordGenerator",
|
||||||
Icon = "@drawable/generate")]
|
Icon = "@drawable/generate")]
|
||||||
[IntentFilter(new string[] { ActionQsTile })]
|
[IntentFilter(new string[] { ActionQsTile })]
|
||||||
[Register("com.x8bit.bitwarden.GeneratorTileService")]
|
[Register("com.x8bit.bitwarden.GeneratorTileService")]
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ using Java.Lang;
|
|||||||
namespace Bit.Droid.Tile
|
namespace Bit.Droid.Tile
|
||||||
{
|
{
|
||||||
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
|
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
|
||||||
Icon = "@drawable/shield")]
|
Icon = "@drawable/shield",
|
||||||
|
Exported = true)]
|
||||||
[IntentFilter(new string[] { ActionQsTile })]
|
[IntentFilter(new string[] { ActionQsTile })]
|
||||||
[Register("com.x8bit.bitwarden.MyVaultTileService")]
|
[Register("com.x8bit.bitwarden.MyVaultTileService")]
|
||||||
public class MyVaultTileService : TileService
|
public class MyVaultTileService : TileService
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
|
||||||
@@ -47,5 +49,22 @@ namespace Bit.Droid.Utilities
|
|||||||
await AppHelpers.SetPreconfiguredSettingsAsync(dict);
|
await AppHelpers.SetPreconfiguredSettingsAsync(dict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PendingIntentFlags AddPendingIntentMutabilityFlag(PendingIntentFlags pendingIntentFlags, bool isMutable)
|
||||||
|
{
|
||||||
|
//Mutable flag was added on API level 31
|
||||||
|
if (isMutable && Build.VERSION.SdkInt >= BuildVersionCodes.S)
|
||||||
|
{
|
||||||
|
return pendingIntentFlags | PendingIntentFlags.Mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Immutable flag was added on API level 23
|
||||||
|
if (!isMutable && Build.VERSION.SdkInt >= BuildVersionCodes.M)
|
||||||
|
{
|
||||||
|
return pendingIntentFlags | PendingIntentFlags.Immutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingIntentFlags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ namespace Bit.Droid.Utilities
|
|||||||
theme = ThemeManager.Dark;
|
theme = ThemeManager.Dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
|
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord || theme == ThemeManager.SolarizedDark)
|
||||||
{
|
{
|
||||||
LightTheme = false;
|
LightTheme = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
[Activity(
|
[Activity(
|
||||||
NoHistory = true,
|
NoHistory = true,
|
||||||
LaunchMode = LaunchMode.SingleTop)]
|
LaunchMode = LaunchMode.SingleTop,
|
||||||
|
Exported = true)]
|
||||||
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
||||||
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
||||||
DataScheme = "bitwarden")]
|
DataScheme = "bitwarden")]
|
||||||
|
|||||||
BIN
src/Android/lib/arm64-v8a/libargon2.so
Executable file
BIN
src/Android/lib/arm64-v8a/libargon2.so
Executable file
Binary file not shown.
BIN
src/Android/lib/armeabi-v7a/libargon2.so
Executable file
BIN
src/Android/lib/armeabi-v7a/libargon2.so
Executable file
Binary file not shown.
BIN
src/Android/lib/x86/libargon2.so
Executable file
BIN
src/Android/lib/x86/libargon2.so
Executable file
Binary file not shown.
BIN
src/Android/lib/x86_64/libargon2.so
Executable file
BIN
src/Android/lib/x86_64/libargon2.so
Executable file
Binary file not shown.
@@ -9,5 +9,6 @@ namespace Bit.App.Abstractions
|
|||||||
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||||
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||||
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||||
|
Task PromptToSwitchToExistingAccountAsync(string userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
@@ -8,46 +8,36 @@ namespace Bit.App.Abstractions
|
|||||||
{
|
{
|
||||||
string DeviceUserAgent { get; }
|
string DeviceUserAgent { get; }
|
||||||
DeviceType DeviceType { get; }
|
DeviceType DeviceType { get; }
|
||||||
|
int SystemMajorVersion();
|
||||||
|
string SystemModel();
|
||||||
|
string GetBuildNumber();
|
||||||
|
|
||||||
void Toast(string text, bool longDuration = false);
|
void Toast(string text, bool longDuration = false);
|
||||||
bool LaunchApp(string appName);
|
|
||||||
Task ShowLoadingAsync(string text);
|
Task ShowLoadingAsync(string text);
|
||||||
Task HideLoadingAsync();
|
Task HideLoadingAsync();
|
||||||
bool OpenFile(byte[] fileData, string id, string fileName);
|
|
||||||
bool SaveFile(byte[] fileData, string id, string fileName, string contentUri);
|
|
||||||
bool CanOpenFile(string fileName);
|
|
||||||
Task ClearCacheAsync();
|
|
||||||
Task SelectFileAsync();
|
|
||||||
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
||||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||||
bool autofocus = true, bool password = false);
|
bool autofocus = true, bool password = false);
|
||||||
void RateApp();
|
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
||||||
|
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
||||||
|
|
||||||
bool SupportsFaceBiometric();
|
bool SupportsFaceBiometric();
|
||||||
Task<bool> SupportsFaceBiometricAsync();
|
Task<bool> SupportsFaceBiometricAsync();
|
||||||
bool SupportsNfc();
|
bool SupportsNfc();
|
||||||
bool SupportsCamera();
|
bool SupportsCamera();
|
||||||
bool SupportsAutofillService();
|
bool SupportsFido2();
|
||||||
int SystemMajorVersion();
|
|
||||||
string SystemModel();
|
bool LaunchApp(string appName);
|
||||||
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
void RateApp();
|
||||||
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
|
||||||
void Autofill(CipherView cipher);
|
|
||||||
void CloseAutofill();
|
|
||||||
void Background();
|
|
||||||
bool AutofillAccessibilityServiceRunning();
|
|
||||||
bool AutofillAccessibilityOverlayPermitted();
|
|
||||||
bool HasAutofillService();
|
|
||||||
bool AutofillServiceEnabled();
|
|
||||||
void DisableAutofillService();
|
|
||||||
bool AutofillServicesEnabled();
|
|
||||||
string GetBuildNumber();
|
|
||||||
void OpenAccessibilitySettings();
|
void OpenAccessibilitySettings();
|
||||||
void OpenAccessibilityOverlayPermissionSettings();
|
void OpenAccessibilityOverlayPermissionSettings();
|
||||||
void OpenAutofillSettings();
|
void OpenAutofillSettings();
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
void CloseMainApp();
|
void CloseMainApp();
|
||||||
bool SupportsFido2();
|
|
||||||
float GetSystemFontSizeScale();
|
float GetSystemFontSizeScale();
|
||||||
Task OnAccountSwitchCompleteAsync();
|
Task OnAccountSwitchCompleteAsync();
|
||||||
Task SetScreenCaptureAllowedAsync();
|
Task SetScreenCaptureAllowedAsync();
|
||||||
|
void OpenAppSettings();
|
||||||
|
void CloseExtensionPopUp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Models;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
@@ -9,6 +10,8 @@ namespace Bit.App.Abstractions
|
|||||||
Task OnRegisteredAsync(string token, string device);
|
Task OnRegisteredAsync(string token, string device);
|
||||||
void OnUnregistered(string device);
|
void OnUnregistered(string device);
|
||||||
void OnError(string message, string device);
|
void OnError(string message, string device);
|
||||||
|
Task OnNotificationTapped(BaseNotificationData data);
|
||||||
|
Task OnNotificationDismissed(BaseNotificationData data);
|
||||||
bool ShouldShowNotification();
|
bool ShouldShowNotification();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Models;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
public interface IPushNotificationService
|
public interface IPushNotificationService
|
||||||
{
|
{
|
||||||
bool IsRegisteredForPush { get; }
|
bool IsRegisteredForPush { get; }
|
||||||
|
Task<bool> AreNotificationsSettingsEnabledAsync();
|
||||||
Task<string> GetTokenAsync();
|
Task<string> GetTokenAsync();
|
||||||
Task RegisterAsync();
|
Task RegisterAsync();
|
||||||
Task UnregisterAsync();
|
Task UnregisterAsync();
|
||||||
|
void SendLocalNotification(string title, string message, BaseNotificationData data);
|
||||||
|
void DismissLocalNotification(string notificationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<RootNamespace>Bit.App</RootNamespace>
|
<RootNamespace>Bit.App</RootNamespace>
|
||||||
<AssemblyName>BitwardenApp</AssemblyName>
|
<AssemblyName>BitwardenApp</AssemblyName>
|
||||||
<Configurations>Debug;Release;FDroid</Configurations>
|
<Configurations>Debug;Release;FDroid</Configurations>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
|
||||||
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
|
||||||
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
|
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
|
||||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
|
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
|
||||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
|
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
|
||||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -122,14 +122,28 @@
|
|||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
||||||
|
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
|
||||||
|
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Pages\Accounts\LoginPasswordlessRequestPage.xaml.cs">
|
||||||
|
<DependentUpon>LoginPasswordlessRequestPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="Behaviors\" />
|
<Folder Include="Behaviors\" />
|
||||||
|
<Folder Include="Lists\" />
|
||||||
|
<Folder Include="Lists\ItemLayouts\" />
|
||||||
|
<Folder Include="Lists\DataTemplateSelectors\" />
|
||||||
|
<Folder Include="Lists\ItemLayouts\CustomFields\" />
|
||||||
|
<Folder Include="Lists\ItemViewModels\" />
|
||||||
|
<Folder Include="Lists\ItemViewModels\CustomFields\" />
|
||||||
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
<Folder Include="Controls\AccountSwitchingOverlay\" />
|
||||||
<Folder Include="Utilities\AccountManagement\" />
|
<Folder Include="Utilities\AccountManagement\" />
|
||||||
<Folder Include="Controls\DateTime\" />
|
<Folder Include="Controls\DateTime\" />
|
||||||
|
<Folder Include="Controls\IconLabelButton\" />
|
||||||
|
<Folder Include="Controls\PasswordStrengthProgressBar\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -412,8 +426,16 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Behaviors\" />
|
<None Remove="Behaviors\" />
|
||||||
<None Remove="Xamarin.CommunityToolkit" />
|
<None Remove="Xamarin.CommunityToolkit" />
|
||||||
|
<None Remove="Lists\" />
|
||||||
|
<None Remove="Lists\DataTemplates\" />
|
||||||
|
<None Remove="Lists\DataTemplateSelectors\" />
|
||||||
|
<None Remove="Lists\DataTemplates\CustomFields\" />
|
||||||
|
<None Remove="Lists\ItemViewModels\" />
|
||||||
|
<None Remove="Lists\ItemViewModels\CustomFields\" />
|
||||||
<None Remove="Controls\AccountSwitchingOverlay\" />
|
<None Remove="Controls\AccountSwitchingOverlay\" />
|
||||||
<None Remove="Utilities\AccountManagement\" />
|
<None Remove="Utilities\AccountManagement\" />
|
||||||
<None Remove="Controls\DateTime\" />
|
<None Remove="Controls\DateTime\" />
|
||||||
|
<None Remove="Controls\IconLabelButton\" />
|
||||||
|
<None Remove="Controls\PasswordStrengthProgressBar\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
@@ -7,9 +8,11 @@ using Bit.App.Resources;
|
|||||||
using Bit.App.Services;
|
using Bit.App.Services;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.App.Utilities.AccountManagement;
|
using Bit.App.Utilities.AccountManagement;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@@ -25,13 +28,15 @@ namespace Bit.App
|
|||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly IStorageService _secureStorageService;
|
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
|
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
|
||||||
|
private static bool _pendingCheckPasswordlessLoginRequests;
|
||||||
|
private static object _processingLoginRequestLock = new object();
|
||||||
|
|
||||||
public App(AppOptions appOptions)
|
public App(AppOptions appOptions)
|
||||||
{
|
{
|
||||||
@@ -47,10 +52,10 @@ namespace Bit.App
|
|||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
||||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||||
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
|
|
||||||
_accountsManager.Init(() => Options, this);
|
_accountsManager.Init(() => Options, this);
|
||||||
|
|
||||||
@@ -140,6 +145,16 @@ namespace Bit.App
|
|||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (message.Command == Constants.PasswordlessLoginRequestKey
|
||||||
|
|| message.Command == "unlocked"
|
||||||
|
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||||
|
{
|
||||||
|
lock (_processingLoginRequestLock)
|
||||||
|
{
|
||||||
|
// lock doesn't allow for async execution
|
||||||
|
CheckPasswordlessLoginRequestsAsync().Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -148,11 +163,91 @@ namespace Bit.App
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CheckPasswordlessLoginRequestsAsync()
|
||||||
|
{
|
||||||
|
if (!_isResumed)
|
||||||
|
{
|
||||||
|
_pendingCheckPasswordlessLoginRequests = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_pendingCheckPasswordlessLoginRequests = false;
|
||||||
|
if (await _vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
||||||
|
if (notification == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await CheckShouldSwitchActiveUserAsync(notification))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay to wait for the vault page to appear
|
||||||
|
await Task.Delay(2000);
|
||||||
|
// if there is a request modal opened ignore all incoming requests
|
||||||
|
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
|
||||||
|
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
|
||||||
|
{
|
||||||
|
PubKey = loginRequestData.PublicKey,
|
||||||
|
Id = loginRequestData.Id,
|
||||||
|
IpAddress = loginRequestData.RequestIpAddress,
|
||||||
|
Email = await _stateService.GetEmailAsync(),
|
||||||
|
FingerprintPhrase = loginRequestData.RequestFingerprint,
|
||||||
|
RequestDate = loginRequestData.CreationDate,
|
||||||
|
DeviceType = loginRequestData.RequestDeviceType,
|
||||||
|
Origin = loginRequestData.Origin
|
||||||
|
});
|
||||||
|
await _stateService.SetPasswordlessLoginNotificationAsync(null);
|
||||||
|
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
|
||||||
|
if (!loginRequestData.IsExpired)
|
||||||
|
{
|
||||||
|
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CheckShouldSwitchActiveUserAsync(PasswordlessRequestNotification notification)
|
||||||
|
{
|
||||||
|
var activeUserId = await _stateService.GetActiveUserIdAsync();
|
||||||
|
if (notification.UserId == activeUserId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationUserEmail = await _stateService.GetEmailAsync(notification.UserId);
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _deviceActionService.DisplayAlertAsync(AppResources.LogInRequested, string.Format(AppResources.LoginAttemptFromXDoYouWantToSwitchToThisAccount, notificationUserEmail), AppResources.Cancel, AppResources.Ok);
|
||||||
|
if (result == AppResources.Ok)
|
||||||
|
{
|
||||||
|
await _stateService.SetActiveUserAsync(notification.UserId);
|
||||||
|
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public AppOptions Options { get; private set; }
|
public AppOptions Options { get; private set; }
|
||||||
|
|
||||||
protected async override void OnStart()
|
protected async override void OnStart()
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
||||||
|
_isResumed = true;
|
||||||
await ClearCacheIfNeededAsync();
|
await ClearCacheIfNeededAsync();
|
||||||
Prime();
|
Prime();
|
||||||
if (string.IsNullOrWhiteSpace(Options.Uri))
|
if (string.IsNullOrWhiteSpace(Options.Uri))
|
||||||
@@ -164,6 +259,10 @@ namespace Bit.App
|
|||||||
SyncIfNeeded();
|
SyncIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
|
{
|
||||||
|
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
||||||
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
@@ -196,6 +295,10 @@ namespace Bit.App
|
|||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||||
_isResumed = true;
|
_isResumed = true;
|
||||||
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
|
{
|
||||||
|
_messagingService.Send(Constants.PasswordlessLoginRequestKey);
|
||||||
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
ResumedAsync().FireAndForget();
|
||||||
@@ -245,7 +348,7 @@ namespace Bit.App
|
|||||||
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
||||||
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
|
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
|
||||||
{
|
{
|
||||||
var task = Task.Run(() => _deviceActionService.ClearCacheAsync());
|
var task = Task.Run(() => _fileService.ClearCacheAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
AccountView = accountView;
|
AccountView = accountView;
|
||||||
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
||||||
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email);
|
?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email, AccountView.AvatarColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountView AccountView
|
public AccountView AccountView
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -10,7 +11,9 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public class AvatarImageSource : StreamImageSource
|
public class AvatarImageSource : StreamImageSource
|
||||||
{
|
{
|
||||||
private string _data;
|
private readonly string _text;
|
||||||
|
private readonly string _id;
|
||||||
|
private readonly string _color;
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
{
|
{
|
||||||
@@ -21,21 +24,23 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
if (obj is AvatarImageSource avatar)
|
if (obj is AvatarImageSource avatar)
|
||||||
{
|
{
|
||||||
return avatar._data == _data;
|
return avatar._id == _id && avatar._text == _text && avatar._color == _color;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.Equals(obj);
|
return base.Equals(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode() => _data?.GetHashCode() ?? -1;
|
public override int GetHashCode() => _id?.GetHashCode() ?? _text?.GetHashCode() ?? -1;
|
||||||
|
|
||||||
public AvatarImageSource(string name = null, string email = null)
|
public AvatarImageSource(string userId = null, string name = null, string email = null, string color = null)
|
||||||
{
|
{
|
||||||
_data = name;
|
_id = userId;
|
||||||
if (string.IsNullOrWhiteSpace(_data))
|
_text = name;
|
||||||
|
if (string.IsNullOrWhiteSpace(_text))
|
||||||
{
|
{
|
||||||
_data = email;
|
_text = email;
|
||||||
}
|
}
|
||||||
|
_color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Func<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
|
public override Func<CancellationToken, Task<Stream>> Stream => GetStreamAsync;
|
||||||
@@ -52,24 +57,24 @@ namespace Bit.App.Controls
|
|||||||
private Stream Draw()
|
private Stream Draw()
|
||||||
{
|
{
|
||||||
string chars;
|
string chars;
|
||||||
string upperData = null;
|
string upperCaseText = null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_data))
|
if (string.IsNullOrEmpty(_text))
|
||||||
{
|
{
|
||||||
chars = "..";
|
chars = "..";
|
||||||
}
|
}
|
||||||
else if (_data?.Length > 1)
|
else if (_text?.Length > 1)
|
||||||
{
|
{
|
||||||
upperData = _data.ToUpper();
|
upperCaseText = _text.ToUpper();
|
||||||
chars = GetFirstLetters(upperData, 2);
|
chars = GetFirstLetters(upperCaseText, 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
chars = upperData = _data.ToUpper();
|
chars = upperCaseText = _text.ToUpper();
|
||||||
}
|
}
|
||||||
|
|
||||||
var bgColor = StringToColor(upperData);
|
var bgColor = _color ?? CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
|
||||||
var textColor = Color.White;
|
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
|
||||||
var size = 50;
|
var size = 50;
|
||||||
|
|
||||||
using (var bitmap = new SKBitmap(size * 2,
|
using (var bitmap = new SKBitmap(size * 2,
|
||||||
@@ -85,7 +90,7 @@ namespace Bit.App.Controls
|
|||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
StrokeJoin = SKStrokeJoin.Miter,
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
Color = SKColor.Parse(bgColor.ToHex())
|
Color = SKColor.Parse(bgColor)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
||||||
@@ -97,7 +102,7 @@ namespace Bit.App.Controls
|
|||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
StrokeJoin = SKStrokeJoin.Miter,
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
Color = SKColor.Parse(bgColor.ToHex())
|
Color = SKColor.Parse(bgColor)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||||
@@ -108,7 +113,7 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
Color = SKColor.Parse(textColor.ToHex()),
|
Color = SKColor.Parse(textColor),
|
||||||
TextSize = textSize,
|
TextSize = textSize,
|
||||||
TextAlign = SKTextAlign.Center,
|
TextAlign = SKTextAlign.Center,
|
||||||
Typeface = typeface
|
Typeface = typeface
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public interface IAvatarImageSourcePool
|
public interface IAvatarImageSourcePool
|
||||||
{
|
{
|
||||||
AvatarImageSource GetOrCreateAvatar(string name, string email);
|
AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
||||||
|
|
||||||
public AvatarImageSource GetOrCreateAvatar(string name, string email)
|
public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email, string color)
|
||||||
{
|
{
|
||||||
var key = $"{name}{email}";
|
var key = $"{userId}{name}{email}{color}";
|
||||||
if (!_cache.TryGetValue(key, out var avatar))
|
if (!_cache.TryGetValue(key, out var avatar))
|
||||||
{
|
{
|
||||||
avatar = new AvatarImageSource(name, email);
|
avatar = new AvatarImageSource(userId, name, email, color);
|
||||||
if (!_cache.TryAdd(key, avatar)
|
if (!_cache.TryAdd(key, avatar)
|
||||||
&&
|
&&
|
||||||
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
|
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ namespace Bit.App.Controls
|
|||||||
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
|
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
|
||||||
|
|
||||||
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
|
||||||
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromHex("175DDC"));
|
||||||
|
|
||||||
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
|
||||||
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.FromHex("dd4b39"));
|
||||||
|
|
||||||
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
|
||||||
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.White);
|
||||||
|
|
||||||
public double Progress
|
public double Progress
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Xamarin.Forms;
|
using System.Linq;
|
||||||
|
using Xamarin.CommunityToolkit.Converters;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -6,4 +8,13 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public string ExtraDataForLogging { get; set; }
|
public string ExtraDataForLogging { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SelectionChangedEventArgsConverter : BaseNullableConverterOneWay<SelectionChangedEventArgs, object>
|
||||||
|
{
|
||||||
|
public override object? ConvertFrom(SelectionChangedEventArgs? value)
|
||||||
|
{
|
||||||
|
return value?.CurrentSelection.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Bit.App.Controls
|
|||||||
public class ExtendedSlider : Slider
|
public class ExtendedSlider : Slider
|
||||||
{
|
{
|
||||||
public static readonly BindableProperty ThumbBorderColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty ThumbBorderColorProperty = BindableProperty.Create(
|
||||||
nameof(ThumbBorderColor), typeof(Color), typeof(ExtendedSlider), Color.Default);
|
nameof(ThumbBorderColor), typeof(Color), typeof(ExtendedSlider), Color.FromHex("b5b5b5"));
|
||||||
|
|
||||||
public Color ThumbBorderColor
|
public Color ThumbBorderColor
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ namespace Bit.App.Controls
|
|||||||
public class ExtendedStepper : Stepper
|
public class ExtendedStepper : Stepper
|
||||||
{
|
{
|
||||||
public static readonly BindableProperty StepperBackgroundColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty StepperBackgroundColorProperty = BindableProperty.Create(
|
||||||
nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Color.Default);
|
nameof(StepperBackgroundColor), typeof(Color), typeof(ExtendedStepper), Color.White);
|
||||||
|
|
||||||
public static readonly BindableProperty StepperForegroundColorProperty = BindableProperty.Create(
|
public static readonly BindableProperty StepperForegroundColorProperty = BindableProperty.Create(
|
||||||
nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Color.Default);
|
nameof(StepperForegroundColor), typeof(Color), typeof(ExtendedStepper), Color.Black);
|
||||||
|
|
||||||
public Color StepperBackgroundColor
|
public Color StepperBackgroundColor
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Xamarin.Forms;
|
using Bit.App.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -16,6 +17,8 @@ namespace Bit.App.Controls
|
|||||||
FontFamily = "bwi-font.ttf#bwi-font";
|
FontFamily = "bwi-font.ttf#bwi-font";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Effects.Add(new RemoveFontPaddingEffect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Xamarin.Forms;
|
using Bit.App.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,8 @@ namespace Bit.App.Controls
|
|||||||
FontFamily = "bwi-font.ttf#bwi-font";
|
FontFamily = "bwi-font.ttf#bwi-font";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Effects.Add(new RemoveFontPaddingEffect());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/App/Controls/IconLabelButton/IconLabelButton.xaml
Normal file
42
src/App/Controls/IconLabelButton/IconLabelButton.xaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Frame xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Controls.IconLabelButton"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Name="_iconLabelButton"
|
||||||
|
HeightRequest="45"
|
||||||
|
Padding="1"
|
||||||
|
StyleClass="btn-icon-secondary"
|
||||||
|
BackgroundColor="{Binding IconLabelBorderColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
BorderColor="Transparent"
|
||||||
|
HasShadow="False">
|
||||||
|
<Frame.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding ButtonCommand, Source={x:Reference _iconLabelButton}}" />
|
||||||
|
</Frame.GestureRecognizers>
|
||||||
|
<Frame
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
CornerRadius="{Binding CornerRadius, Source={x:Reference _iconLabelButton}}"
|
||||||
|
BackgroundColor="{Binding IconLabelBackgroundColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
BorderColor="Transparent"
|
||||||
|
IsClippedToBounds="True"
|
||||||
|
HasShadow="False">
|
||||||
|
<StackLayout
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalOptions="Center">
|
||||||
|
<controls:IconLabel
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
FontSize="Large"
|
||||||
|
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
Text="{Binding Icon, Source={x:Reference _iconLabelButton}}">
|
||||||
|
</controls:IconLabel>
|
||||||
|
<Label
|
||||||
|
VerticalOptions="Center"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
TextColor="{Binding IconLabelColor, Source={x:Reference _iconLabelButton}}"
|
||||||
|
FontSize="Medium"
|
||||||
|
Text="{Binding Label, Source={x:Reference _iconLabelButton}}"/>
|
||||||
|
</StackLayout>
|
||||||
|
</Frame>
|
||||||
|
</Frame>
|
||||||
75
src/App/Controls/IconLabelButton/IconLabelButton.xaml.cs
Normal file
75
src/App/Controls/IconLabelButton/IconLabelButton.xaml.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Xaml;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class IconLabelButton : Frame
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty IconProperty = BindableProperty.Create(
|
||||||
|
nameof(Icon), typeof(string), typeof(IconLabelButton));
|
||||||
|
|
||||||
|
public static readonly BindableProperty LabelProperty = BindableProperty.Create(
|
||||||
|
nameof(Label), typeof(string), typeof(IconLabelButton));
|
||||||
|
|
||||||
|
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
||||||
|
nameof(ButtonCommand), typeof(ICommand), typeof(IconLabelButton));
|
||||||
|
|
||||||
|
public static readonly BindableProperty IconLabelColorProperty = BindableProperty.Create(
|
||||||
|
nameof(IconLabelColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
||||||
|
public static readonly BindableProperty IconLabelBackgroundColorProperty = BindableProperty.Create(
|
||||||
|
nameof(IconLabelBackgroundColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
||||||
|
public static readonly BindableProperty IconLabelBorderColorProperty = BindableProperty.Create(
|
||||||
|
nameof(IconLabelBorderColor), typeof(Color), typeof(IconLabelButton), Color.White);
|
||||||
|
|
||||||
|
public IconLabelButton()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Icon
|
||||||
|
{
|
||||||
|
get => GetValue(IconProperty) as string;
|
||||||
|
set => SetValue(IconProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Label
|
||||||
|
{
|
||||||
|
get => GetValue(LabelProperty) as string;
|
||||||
|
set => SetValue(LabelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand ButtonCommand
|
||||||
|
{
|
||||||
|
get => GetValue(ButtonCommandProperty) as ICommand;
|
||||||
|
set => SetValue(ButtonCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color IconLabelColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(IconLabelColorProperty); }
|
||||||
|
set { SetValue(IconLabelColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color IconLabelBackgroundColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(IconLabelBackgroundColorProperty); }
|
||||||
|
set { SetValue(IconLabelBackgroundColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color IconLabelBorderColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(IconLabelBorderColorProperty); }
|
||||||
|
set { SetValue(IconLabelBorderColorProperty, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public interface IPasswordStrengthable
|
||||||
|
{
|
||||||
|
string Password { get; }
|
||||||
|
List<string> UserInputs { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Bit.Core.Attributes;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public enum PasswordStrengthLevel
|
||||||
|
{
|
||||||
|
[LocalizableEnum("Weak")]
|
||||||
|
VeryWeak,
|
||||||
|
[LocalizableEnum("Weak")]
|
||||||
|
Weak,
|
||||||
|
[LocalizableEnum("Good")]
|
||||||
|
Good,
|
||||||
|
[LocalizableEnum("Strong")]
|
||||||
|
Strong
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<StackLayout
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
x:DataType="controls:PasswordStrengthViewModel"
|
||||||
|
x:Class="Bit.App.Controls.PasswordStrengthProgressBar"
|
||||||
|
StyleClass="box">
|
||||||
|
|
||||||
|
<StackLayout.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:LocalizableEnumConverter x:Key="localizableEnum" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</StackLayout.Resources>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
x:Name="_progressBar"
|
||||||
|
u:ProgressBarExtensions.AnimatedProgress="{Binding PasswordStrength}"
|
||||||
|
ScaleY="2" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
x:Name="_progressLabel"
|
||||||
|
Text="{Binding PasswordStrengthLevel, Converter={StaticResource localizableEnum}, TargetNullValue=' ' }"
|
||||||
|
StyleClass="box-footer-label" />
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class PasswordStrengthProgressBar : StackLayout
|
||||||
|
{
|
||||||
|
public static readonly BindableProperty PasswordStrengthLevelProperty = BindableProperty.Create(
|
||||||
|
nameof(PasswordStrengthLevel),
|
||||||
|
typeof(PasswordStrengthLevel),
|
||||||
|
typeof(PasswordStrengthProgressBar),
|
||||||
|
propertyChanged: OnControlPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty VeryWeakColorProperty = BindableProperty.Create(
|
||||||
|
nameof(VeryWeakColor),
|
||||||
|
typeof(Color),
|
||||||
|
typeof(PasswordStrengthProgressBar),
|
||||||
|
propertyChanged: OnControlPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty WeakColorProperty = BindableProperty.Create(
|
||||||
|
nameof(WeakColor),
|
||||||
|
typeof(Color),
|
||||||
|
typeof(PasswordStrengthProgressBar),
|
||||||
|
propertyChanged: OnControlPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty GoodColorProperty = BindableProperty.Create(
|
||||||
|
nameof(GoodColor),
|
||||||
|
typeof(Color),
|
||||||
|
typeof(PasswordStrengthProgressBar),
|
||||||
|
propertyChanged: OnControlPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty StrongColorProperty = BindableProperty.Create(
|
||||||
|
nameof(StrongColor),
|
||||||
|
typeof(Color),
|
||||||
|
typeof(PasswordStrengthProgressBar),
|
||||||
|
propertyChanged: OnControlPropertyChanged);
|
||||||
|
|
||||||
|
public PasswordStrengthLevel? PasswordStrengthLevel
|
||||||
|
{
|
||||||
|
get { return (PasswordStrengthLevel?)GetValue(PasswordStrengthLevelProperty); }
|
||||||
|
set { SetValue(PasswordStrengthLevelProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color VeryWeakColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(VeryWeakColorProperty); }
|
||||||
|
set { SetValue(VeryWeakColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color WeakColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(WeakColorProperty); }
|
||||||
|
set { SetValue(WeakColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color GoodColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(GoodColorProperty); }
|
||||||
|
set { SetValue(GoodColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color StrongColor
|
||||||
|
{
|
||||||
|
get { return (Color)GetValue(StrongColorProperty); }
|
||||||
|
set { SetValue(StrongColorProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordStrengthProgressBar()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
SetBinding(PasswordStrengthProgressBar.PasswordStrengthLevelProperty, new Binding() { Path = nameof(PasswordStrengthViewModel.PasswordStrengthLevel) });
|
||||||
|
UpdateColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnControlPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
(bindable as PasswordStrengthProgressBar)?.UpdateColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateColors()
|
||||||
|
{
|
||||||
|
if (_progressBar == null || _progressLabel == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_progressBar.ProgressColor = GetColorForStrength();
|
||||||
|
_progressLabel.TextColor = _progressBar.ProgressColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color GetColorForStrength()
|
||||||
|
{
|
||||||
|
switch (PasswordStrengthLevel)
|
||||||
|
{
|
||||||
|
case Controls.PasswordStrengthLevel.VeryWeak:
|
||||||
|
return VeryWeakColor;
|
||||||
|
case Controls.PasswordStrengthLevel.Weak:
|
||||||
|
return WeakColor;
|
||||||
|
case Controls.PasswordStrengthLevel.Good:
|
||||||
|
return GoodColor;
|
||||||
|
case Controls.PasswordStrengthLevel.Strong:
|
||||||
|
return StrongColor;
|
||||||
|
default:
|
||||||
|
return Color.Transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class PasswordStrengthViewModel : ExtendedViewModel
|
||||||
|
{
|
||||||
|
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||||
|
private readonly IPasswordStrengthable _passwordStrengthable;
|
||||||
|
private double _passwordStrength;
|
||||||
|
private Color _passwordColor;
|
||||||
|
private PasswordStrengthLevel? _passwordStrengthLevel;
|
||||||
|
|
||||||
|
public PasswordStrengthViewModel(IPasswordStrengthable passwordStrengthable)
|
||||||
|
{
|
||||||
|
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
||||||
|
_passwordStrengthable = passwordStrengthable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double PasswordStrength
|
||||||
|
{
|
||||||
|
get => _passwordStrength;
|
||||||
|
set => SetProperty(ref _passwordStrength, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordStrengthLevel? PasswordStrengthLevel
|
||||||
|
{
|
||||||
|
get => _passwordStrengthLevel;
|
||||||
|
set => SetProperty(ref _passwordStrengthLevel, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetPasswordStrengthUserInput(string email) => _passwordGenerationService.GetPasswordStrengthUserInput(email);
|
||||||
|
|
||||||
|
public void CalculatePasswordStrength()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_passwordStrengthable.Password))
|
||||||
|
{
|
||||||
|
PasswordStrength = 0;
|
||||||
|
PasswordStrengthLevel = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordStrength = _passwordGenerationService.PasswordStrength(_passwordStrengthable.Password, _passwordStrengthable.UserInputs);
|
||||||
|
// The passwordStrength.Score is 0..4, convertion was made to be used as a progress directly by the control 0..1
|
||||||
|
PasswordStrength = (passwordStrength.Score + 1f) / 5f;
|
||||||
|
if (PasswordStrength <= 0.4f)
|
||||||
|
{
|
||||||
|
PasswordStrengthLevel = Controls.PasswordStrengthLevel.VeryWeak;
|
||||||
|
}
|
||||||
|
else if (PasswordStrength <= 0.6f)
|
||||||
|
{
|
||||||
|
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Weak;
|
||||||
|
}
|
||||||
|
else if (PasswordStrength <= 0.8f)
|
||||||
|
{
|
||||||
|
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Good;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PasswordStrengthLevel = Controls.PasswordStrengthLevel.Strong;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.Collections;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
|
[Obsolete]
|
||||||
public class RepeaterView : StackLayout
|
public class RepeaterView : StackLayout
|
||||||
{
|
{
|
||||||
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
|
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
|
||||||
|
|||||||
13
src/App/Effects/RemoveFontPaddingEffect.cs
Normal file
13
src/App/Effects/RemoveFontPaddingEffect.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Effects
|
||||||
|
{
|
||||||
|
public class RemoveFontPaddingEffect : RoutingEffect
|
||||||
|
{
|
||||||
|
public RemoveFontPaddingEffect()
|
||||||
|
: base("Bitwarden.RemoveFontPaddingEffect")
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Bit.App.Lists.ItemViewModels.CustomFields;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.DataTemplateSelectors
|
||||||
|
{
|
||||||
|
public class CustomFieldItemTemplateSelector : DataTemplateSelector
|
||||||
|
{
|
||||||
|
public DataTemplate TextTemplate { get; set; }
|
||||||
|
public DataTemplate BooleanTemplate { get; set; }
|
||||||
|
public DataTemplate LinkedTemplate { get; set; }
|
||||||
|
public DataTemplate HiddenTemplate { get; set; }
|
||||||
|
|
||||||
|
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
|
||||||
|
{
|
||||||
|
switch (item)
|
||||||
|
{
|
||||||
|
case BooleanCustomFieldItemViewModel _:
|
||||||
|
return BooleanTemplate;
|
||||||
|
case LinkedCustomFieldItemViewModel _:
|
||||||
|
return LinkedTemplate;
|
||||||
|
case HiddenCustomFieldItemViewModel _:
|
||||||
|
return HiddenTemplate;
|
||||||
|
default:
|
||||||
|
return TextTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<StackLayout
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.BooleanCustomFieldItemLayout"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
|
x:DataType="cfvm:BooleanCustomFieldItemViewModel"
|
||||||
|
Spacing="0" Padding="0">
|
||||||
|
<StackLayout.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||||
|
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</StackLayout.Resources>
|
||||||
|
<Grid
|
||||||
|
StyleClass="box-row"
|
||||||
|
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label
|
||||||
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
<Label
|
||||||
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
VerticalOptions="FillAndExpand"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.RowSpan="2" />
|
||||||
|
<controls:IconLabel
|
||||||
|
Text="{Binding BooleanValue, Mode=OneWay, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Checkbox}}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0, 5, 0, 0"
|
||||||
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
<Switch
|
||||||
|
IsToggled="{Binding BooleanValue}"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2" />
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||||
|
Command="{Binding FieldOptionsCommand}"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView StyleClass="box-row-separator" />
|
||||||
|
</StackLayout>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||||
|
{
|
||||||
|
public partial class BooleanCustomFieldItemLayout : StackLayout
|
||||||
|
{
|
||||||
|
public BooleanCustomFieldItemLayout()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.HiddenCustomFieldItemLayout"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
|
x:DataType="cfvm:HiddenCustomFieldItemViewModel"
|
||||||
|
Spacing="0" Padding="0">
|
||||||
|
<StackLayout.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||||
|
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</StackLayout.Resources>
|
||||||
|
<Grid
|
||||||
|
StyleClass="box-row"
|
||||||
|
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label
|
||||||
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0" />
|
||||||
|
<StackLayout
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding IsEditing, Converter={StaticResource inverseBool}}">
|
||||||
|
<controls:MonoLabel
|
||||||
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
IsVisible="{Binding ShowHiddenValue}" />
|
||||||
|
<controls:MonoLabel
|
||||||
|
Text="{Binding Field.MaskedValue, Mode=OneWay}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
||||||
|
</StackLayout>
|
||||||
|
<controls:MonoEntry
|
||||||
|
Text="{Binding Field.Value}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||||
|
IsEnabled="{Binding ShowViewHidden}"
|
||||||
|
IsSpellCheckEnabled="False"
|
||||||
|
IsTextPredictionEnabled="False">
|
||||||
|
<Entry.Keyboard>
|
||||||
|
<Keyboard x:FactoryMethod="Create">
|
||||||
|
<x:Arguments>
|
||||||
|
<KeyboardFlags>None</KeyboardFlags>
|
||||||
|
</x:Arguments>
|
||||||
|
</Keyboard>
|
||||||
|
</Entry.Keyboard>
|
||||||
|
</controls:MonoEntry>
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding ShowHiddenValue, Converter={StaticResource iconGlyphConverter}, ConverterParameter={x:Static u:BooleanGlyphType.Eye}}"
|
||||||
|
Command="{Binding ToggleHiddenValueCommand}"
|
||||||
|
IsVisible="{Binding ShowViewHidden}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
|
Command="{Binding CopyFieldCommand}"
|
||||||
|
IsVisible="{Binding ShowCopyButton}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Copy}" />
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||||
|
Command="{Binding FieldOptionsCommand}"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
</StackLayout>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||||
|
{
|
||||||
|
public partial class HiddenCustomFieldItemLayout : StackLayout
|
||||||
|
{
|
||||||
|
public HiddenCustomFieldItemLayout()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.LinkedCustomFieldItemLayout"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
|
x:DataType="cfvm:LinkedCustomFieldItemViewModel"
|
||||||
|
Spacing="0" Padding="0">
|
||||||
|
<StackLayout.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</StackLayout.Resources>
|
||||||
|
<Grid
|
||||||
|
StyleClass="box-row"
|
||||||
|
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label
|
||||||
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0" />
|
||||||
|
<controls:IconLabel
|
||||||
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
<StackLayout
|
||||||
|
StyleClass="box-row, box-row-input"
|
||||||
|
IsVisible="{Binding IsEditing}">
|
||||||
|
<Picker
|
||||||
|
x:Name="_linkedFieldOptionPicker"
|
||||||
|
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
|
||||||
|
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
|
||||||
|
ItemDisplayBinding="{Binding Key}"
|
||||||
|
StyleClass="box-value" />
|
||||||
|
</StackLayout>
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||||
|
Command="{Binding FieldOptionsCommand}"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
</StackLayout>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||||
|
{
|
||||||
|
public partial class LinkedCustomFieldItemLayout : StackLayout
|
||||||
|
{
|
||||||
|
public LinkedCustomFieldItemLayout()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Lists.ItemLayouts.CustomFields.TextCustomFieldItemLayout"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
xmlns:cfvm="clr-namespace:Bit.App.Lists.ItemViewModels.CustomFields"
|
||||||
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
|
x:DataType="cfvm:TextCustomFieldItemViewModel"
|
||||||
|
Spacing="0" Padding="0">
|
||||||
|
<StackLayout.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
<u:BooleanToBoxRowInputPaddingConverter x:Key="booleanToBoxRowInputPaddingConverter" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</StackLayout.Resources>
|
||||||
|
<Grid
|
||||||
|
StyleClass="box-row"
|
||||||
|
Padding="{Binding IsEditing, Converter={StaticResource booleanToBoxRowInputPaddingConverter}}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label
|
||||||
|
Text="{Binding Field.Name, Mode=OneWay}"
|
||||||
|
StyleClass="box-label"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0" />
|
||||||
|
<Label
|
||||||
|
Text="{Binding ValueText, Mode=OneWay}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
<Entry
|
||||||
|
Text="{Binding Field.Value}"
|
||||||
|
StyleClass="box-value"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding IsEditing}" />
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
|
Command="{Binding CopyFieldCommand}"
|
||||||
|
IsVisible="{Binding ShowCopyButton}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Copy}" />
|
||||||
|
<controls:IconButton
|
||||||
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
|
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||||
|
Command="{Binding FieldOptionsCommand}"
|
||||||
|
IsVisible="{Binding IsEditing}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n Options}" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView StyleClass="box-row-separator" IsVisible="{Binding IsEditing, Mode=OneWay, Converter={StaticResource inverseBool}}" />
|
||||||
|
</StackLayout>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemLayouts.CustomFields
|
||||||
|
{
|
||||||
|
public partial class TextCustomFieldItemLayout : StackLayout
|
||||||
|
{
|
||||||
|
public TextCustomFieldItemLayout()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public abstract class BaseCustomFieldItemViewModel : ExtendedViewModel, ICustomFieldItemViewModel
|
||||||
|
{
|
||||||
|
protected FieldView _field;
|
||||||
|
protected bool _isEditing;
|
||||||
|
private string[] _additionalFieldProperties = new string[]
|
||||||
|
{
|
||||||
|
nameof(ValueText),
|
||||||
|
nameof(ShowCopyButton)
|
||||||
|
};
|
||||||
|
|
||||||
|
public BaseCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
|
||||||
|
{
|
||||||
|
_field = field;
|
||||||
|
_isEditing = isEditing;
|
||||||
|
FieldOptionsCommand = new Command(() => fieldOptionsCommand?.Execute(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldView Field
|
||||||
|
{
|
||||||
|
get => _field;
|
||||||
|
set => SetProperty(ref _field, value,
|
||||||
|
additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(ValueText),
|
||||||
|
nameof(ShowCopyButton),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEditing => _isEditing;
|
||||||
|
|
||||||
|
public virtual bool ShowCopyButton => false;
|
||||||
|
|
||||||
|
public virtual string ValueText => _field.Value;
|
||||||
|
|
||||||
|
public ICommand FieldOptionsCommand { get; }
|
||||||
|
|
||||||
|
public void TriggerFieldChanged()
|
||||||
|
{
|
||||||
|
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user