mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
139 Commits
beeep-totp
...
bugfix/SG-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f75cc95cce | ||
|
|
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 | ||
|
|
525288d804 | ||
|
|
e829279e29 | ||
|
|
3d9555d420 | ||
|
|
5f7a1e769a | ||
|
|
8b118408fa | ||
|
|
de41845e3e | ||
|
|
61597585b5 | ||
|
|
e04b250a73 | ||
|
|
4fbe1b40e3 | ||
|
|
3ef5b576ac | ||
|
|
570b56364a | ||
|
|
ae4e8e2d8e | ||
|
|
2c8406d0ad | ||
|
|
94bd5ceed3 | ||
|
|
aa6be3d691 | ||
|
|
97fe65647a | ||
|
|
ee8b8866e0 | ||
|
|
3128a4c5c8 | ||
|
|
8ec6545bbc | ||
|
|
90a6850d76 | ||
|
|
16f70dc0ce | ||
|
|
f0ebc5e644 | ||
|
|
03c5dd78c1 | ||
|
|
e2b6e99a0c | ||
|
|
263aeef030 | ||
|
|
f809170c51 | ||
|
|
c2fcc0ac52 | ||
|
|
5e61fb0a14 | ||
|
|
cf222bd0c3 | ||
|
|
cb0c52fb26 | ||
|
|
c07c305384 | ||
|
|
d2fbf5bdea | ||
|
|
2d2a883b96 | ||
|
|
1f2fb3f796 | ||
|
|
8f3a4b98a5 | ||
|
|
70cf7431f7 | ||
|
|
f2ba86a62b | ||
|
|
292908f53f | ||
|
|
d621a5d2f3 | ||
|
|
75e8276784 | ||
|
|
67f49a0591 | ||
|
|
cceded2a0f | ||
|
|
846d3a85a2 | ||
|
|
7802da2b9c | ||
|
|
cd56a124d5 | ||
|
|
58a3662d0f | ||
|
|
6c7413e38c | ||
|
|
547e61a66b | ||
|
|
d246d1dece | ||
|
|
e2502e2e0c | ||
|
|
448cce38e1 | ||
|
|
dbc1e5ea3e | ||
|
|
a6ddc2496f | ||
|
|
d9a818279f | ||
|
|
6e2e613fee | ||
|
|
109aeb49e4 | ||
|
|
c892e9fa57 | ||
|
|
b2500557e7 | ||
|
|
7c311fbb55 | ||
|
|
f24388c1b5 | ||
|
|
3aef86bd34 | ||
|
|
c53a85cd50 | ||
|
|
448758a697 | ||
|
|
e51233bf9b | ||
|
|
f9cbe43627 | ||
|
|
5579817f9f | ||
|
|
51a5f58258 | ||
|
|
388ad4e840 | ||
|
|
48a8d9ae35 | ||
|
|
dd6003bd4f | ||
|
|
fba407f3b6 | ||
|
|
88b406544b | ||
|
|
3438ed94ce | ||
|
|
ec71b21264 | ||
|
|
b223f5f16e | ||
|
|
0a64e4c918 | ||
|
|
9b41db962e | ||
|
|
43d3c7b5d7 |
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## Before you submit
|
## Before you submit
|
||||||
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
|
- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
|
||||||
- [ ] This change requires a **documentation update** (notify the documentation team)
|
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
|
||||||
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
|
- If this change requires a **documentation update** - notify the documentation team
|
||||||
|
- If this change has particular **deployment requirements** - notify the DevOps team
|
||||||
|
|||||||
22
.github/renovate.json
vendored
Normal file
22
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base",
|
||||||
|
"schedule:monthly",
|
||||||
|
":maintainLockFilesMonthly",
|
||||||
|
":preserveSemverRanges",
|
||||||
|
":rebaseStalePrs",
|
||||||
|
":disableDependencyDashboard"
|
||||||
|
],
|
||||||
|
"enabledManagers": [
|
||||||
|
"nuget"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["nuget"],
|
||||||
|
"groupName": "Nuget updates",
|
||||||
|
"groupSlug": "nuget",
|
||||||
|
"separateMajorMinor": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
64
.github/workflows/automatic-issue-responses.yml
vendored
Normal file
64
.github/workflows/automatic-issue-responses.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: Automatic responses
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
jobs:
|
||||||
|
close-issue:
|
||||||
|
name: 'Close issue with automatic response'
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
# Feature request
|
||||||
|
- if: github.event.label.name == 'feature-request'
|
||||||
|
name: Feature request
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.
|
||||||
|
|
||||||
|
Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.
|
||||||
|
|
||||||
|
This issue will now be closed. Thanks!
|
||||||
|
# Intended behavior
|
||||||
|
- if: github.event.label.name == 'intended-behavior'
|
||||||
|
name: Intended behaviour
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
Your issue appears to be describing the intended behavior of the software. If you want this to be changed, it would be a feature request.
|
||||||
|
|
||||||
|
We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.
|
||||||
|
|
||||||
|
Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.
|
||||||
|
|
||||||
|
This issue will now be closed. Thanks!
|
||||||
|
# Customer support request
|
||||||
|
- if: github.event.label.name == 'customer-support'
|
||||||
|
name: Customer Support request
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
We use GitHub issues as a place to track bugs and other development related issues. Your issue appears to be a support request, or would otherwise be better handled by our dedicated Customer Success team.
|
||||||
|
|
||||||
|
Please contact us using our [Contact page](https://bitwarden.com/contact). You can include a link to this issue in the message content.
|
||||||
|
|
||||||
|
Alternatively, you can also search for an answer in our [help documentation](https://bitwarden.com/help/) or get help from other Bitwarden users on our [community forums](https://community.bitwarden.com/c/support/). The issue here will be closed.
|
||||||
|
# Resolved
|
||||||
|
- if: github.event.label.name == 'resolved'
|
||||||
|
name: Resolved
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
We’ve closed this issue, as it appears the original problem has been resolved. If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.
|
||||||
|
# Stale
|
||||||
|
- if: github.event.label.name == 'stale'
|
||||||
|
name: Stale
|
||||||
|
uses: peter-evans/close-issue@849549ba7c3a595a064c4b2c56f206ee78f93515 # v2.0.0
|
||||||
|
with:
|
||||||
|
comment: |
|
||||||
|
As we haven’t heard from you about this problem in some time, this issue will now be closed.
|
||||||
|
|
||||||
|
If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.
|
||||||
104
.github/workflows/build.yml
vendored
104
.github/workflows/build.yml
vendored
@@ -57,12 +57,37 @@ jobs:
|
|||||||
|
|
||||||
android:
|
android:
|
||||||
name: Android
|
name: Android
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
|
with:
|
||||||
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
|
run: |
|
||||||
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
|
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
||||||
|
$componentsToAdd = @(
|
||||||
|
"Component.Xamarin"
|
||||||
|
)
|
||||||
|
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
|
||||||
|
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
|
||||||
|
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
||||||
|
if ($process.ExitCode -eq 0)
|
||||||
|
{
|
||||||
|
Write-Host "components have been successfully added"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write-Host "components were not installed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -207,11 +232,36 @@ jobs:
|
|||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Build
|
name: F-Droid Build
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
|
with:
|
||||||
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Set up MSBuild
|
- name: Set up MSBuild
|
||||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||||
|
|
||||||
|
- name: Work Around for broken Windows 2022 Runner Image
|
||||||
|
run: |
|
||||||
|
Set-Location "C:\Program Files (x86)\Microsoft Visual Studio\Installer\"
|
||||||
|
$InstallPath = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"
|
||||||
|
$componentsToAdd = @(
|
||||||
|
"Component.Xamarin"
|
||||||
|
)
|
||||||
|
[string]$workloadArgs = $componentsToAdd | ForEach-Object {" --add " + $_}
|
||||||
|
$Arguments = ('/c', "vs_installer.exe", 'modify', '--installPath', "`"$InstallPath`"",$workloadArgs, '--quiet', '--norestart', '--nocache')
|
||||||
|
$process = Start-Process -FilePath cmd.exe -ArgumentList $Arguments -Wait -PassThru -WindowStyle Hidden
|
||||||
|
if ($process.ExitCode -eq 0)
|
||||||
|
{
|
||||||
|
Write-Host "components have been successfully added"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write-Host "components were not installed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -368,6 +418,11 @@ jobs:
|
|||||||
runs-on: macos-11
|
runs-on: macos-11
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup NuGet
|
||||||
|
uses: nuget/setup-nuget@b2bc17b761a1d88cab755a776c7922eb26eefbfa # v1.0.6
|
||||||
|
with:
|
||||||
|
nuget-version: 5.9.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
nuget help | grep Version
|
nuget help | grep Version
|
||||||
@@ -386,10 +441,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 "::set-output name=$i::$VALUE"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Decrypt secrets
|
- name: Decrypt secrets
|
||||||
env:
|
env:
|
||||||
@@ -580,10 +642,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 "::set-output name=$i::$VALUE"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Upload Sources
|
- name: Upload Sources
|
||||||
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
|
||||||
@@ -640,11 +709,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 "::set-output name=$i::$VALUE"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Notify Slack on failure
|
- name: Notify Slack on failure
|
||||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||||
|
|||||||
14
.github/workflows/crowdin-pull.yml
vendored
14
.github/workflows/crowdin-pull.yml
vendored
@@ -24,13 +24,13 @@ 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@e39093fd75daae7859c68eded4b43d42ec78d8ea
|
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||||
@@ -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 }}
|
||||||
|
|||||||
90
.github/workflows/release.yml
vendored
90
.github/workflows/release.yml
vendored
@@ -13,6 +13,11 @@ on:
|
|||||||
- Initial Release
|
- Initial Release
|
||||||
- Redeploy
|
- Redeploy
|
||||||
- Dry Run
|
- Dry Run
|
||||||
|
fdroid_publish:
|
||||||
|
description: 'Publish to f-droid store'
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -34,29 +39,13 @@ jobs:
|
|||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||||
|
|
||||||
- name: Retrieve Mobile release version
|
- name: Check Release Version
|
||||||
id: retrieve-mobile-version
|
id: version
|
||||||
run: |
|
uses: bitwarden/gh-actions/release-version-check@8f055ef543c7433c967a1b9b04a0f230923233bb
|
||||||
ver=$(sed -E -n '/^<manifest/s/^.*[ ]android:versionName="([^"]+)".*$/\1/p' \
|
with:
|
||||||
./src/Android/Properties/AndroidManifest.xml | tr -d '"')
|
release-type: ${{ github.event.inputs.release_type }}
|
||||||
echo "::set-output name=mobile_version::${ver}"
|
project-type: xamarin
|
||||||
shell: bash
|
file: src/Android/Properties/AndroidManifest.xml
|
||||||
|
|
||||||
- name: Check to make sure Mobile release version has been bumped
|
|
||||||
if: ${{ github.event.inputs.release_type == 'Initial Release' }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
latest_ver=$(hub release -L 1 -f '%T')
|
|
||||||
latest_ver=${latest_ver:1}
|
|
||||||
echo "Latest version: $latest_ver"
|
|
||||||
ver=${{ steps.retrieve-mobile-version.outputs.mobile_version }}
|
|
||||||
echo "Version: $ver"
|
|
||||||
if [ "$latest_ver" = "$ver" ]; then
|
|
||||||
echo "Version has not been bumped!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Get branch name
|
- name: Get branch name
|
||||||
id: branch
|
id: branch
|
||||||
@@ -64,18 +53,38 @@ jobs:
|
|||||||
BRANCH_NAME=$(basename ${{ github.ref }})
|
BRANCH_NAME=$(basename ${{ github.ref }})
|
||||||
echo "::set-output name=branch-name::$BRANCH_NAME"
|
echo "::set-output name=branch-name::$BRANCH_NAME"
|
||||||
|
|
||||||
|
- name: Create GitHub deployment
|
||||||
|
uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48
|
||||||
|
id: deployment
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
initial-status: 'in_progress'
|
||||||
|
environment: 'production'
|
||||||
|
description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ steps.branch.outputs.branch-name }}'
|
||||||
|
task: release
|
||||||
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
|
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:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ steps.branch.outputs.branch-name }}
|
branch: ${{ steps.branch.outputs.branch-name }}
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
|
with:
|
||||||
|
workflow: build.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
branch: master
|
||||||
|
|
||||||
- name: Prep Bitwarden iOS release asset
|
- name: Prep Bitwarden iOS release asset
|
||||||
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
if: github.event.inputs.release_type != 'Dry Run'
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01 # v1.9.0
|
||||||
with:
|
with:
|
||||||
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
|
||||||
@@ -83,22 +92,40 @@ jobs:
|
|||||||
./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"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
tag: v${{ steps.retrieve-mobile-version.outputs.mobile_version }}
|
tag: v${{ steps.version.outputs.version }}
|
||||||
name: Version ${{ steps.retrieve-mobile-version.outputs.mobile_version }}
|
name: Version ${{ steps.version.outputs.version }}
|
||||||
body: "<insert release notes here>"
|
body: "<insert release notes here>"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: true
|
draft: true
|
||||||
|
|
||||||
|
- name: Update deployment status to Success
|
||||||
|
if: ${{ success() }}
|
||||||
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
state: 'success'
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
|
- name: Update deployment status to Failure
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: chrnorm/deployment-status@07b3930847f65e71c9c6802ff5a402f6dfb46b86
|
||||||
|
with:
|
||||||
|
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
state: 'failure'
|
||||||
|
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||||
|
|
||||||
|
|
||||||
f-droid:
|
f-droid:
|
||||||
name: F-Droid Release
|
name: F-Droid Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: release
|
needs: release
|
||||||
|
if: inputs.fdroid_publish
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
|
||||||
|
|
||||||
- name: Download F-Droid .apk artifact
|
- name: Download F-Droid .apk artifact
|
||||||
|
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:
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
@@ -106,6 +133,15 @@ 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
|
||||||
|
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||||
|
uses: dawidd6/action-download-artifact@575b1e4167df67acf7e692af784566618b23c71e # v2.17.10
|
||||||
|
with:
|
||||||
|
workflow: build.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
branch: master
|
||||||
|
name: com.x8bit.bitwarden-fdroid.apk
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
uses: actions/setup-node@1f8c6b94b26d0feae1e387ca63ccbdc44d27b561 # v2.5.1
|
||||||
with:
|
with:
|
||||||
@@ -171,5 +207,5 @@ jobs:
|
|||||||
cd $GITHUB_WORKSPACE
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
- name: Deploy to gh-pages
|
- name: Deploy to gh-pages
|
||||||
if: github.event.inputs.release_type != 'Dry Run'
|
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||||
run: npm run deploy
|
run: npm run deploy
|
||||||
|
|||||||
30
.github/workflows/stale-bot.yml
vendored
Normal file
30
.github/workflows/stale-bot.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: 'Close stale issues and PRs'
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)
|
||||||
|
- cron: '23 5 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
name: 'Check for stale issues and PRs'
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: 'Run stale action'
|
||||||
|
uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 # v5.0.0
|
||||||
|
with:
|
||||||
|
stale-issue-label: 'needs-reply'
|
||||||
|
stale-pr-label: 'needs-changes'
|
||||||
|
days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process
|
||||||
|
days-before-issue-close: 14 # Close issue if no further activity after X days
|
||||||
|
days-before-pr-close: 21 # Close PR if no further activity after X days
|
||||||
|
close-issue-message: |
|
||||||
|
We need more information before we can help you with your problem. As we haven’t heard from you recently, this issue will be closed.
|
||||||
|
|
||||||
|
If this happens again or continues to be an problem, please respond to this issue with the information we’ve requested and anything else relevant.
|
||||||
|
close-pr-message: |
|
||||||
|
We can’t merge your pull request until you make the changes we’ve requested. As we haven’t heard from you recently, this pull request will be closed.
|
||||||
|
|
||||||
|
If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.
|
||||||
|
|
||||||
|
Please make sure to resolve any conflicts with the master branch before requesting another review.
|
||||||
63
.github/workflows/version-auto-bump.yml
vendored
Normal file
63
.github/workflows/version-auto-bump.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: Version Auto Bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
name: "Setup"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
version_number: ${{ steps.version.outputs.new-version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Branch
|
||||||
|
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||||
|
|
||||||
|
- name: Calculate bumped version
|
||||||
|
id: version
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
|
||||||
|
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
|
||||||
|
echo "Current Major: $CURR_MAJOR"
|
||||||
|
echo "Current Patch: $CURR_PATCH"
|
||||||
|
|
||||||
|
NEW_PATCH=$((CURR_PATCH+1))
|
||||||
|
NEW_VER=$CURR_MAJOR.$NEW_PATCH
|
||||||
|
echo "New Version: $NEW_VER"
|
||||||
|
echo "::set-output name=new-version::$NEW_VER"
|
||||||
|
|
||||||
|
trigger_version_bump:
|
||||||
|
name: "Trigger version bump workflow"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- setup
|
||||||
|
steps:
|
||||||
|
- name: Login to Azure
|
||||||
|
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010
|
||||||
|
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-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Call GitHub API to trigger workflow bump
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
VERSION: ${{ needs.setup.outputs.version_number}}
|
||||||
|
run: |
|
||||||
|
JSON_STRING=$(printf '{"ref":"master", "inputs": { "version_number":"%s"}}' "$VERSION")
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
-i -u bitwarden-devops-bot:$TOKEN \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
https://api.github.com/repos/bitwarden/mobile/actions/workflows/version-bump.yml/dispatches \
|
||||||
|
-d $JSON_STRING
|
||||||
48
.github/workflows/version-bump.yml
vendored
48
.github/workflows/version-bump.yml
vendored
@@ -16,15 +16,29 @@ 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 }}
|
||||||
git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
|
||||||
|
|
||||||
- name: Checkout Version Branch
|
|
||||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
|
||||||
with:
|
|
||||||
ref: 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
|
||||||
@@ -56,16 +70,32 @@ jobs:
|
|||||||
version: ${{ github.event.inputs.version_number }}
|
version: ${{ github.event.inputs.version_number }}
|
||||||
file_path: "./src/iOS/Info.plist"
|
file_path: "./src/iOS/Info.plist"
|
||||||
|
|
||||||
- name: Commit files
|
- name: Setup git
|
||||||
|
run: |
|
||||||
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
|
|
||||||
|
- name: Check if version changed
|
||||||
|
id: version-changed
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "::set-output name=changes_to_commit::TRUE"
|
||||||
|
else
|
||||||
|
echo "::set-output name=changes_to_commit::FALSE"
|
||||||
|
echo "No changes to commit!";
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit files
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
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' }}
|
||||||
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
|
||||||
|
|
||||||
- name: Create Version PR
|
- name: Create Version PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
@@ -1,40 +1,3 @@
|
|||||||
# How to Contribute
|
# How to Contribute
|
||||||
|
|
||||||
Contributions of all kinds are welcome!
|
Our [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) are located in our [Contributing Documentation](https://contributing.bitwarden.com/). The documentation also includes recommended tooling, code style tips, and lots of other great information to get you started.
|
||||||
|
|
||||||
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
|
|
||||||
|
|
||||||
Here is how you can get involved:
|
|
||||||
|
|
||||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
|
||||||
|
|
||||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
|
||||||
|
|
||||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
|
||||||
|
|
||||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
|
||||||
|
|
||||||
* **Help other users:** Go to the [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/) on the Community Forums
|
|
||||||
|
|
||||||
* **Translate:** See the localization (i10n) section below
|
|
||||||
|
|
||||||
## Contributor Agreement
|
|
||||||
|
|
||||||
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/mobile) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
|
|
||||||
|
|
||||||
## Pull Request Guidelines
|
|
||||||
|
|
||||||
* commit any pull requests against the `master` branch
|
|
||||||
* include a link to your Community Forums post
|
|
||||||
|
|
||||||
# Localization (l10n)
|
|
||||||
|
|
||||||
[](https://crowdin.com/project/bitwarden-mobile)
|
|
||||||
|
|
||||||
We use a translation tool called [Crowdin](https://crowdin.com) to help manage our localization efforts across many different languages.
|
|
||||||
|
|
||||||
If you are interested in helping translate the Bitwarden mobile app into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-mobile
|
|
||||||
|
|
||||||
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/dwbit).
|
|
||||||
|
|
||||||
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -12,16 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin
|
|||||||
|
|
||||||
# Build/Run
|
# Build/Run
|
||||||
|
|
||||||
**Requirements**
|
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.
|
||||||
|
|
||||||
- [Visual Studio](https://visualstudio.microsoft.com/)
|
|
||||||
- [Xamarin](https://docs.microsoft.com/en-us/xamarin/get-started/installation/?pivots=windows)
|
|
||||||
|
|
||||||
**Run the app**
|
|
||||||
|
|
||||||
- Open the solution file in Visual Studio.
|
|
||||||
- Restore the nuget packages.
|
|
||||||
- Build and run the app.
|
|
||||||
|
|
||||||
# We're Hiring!
|
# We're Hiring!
|
||||||
|
|
||||||
@@ -29,8 +20,7 @@ Interested in contributing in a big way? Consider joining our team! We're hiring
|
|||||||
|
|
||||||
# Contribute
|
# Contribute
|
||||||
|
|
||||||
Code contributions are welcome! Visual Studio with Xamarin is required to work on this project. Please commit any pull requests against the `master` branch.
|
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.
|
||||||
Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
|
|
||||||
|
|
||||||
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
|
||||||
|
|
||||||
@@ -45,11 +35,3 @@ We recently migrated to using dotnet-format as code formatter. All previous bran
|
|||||||
5. Commit
|
5. Commit
|
||||||
6. Run `git merge -Xours 04539af2a66668b6e85476d5cf318c9150ec4357`
|
6. Run `git merge -Xours 04539af2a66668b6e85476d5cf318c9150ec4357`
|
||||||
7. Push
|
7. Push
|
||||||
|
|
||||||
#### Git blame
|
|
||||||
|
|
||||||
We also recommend that you configure git to ignore the prettier revision using:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.google.android.apps.chrome", "url_bar"),
|
new Browser("com.google.android.apps.chrome", "url_bar"),
|
||||||
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
|
||||||
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
|
// Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId.
|
||||||
|
new Browser("com.iode.firefox", "mozac_browser_toolbar_url_view"),
|
||||||
new Browser("com.jamal2367.styx", "search"),
|
new Browser("com.jamal2367.styx", "search"),
|
||||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||||
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
|
new Browser("com.kiwibrowser.browser.dev", "url_bar"),
|
||||||
@@ -67,6 +68,7 @@ namespace Bit.Droid.Accessibility
|
|||||||
new Browser("com.naver.whale", "url_bar"),
|
new Browser("com.naver.whale", "url_bar"),
|
||||||
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.mini.native", "url_field"),
|
new Browser("com.opera.mini.native", "url_field"),
|
||||||
new Browser("com.opera.mini.native.beta", "url_field"),
|
new Browser("com.opera.mini.native.beta", "url_field"),
|
||||||
new Browser("com.opera.touch", "addressbarEdit"),
|
new Browser("com.opera.touch", "addressbarEdit"),
|
||||||
@@ -365,7 +367,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>v12.1</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" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.9" />
|
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.11" />
|
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.10" />
|
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
|
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" />
|
||||||
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
|
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" />
|
||||||
<PackageReference Include="Xamarin.Essentials">
|
<PackageReference Include="Xamarin.Essentials">
|
||||||
<Version>1.7.2</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>
|
||||||
@@ -151,6 +151,11 @@
|
|||||||
<Compile Include="Services\ClipboardService.cs" />
|
<Compile Include="Services\ClipboardService.cs" />
|
||||||
<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="Receivers\NotificationDismissReceiver.cs" />
|
||||||
|
<Compile Include="Services\FileService.cs" />
|
||||||
|
<Compile Include="Services\AutofillHandler.cs" />
|
||||||
|
<Compile Include="Constants.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
@@ -175,6 +180,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" />
|
||||||
@@ -212,6 +218,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" />
|
||||||
@@ -279,6 +292,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>
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -73,6 +74,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.google.android.apps.chrome",
|
"com.google.android.apps.chrome",
|
||||||
"com.google.android.apps.chrome_dev",
|
"com.google.android.apps.chrome_dev",
|
||||||
"com.google.android.captiveportallogin",
|
"com.google.android.captiveportallogin",
|
||||||
|
"com.iode.firefox",
|
||||||
"com.jamal2367.styx",
|
"com.jamal2367.styx",
|
||||||
"com.kiwibrowser.browser",
|
"com.kiwibrowser.browser",
|
||||||
"com.kiwibrowser.browser.dev",
|
"com.kiwibrowser.browser.dev",
|
||||||
@@ -86,6 +88,7 @@ namespace Bit.Droid.Autofill
|
|||||||
"com.naver.whale",
|
"com.naver.whale",
|
||||||
"com.opera.browser",
|
"com.opera.browser",
|
||||||
"com.opera.browser.beta",
|
"com.opera.browser.beta",
|
||||||
|
"com.opera.gx",
|
||||||
"com.opera.mini.native",
|
"com.opera.mini.native",
|
||||||
"com.opera.mini.native.beta",
|
"com.opera.mini.native.beta",
|
||||||
"com.opera.touch",
|
"com.opera.touch",
|
||||||
@@ -268,8 +271,7 @@ namespace Bit.Droid.Autofill
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
intent.PutExtra("autofillFrameworkUri", uri);
|
intent.PutExtra("autofillFrameworkUri", uri);
|
||||||
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
|
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
|
||||||
PendingIntentFlags.CancelCurrent);
|
|
||||||
|
|
||||||
var overlayPresentation = BuildOverlayPresentation(
|
var overlayPresentation = BuildOverlayPresentation(
|
||||||
AppResources.AutofillWithBitwarden,
|
AppResources.AutofillWithBitwarden,
|
||||||
@@ -322,7 +324,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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Android/Effects/NoEmojiKeyboardEffect.cs
Normal file
24
src/Android/Effects/NoEmojiKeyboardEffect.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Android.Widget;
|
||||||
|
using Bit.Droid.Effects;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
|
||||||
|
[assembly: ExportEffect(typeof(NoEmojiKeyboardEffect), nameof(NoEmojiKeyboardEffect))]
|
||||||
|
namespace Bit.Droid.Effects
|
||||||
|
{
|
||||||
|
public class NoEmojiKeyboardEffect : PlatformEffect
|
||||||
|
{
|
||||||
|
protected override void OnAttached()
|
||||||
|
{
|
||||||
|
if (Control is EditText editText)
|
||||||
|
{
|
||||||
|
editText.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationVisiblePassword | Android.Text.InputTypes.TextFlagMultiLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetached()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,12 +5,14 @@ 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;
|
||||||
@@ -18,7 +20,11 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
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 +36,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 +54,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;
|
||||||
@@ -64,12 +76,13 @@ namespace Bit.Droid
|
|||||||
Intent?.Validate();
|
Intent?.Validate();
|
||||||
|
|
||||||
base.OnCreate(savedInstanceState);
|
base.OnCreate(savedInstanceState);
|
||||||
if (!CoreHelpers.InDebugMode())
|
|
||||||
|
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
|
||||||
{
|
{
|
||||||
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)
|
||||||
@@ -80,8 +93,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) =>
|
||||||
{
|
{
|
||||||
@@ -137,6 +151,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)
|
||||||
@@ -190,13 +213,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
|
||||||
{
|
{
|
||||||
@@ -209,7 +232,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;
|
||||||
@@ -231,7 +254,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));
|
||||||
@@ -272,7 +295,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");
|
||||||
@@ -400,5 +423,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ using Bit.Core.Abstractions;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Droid.Services;
|
using Bit.Droid.Services;
|
||||||
using Bit.Droid.Utilities;
|
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
using Plugin.Fingerprint;
|
using Plugin.Fingerprint;
|
||||||
using Xamarin.Android.Net;
|
using Xamarin.Android.Net;
|
||||||
@@ -20,6 +19,8 @@ using System.Net.Http;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.App.Pages;
|
using Bit.App.Pages;
|
||||||
|
using Bit.App.Utilities.AccountManagement;
|
||||||
|
using Bit.App.Controls;
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
using Android.Gms.Security;
|
using Android.Gms.Security;
|
||||||
#endif
|
#endif
|
||||||
@@ -45,8 +46,9 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
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);
|
||||||
|
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(
|
||||||
@@ -62,6 +64,17 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||||
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
|
ServiceContainer.Resolve<ICryptoService>("cryptoService"));
|
||||||
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
ServiceContainer.Register<IVerificationActionsFlowHelper>("verificationActionsFlowHelper", verificationActionsFlowHelper);
|
||||||
|
|
||||||
|
var accountsManager = new AccountsManager(
|
||||||
|
ServiceContainer.Resolve<IBroadcasterService>("broadcasterService"),
|
||||||
|
ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"),
|
||||||
|
ServiceContainer.Resolve<IStorageService>("secureStorageService"),
|
||||||
|
ServiceContainer.Resolve<IStateService>("stateService"),
|
||||||
|
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||||
|
ServiceContainer.Resolve<IAuthService>("authService"),
|
||||||
|
ServiceContainer.Resolve<ILogger>("logger"),
|
||||||
|
ServiceContainer.Resolve<IMessagingService>("messagingService"));
|
||||||
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||||
@@ -90,12 +103,13 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
|
ServiceContainer.Register<INativeLogService>("nativeLogService", new AndroidLogService());
|
||||||
#if FDROID
|
#if FDROID
|
||||||
ServiceContainer.Register<ILogger>("logger", new StubLogger());
|
var logger = new StubLogger();
|
||||||
#elif DEBUG
|
#elif DEBUG
|
||||||
ServiceContainer.Register<ILogger>("logger", DebugLogger.Instance);
|
var logger = DebugLogger.Instance;
|
||||||
#else
|
#else
|
||||||
ServiceContainer.Register<ILogger>("logger", Logger.Instance);
|
var logger = Logger.Instance;
|
||||||
#endif
|
#endif
|
||||||
|
ServiceContainer.Register("logger", logger);
|
||||||
|
|
||||||
// Note: This might cause a race condition. Investigate more.
|
// Note: This might cause a race condition. Investigate more.
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
@@ -115,19 +129,21 @@ namespace Bit.Droid
|
|||||||
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
|
||||||
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
||||||
var localizeService = new LocalizeService();
|
var localizeService = new LocalizeService();
|
||||||
var broadcasterService = new BroadcasterService();
|
var broadcasterService = new BroadcasterService(logger);
|
||||||
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
||||||
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
||||||
var secureStorageService = new SecureStorageService();
|
var secureStorageService = new SecureStorageService();
|
||||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||||
var stateService = new StateService(mobileStorageService, secureStorageService);
|
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
|
||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var deviceActionService = new DeviceActionService(stateService, messagingService,
|
var clipboardService = new ClipboardService(stateService);
|
||||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
||||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
var fileService = new FileService(stateService, broadcasterService);
|
||||||
broadcasterService);
|
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, new LazyResolve<IEventService>());
|
||||||
|
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
||||||
|
messagingService, broadcasterService);
|
||||||
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);
|
||||||
@@ -142,13 +158,16 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
||||||
ServiceContainer.Register<IStateService>("stateService", stateService);
|
ServiceContainer.Register<IStateService>("stateService", stateService);
|
||||||
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
||||||
ServiceContainer.Register<IClipboardService>("clipboardService", new ClipboardService(stateService));
|
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);
|
||||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||||
|
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||||
|
|
||||||
// Push
|
// Push
|
||||||
#if FDROID
|
#if FDROID
|
||||||
@@ -179,5 +198,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,57 +1,49 @@
|
|||||||
<?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.05.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="2022.10.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
|
||||||
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30"/>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.NFC"/>
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
|
||||||
<application android:label="Bitwarden" android:theme="@style/LaunchTheme" android:allowBackup="false" tools:replace="android:allowBackup" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
|
</provider>
|
||||||
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.x8bit.bitwarden.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
|
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions" />
|
||||||
</provider>
|
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
|
||||||
|
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true" />
|
||||||
<meta-data android:name="android.max_aspect" android:value="2.1"/>
|
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true" />
|
||||||
<meta-data android:name="android.content.APP_RESTRICTIONS" android:resource="@xml/app_restrictions"/>
|
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
|
||||||
|
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true" />
|
||||||
<!-- Support for Samsung "Multi Window" mode (for Android < 7.0 users) -->
|
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
|
||||||
<meta-data android:name="com.samsung.android.sdk.multiwindow.enable" android:value="true"/>
|
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
|
||||||
<meta-data android:name="com.samsung.android.sdk.multiwindow.penwindow.enable" android:value="true"/>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<!-- Support for LG "Dual Window" mode (for Android < 7.0 users) -->
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<meta-data android:name="com.lge.support.SPLIT_WINDOW" android:value="true"/>
|
</intent-filter>
|
||||||
<!-- Declare MainActivity manually so we can set LaunchMode using API dependant resource -->
|
<intent-filter>
|
||||||
<activity android:name="com.x8bit.bitwarden.MainActivity" android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|uiMode" android:exported="true" android:icon="@mipmap/ic_launcher" android:label="Bitwarden" android:launchMode="@integer/launchModeAPIlevel" android:theme="@style/LaunchTheme">
|
<action android:name="android.intent.action.SEND" />
|
||||||
<intent-filter>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<data android:mimeType="application/*" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
<data android:mimeType="video/*" />
|
||||||
<intent-filter>
|
<data android:mimeType="text/*" />
|
||||||
<action android:name="android.intent.action.SEND"/>
|
</intent-filter>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
</activity>
|
||||||
<data android:mimeType="application/*"/>
|
</application>
|
||||||
<data android:mimeType="image/*"/>
|
<!-- Package visibility (for Android 11+) -->
|
||||||
<data android:mimeType="video/*"/>
|
<queries>
|
||||||
<data android:mimeType="text/*"/>
|
<intent>
|
||||||
</intent-filter>
|
<action android:name="*" />
|
||||||
</activity>
|
</intent>
|
||||||
</application>
|
</queries>
|
||||||
|
</manifest>
|
||||||
<!-- Package visibility (for Android 11+) -->
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="*"/>
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
</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,34 +18,41 @@ namespace Bit.Droid.Push
|
|||||||
{
|
{
|
||||||
public async override void OnNewToken(string token)
|
public async override void OnNewToken(string token)
|
||||||
{
|
{
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
try {
|
||||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
if (message?.Data == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (message?.Data == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var obj = JObject.Parse(data);
|
var 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,4 +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_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -2,4 +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_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"/>
|
||||||
@@ -77,6 +77,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.google.android.captiveportallogin"
|
android:name="com.google.android.captiveportallogin"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.iode.firefox"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.jamal2367.styx"
|
android:name="com.jamal2367.styx"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
@@ -116,6 +119,9 @@
|
|||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.opera.browser.beta"
|
android:name="com.opera.browser.beta"
|
||||||
android:maxLongVersionCode="10000000000"/>
|
android:maxLongVersionCode="10000000000"/>
|
||||||
|
<compatibility-package
|
||||||
|
android:name="com.opera.gx"
|
||||||
|
android:maxLongVersionCode="10000000000"/>
|
||||||
<compatibility-package
|
<compatibility-package
|
||||||
android:name="com.opera.mini.native"
|
android:name="com.opera.mini.native"
|
||||||
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
|
||||||
|
|||||||
210
src/Android/Services/AutofillHandler.cs
Normal file
210
src/Android/Services/AutofillHandler.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
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.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 LazyResolve<IEventService> _eventService;
|
||||||
|
|
||||||
|
public AutofillHandler(IStateService stateService,
|
||||||
|
IMessagingService messagingService,
|
||||||
|
IClipboardService clipboardService,
|
||||||
|
LazyResolve<IEventService> eventService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_messagingService = messagingService;
|
||||||
|
_clipboardService = clipboardService;
|
||||||
|
_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 = (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 = _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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Bit.Core;
|
using Android.OS;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Droid.Receivers;
|
using Bit.Droid.Receivers;
|
||||||
|
using Bit.Droid.Utilities;
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
using Xamarin.Essentials;
|
using Xamarin.Essentials;
|
||||||
|
|
||||||
@@ -23,14 +24,50 @@ namespace Bit.Droid.Services
|
|||||||
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
|
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
|
||||||
0,
|
0,
|
||||||
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
|
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
|
||||||
PendingIntentFlags.UpdateCurrent));
|
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CopyTextAsync(string text, int expiresInMs = -1)
|
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
|
||||||
{
|
{
|
||||||
await Clipboard.SetTextAsync(text);
|
try
|
||||||
|
{
|
||||||
|
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
|
||||||
|
if ((int)Build.VERSION.SdkInt < 33)
|
||||||
|
{
|
||||||
|
await Clipboard.SetTextAsync(text);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CopyToClipboard(text, isSensitive);
|
||||||
|
}
|
||||||
|
|
||||||
await ClearClipboardAlarmAsync(expiresInMs);
|
await ClearClipboardAlarmAsync(expiresInMs);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
// that the OS catches and just throws this exception.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCopyNotificationHandledByPlatform()
|
||||||
|
{
|
||||||
|
// Android 13+ provides built-in notification when text is copied to the clipboard
|
||||||
|
return (int)Build.VERSION.SdkInt >= 33;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToClipboard(string text, bool isSensitive = true)
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var clipboardManager = activity.GetSystemService(
|
||||||
|
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||||
|
var clipData = ClipData.NewPlainText("bitwarden", text);
|
||||||
|
if (isSensitive)
|
||||||
|
{
|
||||||
|
clipData.Description.Extras ??= new PersistableBundle();
|
||||||
|
clipData.Description.Extras.PutBoolean("android.content.extra.IS_SENSITIVE", true);
|
||||||
|
}
|
||||||
|
clipboardManager.PrimaryClip = clipData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ClearClipboardAlarmAsync(int expiresInMs = -1)
|
private async Task ClearClipboardAlarmAsync(int expiresInMs = -1)
|
||||||
|
|||||||
@@ -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,20 +9,13 @@ 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;
|
||||||
|
|
||||||
@@ -37,33 +25,18 @@ namespace Bit.Droid.Services
|
|||||||
{
|
{
|
||||||
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(
|
||||||
IStateService stateService,
|
IStateService stateService,
|
||||||
IMessagingService messagingService,
|
IMessagingService messagingService)
|
||||||
IBroadcasterService broadcasterService,
|
|
||||||
Func<IEventService> eventServiceFunc)
|
|
||||||
{
|
{
|
||||||
_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
|
||||||
@@ -209,184 +182,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)
|
||||||
@@ -464,34 +259,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(
|
||||||
@@ -523,25 +290,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;
|
||||||
@@ -632,112 +380,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()
|
||||||
{
|
{
|
||||||
@@ -768,25 +410,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
|
||||||
@@ -845,61 +468,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}"));
|
||||||
@@ -917,32 +485,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)
|
|
||||||
{
|
|
||||||
CopyToClipboard(totp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyToClipboard(string text)
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var clipboardManager = activity.GetSystemService(
|
|
||||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
|
||||||
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float GetSystemFontSizeScale()
|
public float GetSystemFontSizeScale()
|
||||||
{
|
{
|
||||||
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
||||||
@@ -953,5 +495,36 @@ namespace Bit.Droid.Services
|
|||||||
{
|
{
|
||||||
// for any Android-specific cleanup required after switching accounts
|
// for any Android-specific cleanup required after switching accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetScreenCaptureAllowedAsync()
|
||||||
|
{
|
||||||
|
if (CoreHelpers.ForceScreenCaptureEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var activity = CrossCurrentActivity.Current?.Activity;
|
||||||
|
if (await _stateService.GetScreenCaptureAllowedAsync())
|
||||||
|
{
|
||||||
|
activity.RunOnUiThread(() => activity.Window.ClearFlags(WindowManagerFlags.Secure));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ namespace Bit.Droid.Utilities
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
|
if (string.IsNullOrWhiteSpace(theme) && osDarkModeEnabled)
|
||||||
{
|
{
|
||||||
theme = "dark";
|
theme = ThemeManager.Dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme == "dark" || theme == "black" || theme == "nord")
|
if (theme == ThemeManager.Dark || theme == ThemeManager.Black || theme == ThemeManager.Nord)
|
||||||
{
|
{
|
||||||
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")]
|
||||||
|
|||||||
13
src/App/Abstractions/IAccountsManager.cs
Normal file
13
src/App/Abstractions/IAccountsManager.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Models;
|
||||||
|
|
||||||
|
namespace Bit.App.Abstractions
|
||||||
|
{
|
||||||
|
public interface IAccountsManager
|
||||||
|
{
|
||||||
|
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||||
|
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||||
|
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/App/Abstractions/IAccountsManagerHost.cs
Normal file
14
src/App/Abstractions/IAccountsManagerHost.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.App.Abstractions
|
||||||
|
{
|
||||||
|
public interface INavigationParams { }
|
||||||
|
|
||||||
|
public interface IAccountsManagerHost
|
||||||
|
{
|
||||||
|
Task SetPreviousPageInfoAsync();
|
||||||
|
void Navigate(NavigationTarget navTarget, INavigationParams navParams = null);
|
||||||
|
Task UpdateThemeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
@@ -8,45 +7,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();
|
||||||
|
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>
|
||||||
@@ -13,12 +13,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
|
<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.1" />
|
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
|
||||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.2" />
|
<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.2401" />
|
<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>
|
||||||
@@ -97,11 +97,11 @@
|
|||||||
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
<Compile Update="Pages\Vault\PasswordHistoryPage.xaml.cs">
|
||||||
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
<DependentUpon>PasswordHistoryPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Vault\AddEditPage.xaml.cs">
|
<Compile Update="Pages\Vault\CipherDetailsPage.xaml.cs">
|
||||||
<DependentUpon>AddEditPage.xaml</DependentUpon>
|
<DependentUpon>CipherDetailsPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Vault\ViewPage.xaml.cs">
|
<Compile Update="Pages\Vault\CipherAddEditPage.xaml.cs">
|
||||||
<DependentUpon>ViewPage.xaml</DependentUpon>
|
<DependentUpon>CipherAddEditPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
<Compile Update="Pages\Settings\SettingsPage\SettingsPage.xaml.cs">
|
||||||
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
<DependentUpon>SettingsPage.xaml</DependentUpon>
|
||||||
@@ -122,19 +122,26 @@
|
|||||||
<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>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="Behaviors\" />
|
<Folder Include="Behaviors\" />
|
||||||
<Folder Include="Pages\Authenticator\" />
|
<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="Controls\DateTime\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="Controls\CipherViewCell\CipherViewCell.xaml">
|
|
||||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
|
<EmbeddedResource Remove="Pages\Accounts\AccountsPopupPage.xaml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -162,12 +169,6 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Styles\Base.xaml">
|
|
||||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Resources\AppResources.cs.Designer.cs">
|
<Compile Update="Resources\AppResources.cs.Designer.cs">
|
||||||
<DependentUpon>AppResources.cs.resx</DependentUpon>
|
<DependentUpon>AppResources.cs.resx</DependentUpon>
|
||||||
@@ -420,6 +421,14 @@
|
|||||||
<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="Controls\DateTime\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ using Bit.App.Pages;
|
|||||||
using Bit.App.Resources;
|
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.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.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms.Xaml;
|
using Xamarin.Forms.Xaml;
|
||||||
@@ -16,19 +20,21 @@ using Xamarin.Forms.Xaml;
|
|||||||
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
namespace Bit.App
|
namespace Bit.App
|
||||||
{
|
{
|
||||||
public partial class App : Application
|
public partial class App : Application, IAccountsManagerHost
|
||||||
{
|
{
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
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 IPushNotificationService _pushNotificationService;
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
|
// this variable is static because the app is launching new activities on notification click, creating new instances of App.
|
||||||
|
private static bool _pendingCheckPasswordlessLoginRequests;
|
||||||
|
|
||||||
public App(AppOptions appOptions)
|
public App(AppOptions appOptions)
|
||||||
{
|
{
|
||||||
@@ -44,139 +50,185 @@ 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");
|
||||||
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
|
|
||||||
|
_accountsManager.Init(() => Options, this);
|
||||||
|
|
||||||
Bootstrap();
|
Bootstrap();
|
||||||
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
_broadcasterService.Subscribe(nameof(App), async (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "showDialog")
|
try
|
||||||
{
|
{
|
||||||
var details = message.Data as DialogDetails;
|
if (message.Command == "showDialog")
|
||||||
var confirmed = true;
|
|
||||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
|
||||||
AppResources.Ok : details.ConfirmText;
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
var details = message.Data as DialogDetails;
|
||||||
|
var confirmed = true;
|
||||||
|
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||||
|
AppResources.Ok : details.ConfirmText;
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
{
|
{
|
||||||
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||||
details.CancelText);
|
{
|
||||||
}
|
confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||||
else
|
details.CancelText);
|
||||||
{
|
}
|
||||||
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
else
|
||||||
}
|
{
|
||||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||||
});
|
}
|
||||||
}
|
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||||
else if (message.Command == "locked")
|
});
|
||||||
{
|
}
|
||||||
var extras = message.Data as Tuple<string, bool>;
|
else if (message.Command == "resumed")
|
||||||
var userId = extras?.Item1;
|
|
||||||
var userInitiated = extras?.Item2 ?? false;
|
|
||||||
Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated));
|
|
||||||
}
|
|
||||||
else if (message.Command == "lockVault")
|
|
||||||
{
|
|
||||||
await _vaultTimeoutService.LockAsync(true);
|
|
||||||
}
|
|
||||||
else if (message.Command == "logout")
|
|
||||||
{
|
|
||||||
var extras = message.Data as Tuple<string, bool, bool>;
|
|
||||||
var userId = extras?.Item1;
|
|
||||||
var userInitiated = extras?.Item2 ?? true;
|
|
||||||
var expired = extras?.Item3 ?? false;
|
|
||||||
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired));
|
|
||||||
}
|
|
||||||
else if (message.Command == "loggedOut")
|
|
||||||
{
|
|
||||||
// Clean up old migrated key if they ever log out.
|
|
||||||
await _secureStorageService.RemoveAsync("oldKey");
|
|
||||||
}
|
|
||||||
else if (message.Command == "resumed")
|
|
||||||
{
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
ResumedAsync().FireAndForget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Command == "slept")
|
||||||
|
{
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
await SleptAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Command == "migrated")
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
||||||
|
message.Command == "popAllAndGoToTabMyVault" ||
|
||||||
|
message.Command == "popAllAndGoToTabSend" ||
|
||||||
|
message.Command == "popAllAndGoToAutofillCiphers")
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
if (Current.MainPage is TabsPage tabsPage)
|
||||||
|
{
|
||||||
|
while (tabsPage.Navigation.ModalStack.Count > 0)
|
||||||
|
{
|
||||||
|
await tabsPage.Navigation.PopModalAsync(false);
|
||||||
|
}
|
||||||
|
if (message.Command == "popAllAndGoToAutofillCiphers")
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabMyVault")
|
||||||
|
{
|
||||||
|
Options.MyVaultTile = false;
|
||||||
|
tabsPage.ResetToVaultPage();
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabGenerator")
|
||||||
|
{
|
||||||
|
Options.GeneratorTile = false;
|
||||||
|
tabsPage.ResetToGeneratorPage();
|
||||||
|
}
|
||||||
|
else if (message.Command == "popAllAndGoToTabSend")
|
||||||
|
{
|
||||||
|
tabsPage.ResetToSendPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (message.Command == "convertAccountToKeyConnector")
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(async () =>
|
||||||
|
{
|
||||||
|
await Application.Current.MainPage.Navigation.PushModalAsync(
|
||||||
|
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||||
|
{
|
||||||
|
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "slept")
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
{
|
|
||||||
await SleptAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (message.Command == "addAccount")
|
|
||||||
{
|
|
||||||
await AddAccount();
|
|
||||||
}
|
|
||||||
else if (message.Command == "accountAdded")
|
|
||||||
{
|
|
||||||
await UpdateThemeAsync();
|
|
||||||
}
|
|
||||||
else if (message.Command == "switchedAccount")
|
|
||||||
{
|
|
||||||
await SwitchedAccountAsync();
|
|
||||||
}
|
|
||||||
else if (message.Command == "migrated")
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
await SetMainPageAsync();
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
|
||||||
message.Command == "popAllAndGoToTabMyVault" ||
|
|
||||||
message.Command == "popAllAndGoToTabSend" ||
|
|
||||||
message.Command == "popAllAndGoToAutofillCiphers")
|
|
||||||
{
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
if (Current.MainPage is TabsPage tabsPage)
|
|
||||||
{
|
|
||||||
while (tabsPage.Navigation.ModalStack.Count > 0)
|
|
||||||
{
|
|
||||||
await tabsPage.Navigation.PopModalAsync(false);
|
|
||||||
}
|
|
||||||
if (message.Command == "popAllAndGoToAutofillCiphers")
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabMyVault")
|
|
||||||
{
|
|
||||||
Options.MyVaultTile = false;
|
|
||||||
tabsPage.ResetToVaultPage();
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabGenerator")
|
|
||||||
{
|
|
||||||
Options.GeneratorTile = false;
|
|
||||||
tabsPage.ResetToGeneratorPage();
|
|
||||||
}
|
|
||||||
else if (message.Command == "popAllAndGoToTabSend")
|
|
||||||
{
|
|
||||||
tabsPage.ResetToSendPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (message.Command == "convertAccountToKeyConnector")
|
|
||||||
{
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
await Application.Current.MainPage.Navigation.PushModalAsync(
|
|
||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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.CreationDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
await Device.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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))
|
||||||
@@ -188,6 +240,10 @@ namespace Bit.App
|
|||||||
SyncIfNeeded();
|
SyncIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
|
{
|
||||||
|
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||||
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
@@ -220,6 +276,10 @@ namespace Bit.App
|
|||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||||
_isResumed = true;
|
_isResumed = true;
|
||||||
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
|
{
|
||||||
|
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||||
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
ResumedAsync().FireAndForget();
|
||||||
@@ -234,6 +294,7 @@ namespace Bit.App
|
|||||||
|
|
||||||
private async Task ResumedAsync()
|
private async Task ResumedAsync()
|
||||||
{
|
{
|
||||||
|
await _stateService.CheckExtensionActiveUserAndSwitchIfNeededAsync();
|
||||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
_messagingService.Send("startEventTimer");
|
_messagingService.Send("startEventTimer");
|
||||||
await UpdateThemeAsync();
|
await UpdateThemeAsync();
|
||||||
@@ -263,108 +324,12 @@ namespace Bit.App
|
|||||||
new System.Globalization.UmAlQuraCalendar();
|
new System.Globalization.UmAlQuraCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LogOutAsync(string userId, bool userInitiated, bool expired)
|
|
||||||
{
|
|
||||||
await AppHelpers.LogOutAsync(userId, userInitiated);
|
|
||||||
await SetMainPageAsync();
|
|
||||||
_authService.LogOut(() =>
|
|
||||||
{
|
|
||||||
if (expired)
|
|
||||||
{
|
|
||||||
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddAccount()
|
|
||||||
{
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
Options.HideAccountSwitcher = false;
|
|
||||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SwitchedAccountAsync()
|
|
||||||
{
|
|
||||||
await AppHelpers.OnAccountSwitchAsync();
|
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
|
||||||
{
|
|
||||||
if (await _vaultTimeoutService.ShouldTimeoutAsync())
|
|
||||||
{
|
|
||||||
await _vaultTimeoutService.ExecuteTimeoutActionAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await SetMainPageAsync();
|
|
||||||
}
|
|
||||||
await Task.Delay(50);
|
|
||||||
await UpdateThemeAsync();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetMainPageAsync()
|
|
||||||
{
|
|
||||||
var authed = await _stateService.IsAuthenticatedAsync();
|
|
||||||
if (authed)
|
|
||||||
{
|
|
||||||
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
|
|
||||||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
|
|
||||||
{
|
|
||||||
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
|
|
||||||
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
|
|
||||||
|
|
||||||
var email = await _stateService.GetEmailAsync();
|
|
||||||
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
|
|
||||||
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
|
|
||||||
}
|
|
||||||
else if (await _vaultTimeoutService.IsLockedAsync() ||
|
|
||||||
await _vaultTimeoutService.ShouldLockAsync())
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new LockPage(Options));
|
|
||||||
}
|
|
||||||
else if (Options.FromAutofillFramework && Options.SaveType.HasValue)
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new AddEditPage(appOptions: Options));
|
|
||||||
}
|
|
||||||
else if (Options.Uri != null)
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
|
||||||
}
|
|
||||||
else if (Options.CreateSend != null)
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Current.MainPage = new TabsPage(Options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Options.HideAccountSwitcher = await _stateService.GetActiveUserIdAsync() == null;
|
|
||||||
if (await _vaultTimeoutService.IsLoggedOutByTimeoutAsync() ||
|
|
||||||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
|
|
||||||
{
|
|
||||||
// TODO implement orgIdentifier flow to SSO Login page, same as email flow below
|
|
||||||
// var orgIdentifier = await _stateService.GetOrgIdentifierAsync();
|
|
||||||
|
|
||||||
var email = await _stateService.GetEmailAsync();
|
|
||||||
Current.MainPage = new NavigationPage(new LoginPage(email, Options));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ClearCacheIfNeededAsync()
|
private async Task ClearCacheIfNeededAsync()
|
||||||
{
|
{
|
||||||
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +385,7 @@ namespace Bit.App
|
|||||||
UpdateThemeAsync();
|
UpdateThemeAsync();
|
||||||
};
|
};
|
||||||
Current.MainPage = new NavigationPage(new HomePage(Options));
|
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||||
var mainPageTask = SetMainPageAsync();
|
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||||
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,50 +406,71 @@ namespace Bit.App
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LockedAsync(string userId, bool userInitiated)
|
public async Task SetPreviousPageInfoAsync()
|
||||||
{
|
{
|
||||||
if (!await _stateService.IsActiveAccountAsync(userId))
|
|
||||||
{
|
|
||||||
_platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var autoPromptBiometric = !userInitiated;
|
|
||||||
if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
|
|
||||||
{
|
|
||||||
var vaultTimeout = await _stateService.GetVaultTimeoutAsync();
|
|
||||||
if (vaultTimeout == 0)
|
|
||||||
{
|
|
||||||
autoPromptBiometric = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PreviousPageInfo lastPageBeforeLock = null;
|
PreviousPageInfo lastPageBeforeLock = null;
|
||||||
if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
||||||
{
|
{
|
||||||
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
var topPage = tabbedPage.Navigation.ModalStack[tabbedPage.Navigation.ModalStack.Count - 1];
|
||||||
if (topPage is NavigationPage navPage)
|
if (topPage is NavigationPage navPage)
|
||||||
{
|
{
|
||||||
if (navPage.CurrentPage is ViewPage viewPage)
|
if (navPage.CurrentPage is CipherDetailsPage cipherDetailsPage)
|
||||||
{
|
{
|
||||||
lastPageBeforeLock = new PreviousPageInfo
|
lastPageBeforeLock = new PreviousPageInfo
|
||||||
{
|
{
|
||||||
Page = "view",
|
Page = "view",
|
||||||
CipherId = viewPage.ViewModel.CipherId
|
CipherId = cipherDetailsPage.ViewModel.CipherId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (navPage.CurrentPage is AddEditPage addEditPage && addEditPage.ViewModel.EditMode)
|
else if (navPage.CurrentPage is CipherAddEditPage cipherAddEditPage && cipherAddEditPage.ViewModel.EditMode)
|
||||||
{
|
{
|
||||||
lastPageBeforeLock = new PreviousPageInfo
|
lastPageBeforeLock = new PreviousPageInfo
|
||||||
{
|
{
|
||||||
Page = "edit",
|
Page = "edit",
|
||||||
CipherId = addEditPage.ViewModel.CipherId
|
CipherId = cipherAddEditPage.ViewModel.CipherId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock);
|
await _stateService.SetPreviousPageInfoAsync(lastPageBeforeLock);
|
||||||
var lockPage = new LockPage(Options, autoPromptBiometric);
|
}
|
||||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
|
||||||
|
public void Navigate(NavigationTarget navTarget, INavigationParams navParams)
|
||||||
|
{
|
||||||
|
switch (navTarget)
|
||||||
|
{
|
||||||
|
case NavigationTarget.HomeLogin:
|
||||||
|
Current.MainPage = new NavigationPage(new HomePage(Options));
|
||||||
|
break;
|
||||||
|
case NavigationTarget.Login:
|
||||||
|
if (navParams is LoginNavigationParams loginParams)
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new LoginPage(loginParams.Email, Options));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NavigationTarget.Lock:
|
||||||
|
if (navParams is LockNavigationParams lockParams)
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new LockPage(Options, lockParams.AutoPromptBiometric));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Current.MainPage = new NavigationPage(new LockPage(Options));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NavigationTarget.Home:
|
||||||
|
Current.MainPage = new TabsPage(Options);
|
||||||
|
break;
|
||||||
|
case NavigationTarget.AddEditCipher:
|
||||||
|
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
||||||
|
break;
|
||||||
|
case NavigationTarget.AutofillCiphers:
|
||||||
|
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
||||||
|
break;
|
||||||
|
case NavigationTarget.SendAddEdit:
|
||||||
|
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70;
|
public int AccountListRowHeight => Device.RuntimePlatform == Device.Android ? 74 : 70;
|
||||||
|
|
||||||
|
public bool LongPressAccountEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
public Action AfterHide { get; set; }
|
||||||
|
|
||||||
public async Task ToggleVisibilityAsync()
|
public async Task ToggleVisibilityAsync()
|
||||||
{
|
{
|
||||||
if (IsVisible)
|
if (IsVisible)
|
||||||
@@ -135,6 +139,8 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
// remove overlay
|
// remove overlay
|
||||||
IsVisible = false;
|
IsVisible = false;
|
||||||
|
|
||||||
|
AfterHide?.Invoke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +173,7 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
private async Task LongPressAccountAsync(AccountViewCellViewModel item)
|
private async Task LongPressAccountAsync(AccountViewCellViewModel item)
|
||||||
{
|
{
|
||||||
if (!item.IsAccount)
|
if (!LongPressAccountEnabled || !item.IsAccount)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,23 +45,28 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
public ICommand LongPressAccountCommand { get; }
|
public ICommand LongPressAccountCommand { get; }
|
||||||
|
|
||||||
|
public bool FromIOSExtension { get; set; }
|
||||||
|
|
||||||
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
private async Task SelectAccountAsync(AccountViewCellViewModel item)
|
||||||
{
|
{
|
||||||
if (item.AccountView.IsAccount)
|
if (!item.AccountView.IsAccount)
|
||||||
{
|
{
|
||||||
if (!item.AccountView.IsActive)
|
_messagingService.Send(AccountsManagerMessageCommands.ADD_ACCOUNT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.AccountView.IsActive)
|
||||||
|
{
|
||||||
|
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
||||||
|
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
|
if (FromIOSExtension)
|
||||||
{
|
{
|
||||||
await _stateService.SetActiveUserAsync(item.AccountView.UserId);
|
await _stateService.SaveExtensionActiveUserIdToStorageAsync(item.AccountView.UserId);
|
||||||
_messagingService.Send("switchedAccount");
|
|
||||||
}
|
|
||||||
else if (AllowActiveAccountSelection)
|
|
||||||
{
|
|
||||||
_messagingService.Send("switchedAccount");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (AllowActiveAccountSelection)
|
||||||
{
|
{
|
||||||
_messagingService.Send("addAccount");
|
_messagingService.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace Bit.App.Controls
|
|||||||
public AccountViewCellViewModel(AccountView accountView)
|
public AccountViewCellViewModel(AccountView accountView)
|
||||||
{
|
{
|
||||||
AccountView = accountView;
|
AccountView = accountView;
|
||||||
AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email);
|
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
|
||||||
|
?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountView AccountView
|
public AccountView AccountView
|
||||||
|
|||||||
@@ -1,39 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Controls.AuthenticatorViewCell"
|
x:Class="Bit.App.Controls.AuthenticatorViewCell"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
|
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
|
||||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||||
StyleClass="list-row, list-row-platform"
|
StyleClass="list-row, list-row-platform"
|
||||||
RowSpacing="0"
|
HorizontalOptions="FillAndExpand"
|
||||||
ColumnSpacing="0"
|
x:DataType="pages:GroupingsPageTOTPListItem"
|
||||||
x:DataType="pages:AuthenticatorPageListItem">
|
ColumnDefinitions="40,*,40,Auto,40"
|
||||||
|
RowSpacing="0"
|
||||||
|
Padding="0,10,0,0"
|
||||||
|
RowDefinitions="*,*">
|
||||||
|
|
||||||
<Grid.Resources>
|
<Grid.Resources>
|
||||||
<u:IconGlyphConverter x:Key="iconGlyphConverter"/>
|
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||||
<u:IconImageConverter x:Key="iconImageConverter"/>
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
|
||||||
<u:StringHasValueConverter x:Key="stringHasValueConverter" />
|
|
||||||
</Grid.Resources>
|
</Grid.Resources>
|
||||||
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="40" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="60" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<controls:IconLabel
|
<controls:IconLabel
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
StyleClass="list-icon, list-icon-platform"
|
StyleClass="list-icon, list-icon-platform"
|
||||||
|
Grid.RowSpan="2"
|
||||||
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
|
||||||
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
|
||||||
AutomationProperties.IsInAccessibleTree="False" />
|
AutomationProperties.IsInAccessibleTree="False" />
|
||||||
@@ -47,67 +39,89 @@
|
|||||||
VerticalOptions="Center"
|
VerticalOptions="Center"
|
||||||
WidthRequest="22"
|
WidthRequest="22"
|
||||||
HeightRequest="22"
|
HeightRequest="22"
|
||||||
|
Grid.RowSpan="2"
|
||||||
IsVisible="{Binding ShowIconImage}"
|
IsVisible="{Binding ShowIconImage}"
|
||||||
Source="{Binding IconImageSource, Mode=OneTime}"
|
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||||
AutomationProperties.IsInAccessibleTree="False" />
|
AutomationProperties.IsInAccessibleTree="False" />
|
||||||
|
|
||||||
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center" Padding="0, 7">
|
<Label
|
||||||
<Grid.RowDefinitions>
|
LineBreakMode="TailTruncation"
|
||||||
<RowDefinition Height="Auto" />
|
Grid.Column="1"
|
||||||
<RowDefinition Height="Auto" />
|
Grid.Row="0"
|
||||||
</Grid.RowDefinitions>
|
VerticalTextAlignment="Center"
|
||||||
|
VerticalOptions="Fill"
|
||||||
|
StyleClass="list-title, list-title-platform"
|
||||||
|
Text="{Binding Cipher.Name}" />
|
||||||
|
|
||||||
<Grid.ColumnDefinitions>
|
<Label
|
||||||
<ColumnDefinition Width="Auto" />
|
LineBreakMode="TailTruncation"
|
||||||
<ColumnDefinition Width="Auto" />
|
Grid.Column="1"
|
||||||
<ColumnDefinition Width="*" />
|
Grid.Row="1"
|
||||||
</Grid.ColumnDefinitions>
|
VerticalTextAlignment="Center"
|
||||||
|
VerticalOptions="Fill"
|
||||||
|
StyleClass="list-subtitle, list-subtitle-platform"
|
||||||
|
Text="{Binding Cipher.SubTitle}" />
|
||||||
|
|
||||||
<controls:MonoLabel
|
<controls:CircularProgressbarView
|
||||||
LineBreakMode="TailTruncation"
|
Progress="{Binding Progress}"
|
||||||
Grid.Column="0"
|
Grid.Row="0"
|
||||||
Grid.Row="0"
|
Grid.Column="2"
|
||||||
Grid.ColumnSpan="3"
|
Grid.RowSpan="2"
|
||||||
StyleClass="list-title, list-title-platform"
|
HorizontalOptions="Fill"
|
||||||
Text="{Binding TotpCodeFormatted, Mode=OneWay}" />
|
VerticalOptions="CenterAndExpand" />
|
||||||
<Label
|
|
||||||
LineBreakMode="TailTruncation"
|
|
||||||
Grid.Column="0"
|
|
||||||
Grid.Row="1"
|
|
||||||
StyleClass="list-subtitle, list-subtitle-platform"
|
|
||||||
Text="{Binding Cipher.Name}" />
|
|
||||||
<controls:IconLabel
|
|
||||||
Grid.Column="1"
|
|
||||||
Grid.Row="1"
|
|
||||||
HorizontalOptions="Start"
|
|
||||||
VerticalOptions="Center"
|
|
||||||
StyleClass="list-title-icon"
|
|
||||||
Margin="5, 0, 0, 0"
|
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Collection}}"
|
|
||||||
IsVisible="{Binding Cipher.Shared, Mode=OneTime}"
|
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
|
||||||
AutomationProperties.Name="{u:I18n Shared}" />
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
Text="{Binding TotpSec, Mode=OneWay}"
|
Text="{Binding TotpSec, Mode=OneWay}"
|
||||||
Style="{DynamicResource textTotp}"
|
Style="{DynamicResource textTotp}"
|
||||||
Margin="0, 0, 10, 0"
|
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="2"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
HorizontalOptions="End"
|
StyleClass="text-sm"
|
||||||
HorizontalTextAlignment="End"
|
HorizontalTextAlignment="Center"
|
||||||
VerticalOptions="CenterAndExpand" />
|
HorizontalOptions="Fill"
|
||||||
<controls:IconButton
|
VerticalTextAlignment="Center"
|
||||||
|
VerticalOptions="Fill" />
|
||||||
|
|
||||||
|
<StackLayout
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="3"
|
||||||
|
Margin="3,0,2,0"
|
||||||
|
Spacing="5"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalOptions="Fill"
|
||||||
|
VerticalOptions="Fill">
|
||||||
|
|
||||||
|
<controls:MonoLabel
|
||||||
|
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
|
||||||
|
Style="{DynamicResource textTotp}"
|
||||||
|
StyleClass="text-lg"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="FillAndExpand" />
|
||||||
|
|
||||||
|
<controls:MonoLabel
|
||||||
|
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
|
||||||
|
Style="{DynamicResource textTotp}"
|
||||||
|
StyleClass="text-lg"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
VerticalTextAlignment="Center"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="FillAndExpand" />
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<controls:IconButton
|
||||||
StyleClass="box-row-button, box-row-button-platform"
|
StyleClass="box-row-button, box-row-button-platform"
|
||||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||||
Command="{Binding CopyCommand}"
|
Command="{Binding CopyCommand}"
|
||||||
CommandParameter="LoginTotp"
|
CommandParameter="LoginTotp"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="2"
|
Grid.Column="4"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
Padding="0,0,1,0"
|
Padding="0,0,1,0"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
VerticalOptions="Center"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||||
</controls:ExtendedGrid>
|
</controls:ExtendedGrid>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Bit.App.Pages;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@@ -17,9 +18,6 @@ namespace Bit.App.Controls
|
|||||||
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
|
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
|
||||||
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
|
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
|
||||||
|
|
||||||
//public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
|
|
||||||
// nameof(ButtonCommand), typeof(Command<CipherView>), typeof(AuthenticatorViewCell));
|
|
||||||
|
|
||||||
public AuthenticatorViewCell()
|
public AuthenticatorViewCell()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -65,57 +63,5 @@ namespace Bit.App.Controls
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _totpCodeFormatted = "938 928";
|
|
||||||
public string TotpCodeFormatted
|
|
||||||
{
|
|
||||||
get => _totpCodeFormatted;
|
|
||||||
set => _totpCodeFormatted = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//public Command<CipherView> ButtonCommand
|
|
||||||
//{
|
|
||||||
// get => GetValue(ButtonCommandProperty) as Command<CipherView>;
|
|
||||||
// set => SetValue(ButtonCommandProperty, value);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//protected override void OnPropertyChanged(string propertyName = null)
|
|
||||||
//{
|
|
||||||
// base.OnPropertyChanged(propertyName);
|
|
||||||
// if (propertyName == CipherProperty.PropertyName)
|
|
||||||
// {
|
|
||||||
// if (Cipher == null)
|
|
||||||
// {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// _cipherLabel.Text = Cipher.Name;
|
|
||||||
// }
|
|
||||||
// else if (propertyName == WebsiteIconsEnabledProperty.PropertyName)
|
|
||||||
// {
|
|
||||||
// if (Cipher == null)
|
|
||||||
// {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// ((AuthenticatorViewCellViewModel)BindingContext).WebsiteIconsEnabled = WebsiteIconsEnabled ?? false;
|
|
||||||
// }
|
|
||||||
// else if (propertyName == TotpSecProperty.PropertyName)
|
|
||||||
// {
|
|
||||||
// if (Cipher == null)
|
|
||||||
// {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// ((AuthenticatorViewCellViewModel)BindingContext).UpdateTotpSec(TotpSec);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
private void MoreButton_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var cipher = ((sender as MiButton)?.BindingContext as AuthenticatorViewCellViewModel)?.Cipher;
|
|
||||||
if (cipher != null)
|
|
||||||
{
|
|
||||||
//ButtonCommand?.Execute(cipher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.App.Utilities;
|
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
|
||||||
{
|
|
||||||
public class AuthenticatorViewCellViewModel : ExtendedViewModel
|
|
||||||
{
|
|
||||||
private CipherView _cipher;
|
|
||||||
private string _totpCodeFormatted = "938 928";
|
|
||||||
private string _totpSec;
|
|
||||||
private bool _websiteIconsEnabled;
|
|
||||||
private string _iconImageSource = string.Empty;
|
|
||||||
|
|
||||||
public AuthenticatorViewCellViewModel(CipherView cipherView, bool websiteIconsEnabled)
|
|
||||||
{
|
|
||||||
Cipher = cipherView;
|
|
||||||
WebsiteIconsEnabled = websiteIconsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Command CopyCommand { get; set; }
|
|
||||||
|
|
||||||
public CipherView Cipher
|
|
||||||
{
|
|
||||||
get => _cipher;
|
|
||||||
set => SetProperty(ref _cipher, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string TotpCodeFormatted
|
|
||||||
{
|
|
||||||
get => _totpCodeFormatted;
|
|
||||||
set => SetProperty(ref _totpCodeFormatted, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string TotpSec
|
|
||||||
{
|
|
||||||
get => _totpSec;
|
|
||||||
set => SetProperty(ref _totpSec, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool WebsiteIconsEnabled
|
|
||||||
{
|
|
||||||
get => _websiteIconsEnabled;
|
|
||||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowIconImage
|
|
||||||
{
|
|
||||||
get => WebsiteIconsEnabled
|
|
||||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
|
||||||
&& IconImageSource != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string IconImageSource
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
|
||||||
{
|
|
||||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
|
||||||
}
|
|
||||||
return _iconImageSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateTotpSec(long totpSec)
|
|
||||||
{
|
|
||||||
_totpSec = totpSec.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
//private async Task TotpUpdateCodeAsync()
|
|
||||||
//{
|
|
||||||
// if (Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null)
|
|
||||||
// {
|
|
||||||
// _totpInterval = null;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// _totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp);
|
|
||||||
// if (_totpCode != null)
|
|
||||||
// {
|
|
||||||
// if (_totpCode.Length > 4)
|
|
||||||
// {
|
|
||||||
// var half = (int)Math.Floor(_totpCode.Length / 2M);
|
|
||||||
// TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half),
|
|
||||||
// _totpCode.Substring(half));
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// TotpCodeFormatted = _totpCode;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// TotpCodeFormatted = null;
|
|
||||||
// _totpInterval = null;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -9,7 +11,8 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public class AvatarImageSource : StreamImageSource
|
public class AvatarImageSource : StreamImageSource
|
||||||
{
|
{
|
||||||
private string _data;
|
private readonly string _text;
|
||||||
|
private readonly string _id;
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
{
|
{
|
||||||
@@ -20,20 +23,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
_data = name;
|
_id = userId;
|
||||||
if (string.IsNullOrWhiteSpace(_data))
|
_text = name;
|
||||||
|
if (string.IsNullOrWhiteSpace(_text))
|
||||||
{
|
{
|
||||||
_data = email;
|
_text = email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,83 +54,104 @@ namespace Bit.App.Controls
|
|||||||
|
|
||||||
private Stream Draw()
|
private Stream Draw()
|
||||||
{
|
{
|
||||||
string chars = null;
|
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 = CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
|
||||||
var textColor = Color.White;
|
var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
|
||||||
var size = 50;
|
var size = 50;
|
||||||
|
|
||||||
var bitmap = new SKBitmap(
|
using (var bitmap = new SKBitmap(size * 2,
|
||||||
size * 2,
|
|
||||||
size * 2,
|
size * 2,
|
||||||
SKImageInfo.PlatformColorType,
|
SKImageInfo.PlatformColorType,
|
||||||
SKAlphaType.Premul);
|
SKAlphaType.Premul))
|
||||||
var canvas = new SKCanvas(bitmap);
|
|
||||||
canvas.Clear(SKColors.Transparent);
|
|
||||||
|
|
||||||
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
|
||||||
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
|
|
||||||
var radius = midX - midX / 5;
|
|
||||||
|
|
||||||
var circlePaint = new SKPaint
|
|
||||||
{
|
{
|
||||||
IsAntialias = true,
|
using (var canvas = new SKCanvas(bitmap))
|
||||||
Style = SKPaintStyle.Fill,
|
{
|
||||||
StrokeJoin = SKStrokeJoin.Miter,
|
canvas.Clear(SKColors.Transparent);
|
||||||
Color = SKColor.Parse(bgColor.ToHex())
|
using (var paint = new SKPaint
|
||||||
};
|
{
|
||||||
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
IsAntialias = true,
|
||||||
|
Style = SKPaintStyle.Fill,
|
||||||
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
|
Color = SKColor.Parse(bgColor)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
|
||||||
|
var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2;
|
||||||
|
var radius = midX - midX / 5;
|
||||||
|
|
||||||
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
|
using (var circlePaint = new SKPaint
|
||||||
var textSize = midX / 1.3f;
|
{
|
||||||
var textPaint = new SKPaint
|
IsAntialias = true,
|
||||||
{
|
Style = SKPaintStyle.Fill,
|
||||||
IsAntialias = true,
|
StrokeJoin = SKStrokeJoin.Miter,
|
||||||
Style = SKPaintStyle.Fill,
|
Color = SKColor.Parse(bgColor)
|
||||||
Color = SKColor.Parse(textColor.ToHex()),
|
})
|
||||||
TextSize = textSize,
|
{
|
||||||
TextAlign = SKTextAlign.Center,
|
canvas.DrawCircle(midX, midY, radius, circlePaint);
|
||||||
Typeface = typeface
|
|
||||||
};
|
|
||||||
var rect = new SKRect();
|
|
||||||
textPaint.MeasureText(chars, ref rect);
|
|
||||||
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
|
|
||||||
|
|
||||||
return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream();
|
var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal);
|
||||||
|
var textSize = midX / 1.3f;
|
||||||
|
using (var textPaint = new SKPaint
|
||||||
|
{
|
||||||
|
IsAntialias = true,
|
||||||
|
Style = SKPaintStyle.Fill,
|
||||||
|
Color = SKColor.Parse(textColor),
|
||||||
|
TextSize = textSize,
|
||||||
|
TextAlign = SKTextAlign.Center,
|
||||||
|
Typeface = typeface
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var rect = new SKRect();
|
||||||
|
textPaint.MeasureText(chars, ref rect);
|
||||||
|
canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint);
|
||||||
|
|
||||||
|
using (var img = SKImage.FromBitmap(bitmap))
|
||||||
|
{
|
||||||
|
var data = img.Encode(SKEncodedImageFormat.Png, 100);
|
||||||
|
return data?.AsStream(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFirstLetters(string data, int charCount)
|
private string GetFirstLetters(string data, int charCount)
|
||||||
{
|
{
|
||||||
var parts = data.Split();
|
var sanitizedData = data.Trim();
|
||||||
|
var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
if (parts.Length > 1 && charCount <= 2)
|
if (parts.Length > 1 && charCount <= 2)
|
||||||
{
|
{
|
||||||
var text = "";
|
var text = string.Empty;
|
||||||
for (int i = 0; i < charCount; i++)
|
for (var i = 0; i < charCount; i++)
|
||||||
{
|
{
|
||||||
text += parts[i].Substring(0, 1);
|
text += parts[i][0];
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
if (data.Length > 2)
|
if (sanitizedData.Length > 2)
|
||||||
{
|
{
|
||||||
return data.Substring(0, 2);
|
return sanitizedData.Substring(0, 2);
|
||||||
}
|
}
|
||||||
return data;
|
return sanitizedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color StringToColor(string str)
|
private Color StringToColor(string str)
|
||||||
|
|||||||
33
src/App/Controls/AvatarImageSourcePool.cs
Normal file
33
src/App/Controls/AvatarImageSourcePool.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public interface IAvatarImageSourcePool
|
||||||
|
{
|
||||||
|
AvatarImageSource GetOrCreateAvatar(string userId, string name, string email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AvatarImageSourcePool : IAvatarImageSourcePool
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
|
||||||
|
|
||||||
|
public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email)
|
||||||
|
{
|
||||||
|
var key = $"{userId}{name}{email}";
|
||||||
|
if (!_cache.TryGetValue(key, out var avatar))
|
||||||
|
{
|
||||||
|
avatar = new AvatarImageSource(userId, name, email);
|
||||||
|
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.
|
||||||
|
{
|
||||||
|
// if add and get after fails, then something wrong is going on with this method.
|
||||||
|
throw new InvalidOperationException("Something is wrong creating the avatar image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
139
src/App/Controls/CircularProgressbarView.cs
Normal file
139
src/App/Controls/CircularProgressbarView.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.Views.Forms;
|
||||||
|
using Xamarin.Essentials;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class CircularProgressbarView : SKCanvasView
|
||||||
|
{
|
||||||
|
private Circle _circle;
|
||||||
|
|
||||||
|
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
|
||||||
|
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
|
||||||
|
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
|
||||||
|
|
||||||
|
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
|
||||||
|
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
|
||||||
|
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||||
|
|
||||||
|
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
|
||||||
|
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||||
|
|
||||||
|
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
|
||||||
|
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
|
||||||
|
|
||||||
|
public double Progress
|
||||||
|
{
|
||||||
|
get { return (double)GetValue(ProgressProperty); }
|
||||||
|
set { SetValue(ProgressProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Radius
|
||||||
|
{
|
||||||
|
get => (float)GetValue(RadiusProperty);
|
||||||
|
set => SetValue(RadiusProperty, value);
|
||||||
|
}
|
||||||
|
public float StrokeWidth
|
||||||
|
{
|
||||||
|
get => (float)GetValue(StrokeWidthProperty);
|
||||||
|
set => SetValue(StrokeWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color ProgressColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(ProgressColorProperty);
|
||||||
|
set => SetValue(ProgressColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color EndingProgressColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(EndingProgressColorProperty);
|
||||||
|
set => SetValue(EndingProgressColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color BackgroundProgressColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(BackgroundProgressColorProperty);
|
||||||
|
set => SetValue(BackgroundProgressColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
|
||||||
|
{
|
||||||
|
var context = bindable as CircularProgressbarView;
|
||||||
|
context.InvalidateSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
if (propertyName == nameof(Progress))
|
||||||
|
{
|
||||||
|
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaintSurface(e);
|
||||||
|
if (_circle != null)
|
||||||
|
{
|
||||||
|
_circle.CalculateCenter(e.Info);
|
||||||
|
e.Surface.Canvas.Clear();
|
||||||
|
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
|
||||||
|
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
|
||||||
|
{
|
||||||
|
canvas.DrawCircle(circle.Center, circle.Redius,
|
||||||
|
new SKPaint()
|
||||||
|
{
|
||||||
|
StrokeWidth = strokewidth,
|
||||||
|
Color = color,
|
||||||
|
IsStroke = true,
|
||||||
|
IsAntialias = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
|
||||||
|
{
|
||||||
|
var progressValue = progress();
|
||||||
|
var angle = progressValue * 3.6f;
|
||||||
|
canvas.DrawArc(circle.Rect, 270, angle, false,
|
||||||
|
new SKPaint()
|
||||||
|
{
|
||||||
|
StrokeWidth = strokewidth,
|
||||||
|
Color = progressValue < 20f ? progressEndColor : color,
|
||||||
|
IsStroke = true,
|
||||||
|
IsAntialias = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Circle
|
||||||
|
{
|
||||||
|
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
|
||||||
|
|
||||||
|
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
|
||||||
|
{
|
||||||
|
_centerFunc = centerFunc;
|
||||||
|
Redius = redius;
|
||||||
|
}
|
||||||
|
public SKPoint Center { get; set; }
|
||||||
|
public float Redius { get; set; }
|
||||||
|
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
|
||||||
|
|
||||||
|
public void CalculateCenter(SKImageInfo argsInfo)
|
||||||
|
{
|
||||||
|
Center = _centerFunc(argsInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/App/Controls/DateTime/DateTimePicker.xaml
Normal file
20
src/App/Controls/DateTime/DateTimePicker.xaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<Grid
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
x:Class="Bit.App.Controls.DateTimePicker"
|
||||||
|
ColumnDefinitions="*,*">
|
||||||
|
<controls:ExtendedDatePicker
|
||||||
|
x:Name="_datePicker"
|
||||||
|
Grid.Column="0"
|
||||||
|
NullableDate="{Binding Date, Mode=TwoWay}"
|
||||||
|
Format="d"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True" />
|
||||||
|
<controls:ExtendedTimePicker
|
||||||
|
x:Name="_timePicker"
|
||||||
|
Grid.Column="1"
|
||||||
|
NullableTime="{Binding Time, Mode=TwoWay}"
|
||||||
|
Format="t"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True" />
|
||||||
|
</Grid>
|
||||||
34
src/App/Controls/DateTime/DateTimePicker.xaml.cs
Normal file
34
src/App/Controls/DateTime/DateTimePicker.xaml.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Xamarin.CommunityToolkit.UI.Views;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public partial class DateTimePicker : Grid
|
||||||
|
{
|
||||||
|
public DateTimePicker()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(propertyName);
|
||||||
|
|
||||||
|
if (propertyName == nameof(BindingContext)
|
||||||
|
&&
|
||||||
|
BindingContext is DateTimeViewModel dateTimeViewModel)
|
||||||
|
{
|
||||||
|
AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName);
|
||||||
|
AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName);
|
||||||
|
|
||||||
|
_datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder;
|
||||||
|
_timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LazyDateTimePicker : LazyView<DateTimePicker>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/App/Controls/DateTime/DateTimeViewModel.cs
Normal file
70
src/App/Controls/DateTime/DateTimeViewModel.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class DateTimeViewModel : ExtendedViewModel
|
||||||
|
{
|
||||||
|
DateTime? _date;
|
||||||
|
TimeSpan? _time;
|
||||||
|
|
||||||
|
public DateTimeViewModel(string dateName, string timeName)
|
||||||
|
{
|
||||||
|
DateName = dateName;
|
||||||
|
TimeName = timeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<DateTime?> OnDateChanged { get; set; }
|
||||||
|
public Action<TimeSpan?> OnTimeChanged { get; set; }
|
||||||
|
|
||||||
|
public DateTime? Date
|
||||||
|
{
|
||||||
|
get => _date;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _date, value))
|
||||||
|
{
|
||||||
|
OnDateChanged?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public TimeSpan? Time
|
||||||
|
{
|
||||||
|
get => _time;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _time, value))
|
||||||
|
{
|
||||||
|
OnTimeChanged?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DateName { get; }
|
||||||
|
public string TimeName { get; }
|
||||||
|
|
||||||
|
public string DatePlaceholder { get; set; }
|
||||||
|
public string TimePlaceholder { get; set; }
|
||||||
|
|
||||||
|
public DateTime? DateTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Date.HasValue)
|
||||||
|
{
|
||||||
|
if (Time.HasValue)
|
||||||
|
{
|
||||||
|
return Date.Value.Add(Time.Value);
|
||||||
|
}
|
||||||
|
return Date;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Date = value?.Date;
|
||||||
|
Time = value?.Date.TimeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,5 +4,6 @@ namespace Bit.App.Controls
|
|||||||
{
|
{
|
||||||
public class ExtendedCollectionView : CollectionView
|
public class ExtendedCollectionView : CollectionView
|
||||||
{
|
{
|
||||||
|
public string ExtraDataForLogging { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
12
src/App/Effects/NoEmojiKeyboardEffect.cs
Normal file
12
src/App/Effects/NoEmojiKeyboardEffect.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Effects
|
||||||
|
{
|
||||||
|
public class NoEmojiKeyboardEffect : RoutingEffect
|
||||||
|
{
|
||||||
|
public NoEmojiKeyboardEffect()
|
||||||
|
: base("Bitwarden.NoEmojiKeyboardEffect")
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public class BooleanCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
||||||
|
{
|
||||||
|
public BooleanCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand)
|
||||||
|
: base(field, isEditing, fieldOptionsCommand)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BooleanValue
|
||||||
|
{
|
||||||
|
get => bool.TryParse(Field.Value, out var boolVal) && boolVal;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Field.Value = value.ToString().ToLower();
|
||||||
|
TriggerPropertyChanged(nameof(BooleanValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public interface ICustomFieldItemFactory
|
||||||
|
{
|
||||||
|
ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
|
||||||
|
bool isEditing,
|
||||||
|
CipherView cipher,
|
||||||
|
IPasswordPromptable passwordPromptable,
|
||||||
|
ICommand copyFieldCommand,
|
||||||
|
ICommand fieldOptionsCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomFieldItemFactory : ICustomFieldItemFactory
|
||||||
|
{
|
||||||
|
readonly II18nService _i18nService;
|
||||||
|
readonly IEventService _eventService;
|
||||||
|
|
||||||
|
public CustomFieldItemFactory(II18nService i18nService, IEventService eventService)
|
||||||
|
{
|
||||||
|
_i18nService = i18nService;
|
||||||
|
_eventService = eventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICustomFieldItemViewModel CreateCustomFieldItem(FieldView field,
|
||||||
|
bool isEditing,
|
||||||
|
CipherView cipher,
|
||||||
|
IPasswordPromptable passwordPromptable,
|
||||||
|
ICommand copyFieldCommand,
|
||||||
|
ICommand fieldOptionsCommand)
|
||||||
|
{
|
||||||
|
switch (field.Type)
|
||||||
|
{
|
||||||
|
case FieldType.Text:
|
||||||
|
return new TextCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, copyFieldCommand);
|
||||||
|
case FieldType.Boolean:
|
||||||
|
return new BooleanCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand);
|
||||||
|
case FieldType.Hidden:
|
||||||
|
return new HiddenCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, passwordPromptable, _eventService, copyFieldCommand);
|
||||||
|
case FieldType.Linked:
|
||||||
|
return new LinkedCustomFieldItemViewModel(field, isEditing, fieldOptionsCommand, cipher, _i18nService);
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException("There is no custom field item for field type " + field.Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public class HiddenCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
||||||
|
{
|
||||||
|
private readonly CipherView _cipher;
|
||||||
|
private readonly IPasswordPromptable _passwordPromptable;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private bool _showHiddenValue;
|
||||||
|
|
||||||
|
public HiddenCustomFieldItemViewModel(FieldView field,
|
||||||
|
bool isEditing,
|
||||||
|
ICommand fieldOptionsCommand,
|
||||||
|
CipherView cipher,
|
||||||
|
IPasswordPromptable passwordPromptable,
|
||||||
|
IEventService eventService,
|
||||||
|
ICommand copyFieldCommand)
|
||||||
|
: base(field, isEditing, fieldOptionsCommand)
|
||||||
|
{
|
||||||
|
_cipher = cipher;
|
||||||
|
_passwordPromptable = passwordPromptable;
|
||||||
|
_eventService = eventService;
|
||||||
|
|
||||||
|
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
|
||||||
|
ToggleHiddenValueCommand = new AsyncCommand(ToggleHiddenValueAsync, (Func<bool>)null, ex =>
|
||||||
|
{
|
||||||
|
#if !FDROID
|
||||||
|
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand CopyFieldCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ToggleHiddenValueCommand { get; set; }
|
||||||
|
|
||||||
|
public bool ShowHiddenValue
|
||||||
|
{
|
||||||
|
get => _showHiddenValue;
|
||||||
|
set => SetProperty(ref _showHiddenValue, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowViewHidden => _cipher.ViewPassword || (_isEditing && _field.NewField);
|
||||||
|
|
||||||
|
public override bool ShowCopyButton => !_isEditing && _cipher.ViewPassword && !string.IsNullOrWhiteSpace(Field.Value);
|
||||||
|
|
||||||
|
public async Task ToggleHiddenValueAsync()
|
||||||
|
{
|
||||||
|
if (!_isEditing && !await _passwordPromptable.PromptPasswordAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowHiddenValue = !ShowHiddenValue;
|
||||||
|
if (ShowHiddenValue && (!_isEditing || _cipher?.Id != null))
|
||||||
|
{
|
||||||
|
await _eventService.CollectAsync(
|
||||||
|
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public interface ICustomFieldItemViewModel
|
||||||
|
{
|
||||||
|
FieldView Field { get; set; }
|
||||||
|
|
||||||
|
bool ShowCopyButton { get; }
|
||||||
|
|
||||||
|
void TriggerFieldChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public class LinkedCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
||||||
|
{
|
||||||
|
private readonly CipherView _cipher;
|
||||||
|
private readonly II18nService _i18nService;
|
||||||
|
private int _linkedFieldOptionSelectedIndex;
|
||||||
|
|
||||||
|
public LinkedCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, CipherView cipher, II18nService i18nService)
|
||||||
|
: base(field, isEditing, fieldOptionsCommand)
|
||||||
|
{
|
||||||
|
_cipher = cipher;
|
||||||
|
_i18nService = i18nService;
|
||||||
|
|
||||||
|
LinkedFieldOptionSelectedIndex = Field.LinkedId.HasValue
|
||||||
|
? LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (isEditing && Field.LinkedId is null)
|
||||||
|
{
|
||||||
|
field.LinkedId = LinkedFieldOptions[0].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ValueText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
|
||||||
|
return $"{BitwardenIcons.Link} {_i18nService.T(i18nKey)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int LinkedFieldOptionSelectedIndex
|
||||||
|
{
|
||||||
|
get => _linkedFieldOptionSelectedIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _linkedFieldOptionSelectedIndex, value))
|
||||||
|
{
|
||||||
|
LinkedFieldValueChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions
|
||||||
|
{
|
||||||
|
get => _cipher.LinkedFieldOptions
|
||||||
|
.Select(kvp => new KeyValuePair<string, LinkedIdType>(_i18nService.T(kvp.Key), kvp.Value))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LinkedFieldValueChanged()
|
||||||
|
{
|
||||||
|
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
|
||||||
|
{
|
||||||
|
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
|
||||||
|
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Lists.ItemViewModels.CustomFields
|
||||||
|
{
|
||||||
|
public class TextCustomFieldItemViewModel : BaseCustomFieldItemViewModel
|
||||||
|
{
|
||||||
|
public TextCustomFieldItemViewModel(FieldView field, bool isEditing, ICommand fieldOptionsCommand, ICommand copyFieldCommand)
|
||||||
|
: base(field, isEditing, fieldOptionsCommand)
|
||||||
|
{
|
||||||
|
CopyFieldCommand = new Command(() => copyFieldCommand?.Execute(Field));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool ShowCopyButton => !_isEditing && !string.IsNullOrWhiteSpace(Field.Value);
|
||||||
|
|
||||||
|
public ICommand CopyFieldCommand { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/App/Models/NotificationData.cs
Normal file
23
src/App/Models/NotificationData.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
namespace Bit.App.Models
|
||||||
|
{
|
||||||
|
public abstract class BaseNotificationData
|
||||||
|
{
|
||||||
|
public abstract string Type { get; }
|
||||||
|
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PasswordlessNotificationData : BaseNotificationData
|
||||||
|
{
|
||||||
|
public const string TYPE = "passwordlessNotificationData";
|
||||||
|
|
||||||
|
public override string Type => TYPE;
|
||||||
|
|
||||||
|
public int TimeoutInMinutes { get; set; }
|
||||||
|
|
||||||
|
public string UserEmail { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
|
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitCommand}" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ namespace Bit.App.Pages
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Submit_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
await _vm.SubmitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SubmitSuccessAsync()
|
private async Task SubmitSuccessAsync()
|
||||||
{
|
{
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class EnvironmentPageViewModel : BaseViewModel
|
public class EnvironmentPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IEnvironmentService _environmentService;
|
private readonly IEnvironmentService _environmentService;
|
||||||
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
|
||||||
public EnvironmentPageViewModel()
|
public EnvironmentPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -22,10 +24,10 @@ namespace Bit.App.Pages
|
|||||||
IdentityUrl = _environmentService.IdentityUrl;
|
IdentityUrl = _environmentService.IdentityUrl;
|
||||||
IconsUrl = _environmentService.IconsUrl;
|
IconsUrl = _environmentService.IconsUrl;
|
||||||
NotificationsUrls = _environmentService.NotificationsUrl;
|
NotificationsUrls = _environmentService.NotificationsUrl;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
public string BaseUrl { get; set; }
|
public string BaseUrl { get; set; }
|
||||||
public string ApiUrl { get; set; }
|
public string ApiUrl { get; set; }
|
||||||
public string IdentityUrl { get; set; }
|
public string IdentityUrl { get; set; }
|
||||||
@@ -37,6 +39,12 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
{
|
{
|
||||||
|
if (!ValidateUrls())
|
||||||
|
{
|
||||||
|
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||||
{
|
{
|
||||||
Base = BaseUrl,
|
Base = BaseUrl,
|
||||||
@@ -57,5 +65,25 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
SubmitSuccessAction?.Invoke();
|
SubmitSuccessAction?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ValidateUrls()
|
||||||
|
{
|
||||||
|
bool IsUrlValid(string url)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsUrlValid(BaseUrl)
|
||||||
|
&& IsUrlValid(ApiUrl)
|
||||||
|
&& IsUrlValid(IdentityUrl)
|
||||||
|
&& IsUrlValid(WebVaultUrl)
|
||||||
|
&& IsUrlValid(IconsUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSubmitException(Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
<ToolbarItem Text="{u:I18n Submit}" Command="{Binding SubmitCommand}" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -24,14 +23,6 @@ namespace Bit.App.Pages
|
|||||||
RequestFocus(_email);
|
RequestFocus(_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Submit_Clicked(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (DoOnce())
|
|
||||||
{
|
|
||||||
await _vm.SubmitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
@@ -13,18 +14,26 @@ namespace Bit.App.Pages
|
|||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IApiService _apiService;
|
private readonly IApiService _apiService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public HintPageViewModel()
|
public HintPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
|
||||||
PageTitle = AppResources.PasswordHint;
|
PageTitle = AppResources.PasswordHint;
|
||||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
SubmitCommand = new AsyncCommand(SubmitAsync,
|
||||||
|
onException: ex =>
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
_deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||||
|
},
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command SubmitCommand { get; }
|
public ICommand SubmitCommand { get; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
public async Task SubmitAsync()
|
public async Task SubmitAsync()
|
||||||
@@ -37,14 +46,14 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(Email))
|
if (string.IsNullOrWhiteSpace(Email))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred,
|
||||||
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
string.Format(AppResources.ValidationFieldRequired, AppResources.EmailAddress),
|
||||||
AppResources.Ok);
|
AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Email.Contains("@"))
|
if (!Email.Contains("@"))
|
||||||
{
|
{
|
||||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
await _deviceActionService.DisplayAlertAsync(AppResources.AnErrorHasOccurred, AppResources.InvalidEmail, AppResources.Ok);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +63,7 @@ namespace Bit.App.Pages
|
|||||||
await _apiService.PostPasswordHintAsync(
|
await _apiService.PostPasswordHintAsync(
|
||||||
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
new Core.Models.Request.PasswordHintRequest { Email = Email });
|
||||||
await _deviceActionService.HideLoadingAsync();
|
await _deviceActionService.HideLoadingAsync();
|
||||||
await Page.DisplayAlert(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
await _deviceActionService.DisplayAlertAsync(null, AppResources.PasswordHintAlert, AppResources.Ok);
|
||||||
await Page.Navigation.PopModalAsync();
|
await Page.Navigation.PopModalAsync();
|
||||||
}
|
}
|
||||||
catch (ApiException e)
|
catch (ApiException e)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||||
}
|
}
|
||||||
_broadcasterService.Subscribe(nameof(HomePage), async (message) =>
|
_broadcasterService.Subscribe(nameof(HomePage), (message) =>
|
||||||
{
|
{
|
||||||
if (message.Command == "updatedTheme")
|
if (message.Command == "updatedTheme")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ namespace Bit.App.Pages
|
|||||||
_vm = BindingContext as LockPageViewModel;
|
_vm = BindingContext as LockPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
_vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync());
|
||||||
MasterPasswordEntry = _masterPassword;
|
|
||||||
PinEntry = _pin;
|
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
{
|
{
|
||||||
@@ -38,8 +36,17 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry MasterPasswordEntry { get; set; }
|
public Entry SecretEntry
|
||||||
public Entry PinEntry { get; set; }
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_vm?.PinLock ?? false)
|
||||||
|
{
|
||||||
|
return _pin;
|
||||||
|
}
|
||||||
|
return _masterPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAfterResumeAsync()
|
public async Task PromptBiometricAfterResumeAsync()
|
||||||
{
|
{
|
||||||
@@ -70,16 +77,12 @@ namespace Bit.App.Pages
|
|||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||||
|
|
||||||
await _vm.InitAsync();
|
await _vm.InitAsync();
|
||||||
|
|
||||||
|
_vm.FocusSecretEntry += PerformFocusSecretEntry;
|
||||||
|
|
||||||
if (!_vm.BiometricLock)
|
if (!_vm.BiometricLock)
|
||||||
{
|
{
|
||||||
if (_vm.PinLock)
|
RequestFocus(SecretEntry);
|
||||||
{
|
|
||||||
RequestFocus(PinEntry);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RequestFocus(MasterPasswordEntry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -99,6 +102,18 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PerformFocusSecretEntry(int? cursorPosition)
|
||||||
|
{
|
||||||
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
SecretEntry.Focus();
|
||||||
|
if (cursorPosition.HasValue)
|
||||||
|
{
|
||||||
|
SecretEntry.CursorPosition = cursorPosition.Value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool OnBackButtonPressed()
|
protected override bool OnBackButtonPressed()
|
||||||
{
|
{
|
||||||
if (_accountListOverlay.IsVisible)
|
if (_accountListOverlay.IsVisible)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.Helpers;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -27,6 +28,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IBiometricService _biometricService;
|
private readonly IBiometricService _biometricService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly WeakEventManager<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
||||||
|
|
||||||
private string _email;
|
private string _email;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
@@ -133,6 +135,11 @@ namespace Bit.App.Pages
|
|||||||
public string MasterPassword { get; set; }
|
public string MasterPassword { get; set; }
|
||||||
public string Pin { get; set; }
|
public string Pin { get; set; }
|
||||||
public Action UnlockedAction { get; set; }
|
public Action UnlockedAction { get; set; }
|
||||||
|
public event Action<int?> FocusSecretEntry
|
||||||
|
{
|
||||||
|
add => _secretEntryFocusWeakEventManager.AddEventHandler(value);
|
||||||
|
remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
@@ -346,11 +353,8 @@ namespace Bit.App.Pages
|
|||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
var page = (Page as LockPage);
|
var secret = PinLock ? Pin : MasterPassword;
|
||||||
var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry;
|
_secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry));
|
||||||
var str = PinLock ? Pin : MasterPassword;
|
|
||||||
entry.Focus();
|
|
||||||
entry.CursorPosition = String.IsNullOrEmpty(str) ? 0 : str.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PromptBiometricAsync()
|
public async Task PromptBiometricAsync()
|
||||||
@@ -361,18 +365,8 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
PinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
{
|
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)));
|
||||||
var page = Page as LockPage;
|
|
||||||
if (PinLock)
|
|
||||||
{
|
|
||||||
page.PinEntry.Focus();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
page.MasterPasswordEntry.Focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<Entry
|
<Entry
|
||||||
x:Name="_email"
|
x:Name="_email"
|
||||||
Text="{Binding Email}"
|
Text="{Binding Email}"
|
||||||
|
IsEnabled="{Binding IsEmailEnabled}"
|
||||||
Keyboard="Email"
|
Keyboard="Email"
|
||||||
StyleClass="box-value">
|
StyleClass="box-value">
|
||||||
<VisualStateManager.VisualStateGroups>
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private bool _inputFocused;
|
private bool _inputFocused;
|
||||||
|
|
||||||
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||||
|
|
||||||
public LoginPage(string email = null, AppOptions appOptions = null)
|
public LoginPage(string email = null, AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
@@ -30,11 +32,10 @@ namespace Bit.App.Pages
|
|||||||
await _accountListOverlay.HideAsync();
|
await _accountListOverlay.HideAsync();
|
||||||
await Navigation.PopModalAsync();
|
await Navigation.PopModalAsync();
|
||||||
};
|
};
|
||||||
if (!string.IsNullOrWhiteSpace(email))
|
_vm.IsEmailEnabled = string.IsNullOrWhiteSpace(email);
|
||||||
{
|
_vm.IsIosExtension = _appOptions?.IosExtension ?? false;
|
||||||
_email.IsEnabled = false;
|
|
||||||
}
|
if (_vm.IsEmailEnabled)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_vm.ShowCancelButton = true;
|
_vm.ShowCancelButton = true;
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ namespace Bit.App.Pages
|
|||||||
ToolbarItems.Add(_getPasswordHint);
|
ToolbarItems.Add(_getPasswordHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Device.RuntimePlatform == Device.Android && !_email.IsEnabled)
|
if (Device.RuntimePlatform == Device.Android && !_vm.IsEmailEnabled)
|
||||||
{
|
{
|
||||||
ToolbarItems.Add(_removeAccount);
|
ToolbarItems.Add(_removeAccount);
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
if (DoOnce())
|
if (DoOnce())
|
||||||
{
|
{
|
||||||
await _vm.LogInAsync(true, _email.IsEnabled);
|
await _vm.LogInAsync(true, _vm.IsEmailEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,26 +140,16 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void More_Clicked(object sender, System.EventArgs e)
|
private async void More_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await _accountListOverlay.HideAsync();
|
try
|
||||||
if (!DoOnce())
|
|
||||||
{
|
{
|
||||||
return;
|
await _accountListOverlay.HideAsync();
|
||||||
|
_vm.MoreCommand.Execute(null);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
var buttons = _email.IsEnabled ? new[] { AppResources.GetPasswordHint }
|
|
||||||
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
|
||||||
var selection = await DisplayActionSheet(AppResources.Options,
|
|
||||||
AppResources.Cancel, null, buttons);
|
|
||||||
|
|
||||||
if (selection == AppResources.GetPasswordHint)
|
|
||||||
{
|
{
|
||||||
await Navigation.PushModalAsync(new NavigationPage(new HintPage()));
|
_logger.Value.Exception(ex);
|
||||||
}
|
|
||||||
else if (selection == AppResources.RemoveAccount)
|
|
||||||
{
|
|
||||||
await _vm.RemoveAccountAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
@@ -8,6 +9,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@@ -28,6 +30,7 @@ namespace Bit.App.Pages
|
|||||||
private bool _showCancelButton;
|
private bool _showCancelButton;
|
||||||
private string _email;
|
private string _email;
|
||||||
private string _masterPassword;
|
private string _masterPassword;
|
||||||
|
private bool _isEmailEnabled;
|
||||||
|
|
||||||
public LoginPageViewModel()
|
public LoginPageViewModel()
|
||||||
{
|
{
|
||||||
@@ -44,6 +47,7 @@ namespace Bit.App.Pages
|
|||||||
PageTitle = AppResources.Bitwarden;
|
PageTitle = AppResources.Bitwarden;
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
LogInCommand = new Command(async () => await LogInAsync());
|
LogInCommand = new Command(async () => await LogInAsync());
|
||||||
|
MoreCommand = new AsyncCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
{
|
{
|
||||||
@@ -81,10 +85,19 @@ namespace Bit.App.Pages
|
|||||||
set => SetProperty(ref _masterPassword, value);
|
set => SetProperty(ref _masterPassword, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsEmailEnabled
|
||||||
|
{
|
||||||
|
get => _isEmailEnabled;
|
||||||
|
set => SetProperty(ref _isEmailEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsIosExtension { get; set; }
|
||||||
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
public Command LogInCommand { get; }
|
public Command LogInCommand { get; }
|
||||||
public Command TogglePasswordCommand { get; }
|
public Command TogglePasswordCommand { get; }
|
||||||
|
public ICommand MoreCommand { get; internal set; }
|
||||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public Action StartTwoFactorAction { get; set; }
|
public Action StartTwoFactorAction { get; set; }
|
||||||
@@ -201,6 +214,28 @@ namespace Bit.App.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task MoreAsync()
|
||||||
|
{
|
||||||
|
var buttons = IsEmailEnabled
|
||||||
|
? new[] { AppResources.GetPasswordHint }
|
||||||
|
: new[] { AppResources.GetPasswordHint, AppResources.RemoveAccount };
|
||||||
|
var selection = await _deviceActionService.DisplayActionSheetAsync(AppResources.Options, AppResources.Cancel, null, buttons);
|
||||||
|
|
||||||
|
if (selection == AppResources.GetPasswordHint)
|
||||||
|
{
|
||||||
|
var hintNavigationPage = new NavigationPage(new HintPage());
|
||||||
|
if (IsIosExtension)
|
||||||
|
{
|
||||||
|
ThemeManager.ApplyResourcesTo(hintNavigationPage);
|
||||||
|
}
|
||||||
|
await Page.Navigation.PushModalAsync(hintNavigationPage);
|
||||||
|
}
|
||||||
|
else if (selection == AppResources.RemoveAccount)
|
||||||
|
{
|
||||||
|
await RemoveAccountAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void TogglePassword()
|
public void TogglePassword()
|
||||||
{
|
{
|
||||||
ShowPassword = !ShowPassword;
|
ShowPassword = !ShowPassword;
|
||||||
|
|||||||
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.LoginPasswordlessPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
x:DataType="pages:LoginPasswordlessViewModel"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:LoginPasswordlessViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
|
x:Name="_closeItem" x:Key="closeItem" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
<StackLayout
|
||||||
|
Padding="7, 0, 7, 20">
|
||||||
|
<ScrollView
|
||||||
|
VerticalOptions="FillAndExpand">
|
||||||
|
<StackLayout>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n AreYouTryingToLogIn}"
|
||||||
|
FontSize="Title"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
Margin="0,14,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LogInAttemptByLabel}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n FingerprintPhrase}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<controls:MonoLabel
|
||||||
|
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||||
|
FontSize="Medium"
|
||||||
|
TextColor="{DynamicResource FingerprintPhrase}"
|
||||||
|
Margin="0,0,0,27"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DeviceType}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LoginRequest.DeviceType}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n IpAddress}"
|
||||||
|
IsVisible="{Binding ShowIpAddress}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LoginRequest.IpAddress}"
|
||||||
|
IsVisible="{Binding ShowIpAddress}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Time}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding TimeOfRequestText}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,57"/>
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n ConfirmLogIn}"
|
||||||
|
Command="{Binding AcceptRequestCommand}"
|
||||||
|
Margin="0,0,0,17"
|
||||||
|
StyleClass="btn-primary"/>
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n DenyLogIn}"
|
||||||
|
Command="{Binding RejectRequestCommand}"
|
||||||
|
StyleClass="btn-secundary"/>
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</pages:BaseContentPage>
|
||||||
40
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
40
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class LoginPasswordlessPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private LoginPasswordlessViewModel _vm;
|
||||||
|
|
||||||
|
public LoginPasswordlessPage(LoginPasswordlessDetails loginPasswordlessDetails)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_vm = BindingContext as LoginPasswordlessViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
|
||||||
|
_vm.LoginRequest = loginPasswordlessDetails;
|
||||||
|
|
||||||
|
ToolbarItems.Add(_closeItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
_vm.StartRequestTimeUpdater();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
base.OnDisappearing();
|
||||||
|
_vm.StopRequestTimeUpdater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
175
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
175
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class LoginPasswordlessViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private IDeviceActionService _deviceActionService;
|
||||||
|
private IAuthService _authService;
|
||||||
|
private IPlatformUtilsService _platformUtilsService;
|
||||||
|
private ILogger _logger;
|
||||||
|
private LoginPasswordlessDetails _resquest;
|
||||||
|
private CancellationTokenSource _requestTimeCts;
|
||||||
|
private Task _requestTimeTask;
|
||||||
|
|
||||||
|
private const int REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES = 5;
|
||||||
|
|
||||||
|
public LoginPasswordlessViewModel()
|
||||||
|
{
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
||||||
|
PageTitle = AppResources.LogInRequested;
|
||||||
|
|
||||||
|
AcceptRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(true),
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
RejectRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(false),
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand AcceptRequestCommand { get; }
|
||||||
|
|
||||||
|
public ICommand RejectRequestCommand { get; }
|
||||||
|
|
||||||
|
public string LogInAttemptByLabel => LoginRequest != null ? string.Format(AppResources.LogInAttemptByXOnY, LoginRequest.Email, LoginRequest.Origin) : string.Empty;
|
||||||
|
|
||||||
|
public string TimeOfRequestText => CreateRequestDate(LoginRequest?.RequestDate);
|
||||||
|
|
||||||
|
public bool ShowIpAddress => !string.IsNullOrEmpty(LoginRequest?.IpAddress);
|
||||||
|
|
||||||
|
public LoginPasswordlessDetails LoginRequest
|
||||||
|
{
|
||||||
|
get => _resquest;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _resquest, value, additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(LogInAttemptByLabel),
|
||||||
|
nameof(TimeOfRequestText),
|
||||||
|
nameof(ShowIpAddress),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopRequestTimeUpdater()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_requestTimeCts?.Cancel();
|
||||||
|
_requestTimeCts?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartRequestTimeUpdater()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_requestTimeCts?.Cancel();
|
||||||
|
_requestTimeCts = new CancellationTokenSource();
|
||||||
|
_requestTimeTask = new TimerTask(_logger, UpdateRequestTime, _requestTimeCts).RunPeriodic(TimeSpan.FromMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateRequestTime()
|
||||||
|
{
|
||||||
|
TriggerPropertyChanged(nameof(TimeOfRequestText));
|
||||||
|
if (DateTime.UtcNow > LoginRequest?.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
|
||||||
|
{
|
||||||
|
StopRequestTimeUpdater();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||||
|
await Page.Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PasswordlessLoginAsync(bool approveRequest)
|
||||||
|
{
|
||||||
|
if (LoginRequest.RequestDate.ToUniversalTime().AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
|
||||||
|
await Page.Navigation.PopModalAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
await _authService.PasswordlessLoginAsync(LoginRequest.Id, LoginRequest.PubKey, approveRequest);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await Page.Navigation.PopModalAsync();
|
||||||
|
_platformUtilsService.ShowToast("info", null, approveRequest ? AppResources.LogInAccepted : AppResources.LogInDenied);
|
||||||
|
|
||||||
|
StopRequestTimeUpdater();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateRequestDate(DateTime? requestDate)
|
||||||
|
{
|
||||||
|
if (!requestDate.HasValue)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow < requestDate.Value.ToUniversalTime().AddMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES))
|
||||||
|
{
|
||||||
|
return AppResources.JustNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(AppResources.XMinutesAgo, DateTime.UtcNow.Minute - requestDate.Value.ToUniversalTime().Minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleException(Exception ex)
|
||||||
|
{
|
||||||
|
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
||||||
|
}).FireAndForget();
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoginPasswordlessDetails
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
public string PubKey { get; set; }
|
||||||
|
|
||||||
|
public string Origin { get; set; }
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string FingerprintPhrase { get; set; }
|
||||||
|
|
||||||
|
public DateTime RequestDate { get; set; }
|
||||||
|
|
||||||
|
public string DeviceType { get; set; }
|
||||||
|
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user