1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-16 00:03:22 +00:00

Compare commits

..

72 Commits

Author SHA1 Message Date
Federico Andrés Maccaroni
a8dec31a16 EC-295 Added custom renderer for CollectionView to attach ItemTouchHelper callback to handle swiping natively in Android 2022-10-24 21:01:19 -03:00
Federico Andrés Maccaroni
70ac2194cc Merged master into EC-295-swipe-to-copy-logins 2022-10-21 12:00:47 -03:00
github-actions[bot]
3cb9f37997 Autosync the updated translations (#2146)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2022-10-21 12:18:18 +02:00
aj-rosado
4c2f5c05e5 [PS-1188] Add new Solarized Dark Theme (#2141)
* PS-1188 Add new Solarized Dark Theme
2022-10-19 10:57:01 +01:00
André Bispo
eefc9bd239 [SG-705] Popup when a request for authentication comes in on a logged-in account that is not active (#2135)
* [SG-705] Added pop up to perform account switching if the user receives a login request from another account.

* [SG-705] missing resource designer

* [SG-705] Fixed wrong key for state service variable.

* [SG-705] Fix formatting of account switch alert.

* [SG-705] dotnet format run

* [SG-705] Removed async

* [SG-705] Refactor on App
2022-10-18 17:21:45 +01:00
sneakernuts
d18efdea73 updated version bump + crowdin-pull workflows (#2119)
* updated version bump + crowdin-pull workflows

* updated retrieve secrets step and changed gpg vars to reference kv secrets

* Removed duplicate step

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2022-10-17 15:19:06 +00:00
Michał Chęciński
a72a0ec0ce Fix autobump workflow (#2138) 2022-10-17 11:50:38 +02:00
André Bispo
c7e9f30a9a [SG-703] Login request is not removed after dismissing push notification (#2134)
* [SG-703] Added category to iOS notifications in order to be able to receive dismiss actions

* [SG-703] PR Fix
2022-10-14 17:35:17 +01:00
mp-bw
6c404c8229 Testing removal of Xamarin.Forms dependency from iOS.ShareExtension (#2137) 2022-10-14 15:53:18 +01:00
github-actions[bot]
e5de530c2c Autosync the updated translations (#2133)
Co-authored-by: github-actions <>
2022-10-14 14:00:38 +02:00
github-actions[bot]
3a87378847 Bumped version to 2022.10.1 (#2132)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-13 12:43:48 -07:00
Danielle Flinn
6048a10d6d [PS-1543]Sentence case mobile (#2095)
* transitioned to sentence case
* removed unnecessary punctuation
* other small content improvements
2022-10-13 12:31:03 -07:00
github-actions[bot]
a5ad43b134 Bumped version to 2022.10.0 (#2130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-12 17:42:14 +01:00
André Bispo
539f8fe5b5 [SG-690] Timeout fix (#2129)
[SG-690] Updated server times to utc
2022-10-12 15:55:38 +01:00
André Bispo
569922805f [SG-703] Login request is not removed after dismissing push notification (#2125)
* [SG-703] Handle iOS dismiss notification action. Added core logic to remove passwordless notification from local storage.

* [SG-702] Added broadcast receiver to catch dismiss notfication events on android.

* [SG-703] PR fixes.

* [SG-703] Fix constants namespaces. Lazyloading services on broadcast receiver.

* [SG-703] Change services to use lazy loading

* [SG-703] Change lazy loading to be parameterless.
2022-10-12 15:55:01 +01:00
github-actions[bot]
3972e3de8a Autosync the updated translations (#2118)
Co-authored-by: github-actions <>
2022-10-12 15:14:24 +02:00
Federico Maccaroni
ba677a96aa [EC-519] Refactor Split DeviceActionService (#2081)
* EC-519 Refactored IDeviceActionService to be split into IFileService and IAutofillManager also some cleanups were made

* EC-519 Fix format

* EC-519 Fix merge to use the new AutofillHandler
2022-10-11 18:19:32 -03:00
Federico Maccaroni
d800e9a43e EC-602 Constants namespace fix (#2127) 2022-10-11 16:47:52 -04:00
Federico Maccaroni
2d35a00caa EC-602 added droid constants file with the package name constant (#2126) 2022-10-11 16:08:36 -04:00
Michał Chęciński
dc5698b353 [DEVOPS-1014] Fix version auto bump workflow (#2121)
* Fix autobump

* Fix regex

* Use tag name

* DEVOPS-1014 - Review (#2123)

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2022-10-11 16:27:07 +02:00
André Bispo
abada481b7 [SG-702] Tapping Push Notification does not open the account the request is for (#2112)
* [SG-702] Tap notification now switches accounts if it is a passwordless notification.

* [SG-702] Fix compilation errors

* [SG-702] Fixed iOS notification tap fix

* [SG-702] Notification data model

* [SG-702] Change method signature with object containing properties. PR fixes.
2022-10-07 12:06:57 +01:00
André Bispo
1e5eab0574 [SG-690] DateTime to Utc fix (#2115)
* [SG-690] DateTime to Utc fix

* [SG-690] Removed Utc from server side datetime.
2022-10-04 20:40:28 -04:00
André Bispo
c1101af582 [SG-687] added try catch to cancellation token disposal. (#2114) 2022-10-04 20:25:52 +01:00
André Bispo
1db4c4fc8b SG-687 Request time not updating (#2108)
* [SG-687] request updates text with timer task and expires after 15 mins.

* [SG-697] PR Fixes

* [SG-687] Ran code format

* [SG-687] PR Fixes

* [SG-687] missed constant replacement

* [SG-687] PR fixes
2022-10-03 17:42:33 +01:00
André Bispo
bc949fe87a [SG-691] Login request is not displayed after changing accounts (#2111)
* [SG-691] Added new message to be broadcasted when account is switched to trigger a check for login requests.

* [SG-691] PR fixes
2022-10-03 17:11:38 +01:00
Carlos Gonçalves
a890ee6612 [SG-666][SG-667] Email is not prefilled and username isn't generated automatically (#2109)
* SG-666 SG-667 - Email is now prefilled for plus addressed email username type
* Username is auto generated upon navigation

* SG-666 - Fixed PR comments
* Added missing property initialization
2022-10-03 16:51:22 +01:00
André Bispo
90e0b5dcf0 [SG-690] Login Request does not disappear after 15 minutes (#2106)
* [SG-690] Add timeout of 15 for android notifications. Add condition to not prompt login requests if 15mins have passed. Add constant for timeout time.

* [SG-690] Added dialog on click confirm/deny if the request is expired.

* [SG-690] PR fixes

* [SG-690] PR fixes
2022-09-30 20:44:56 +01:00
mp-bw
9631988fc2 remove duplicate/older community toolkit dependency (#2107) 2022-09-30 15:44:18 -03:00
André Bispo
b5f80da28d [SG-696] Android notification icon blank (#2105)
* [SG-696] Added new icon for android notifications. Changed notification intent flags to use helper.

* [SG-696] PR fix. Min version was already 21.
2022-09-30 14:47:03 +01:00
github-actions[bot]
7e0b943b70 Autosync the updated translations (#2104)
Co-authored-by: github-actions <>
2022-09-30 13:10:59 +02:00
mp-bw
425be32c15 added a11y disclosure prompt for Android (#2102) 2022-09-27 15:03:06 -03:00
André Bispo
f9a32e4abc Passwordless feature branch PR (#2100)
* [SG-471] Passwordless device login screen (#2017)

* [SSG-471] Added UI for the device login request response.

* [SG-471] Added text resources and arguments to Page.

* [SG-471] Added properties to speed up page bindings

* [SG-471] Added mock services. Added Accept/reject command binding, navigation and toast messages.

* [SG-471] fixed code styling with dotnet-format

* [SG-471] Fixed back button placement. PR fixes.

* [SG-471] Added new Origin parameter to the page.

* [SG-471] PR Fixes

* [SG-471] PR fixes

* [SG-471] PR Fix: added FireAndForget.

* [SG-471] Moved fire and forget to run on ui thread task.

* [SG-381] Passwordless - Add setting to Mobile (#2037)

* [SG-381] Added settings option to approve passwordless login request. If user has notifications disabled, prompt to go to settings and enable them.

* [SG-381] Update settings pop up texts.

* [SG-381] Added new method to get notifications state on device settings. Added userId to property saved on device to differentiate value between users.

* [SG-381] Added text for the popup on selection.

* [SG-381] PR Fixes

* [SG-408] Implement passwordless api methods (#2055)

* [SG-408] Update notification model.

* [SG-408] removed duplicated resource

* [SG-408] Added implementation to Api Service of new passwordless methods.

* removed qa endpoints

* [SG-408] Changed auth methods implementation, added method call to viewmodel.

* [SG-408] ran code format

* [SG-408] PR fixes

* [SG-472] Add configuration for new notification type (#2056)

* [SG-472] Added methods to present local notification to the user. Configured new notification type for passwordless logins

* [SG-472] Updated code to new api service changes.

* [SG-472] ran dotnet format

* [SG-472] PR Fixes.

* [SG-472] PR Fixes

* [SG-169] End-to-end testing refactor. (#2073)

* [SG-169] Passwordless demo change requests (#2079)

* [SG-169] End-to-end testing refactor.

* [SG-169] Fixed labels. Changed color of Fingerprint phrase. Waited for app to be in foreground to launch passwordless modal to fix Android issues.

* [SG-169] Anchored buttons to the bottom of the screen.

* [SG-169] Changed device type from enum to string.

* [SG-169] PR fixes

* [SG-169] PR fixes

* [SG-169] Added comment on static variable
2022-09-26 18:27:57 +01:00
André Bispo
2f4cd36595 [SG-671] OTP Menu Screen causes Crash on Android (#2097)
* [SG-671] removed unnecessary calc of otpauth period. protected cal of otpauth from crashing the app if url has a wrong format.

* [SG-671] changed logger

* [SG-671] Refactored GetQueryParams code to used HttpUtility.ParseQueryString.

* [SG-671] refactor and null protection.

* [SG-671] code format

* [SG-671] fixed bug where totp circle countdown was fixed to 30.

* [SG-167] added fallback for uri check. Changed all default totp timers to constant.

* [SG-671] missed unsaved file

* [SG-671] simplified code
2022-09-26 17:51:03 +01:00
github-actions[bot]
70ee24d82a Autosync the updated translations (#2099)
Co-authored-by: github-actions <>
2022-09-26 15:58:33 +02:00
Federico Andrés Maccaroni
54cbb1d683 Merge branch 'master' into EC-295-swipe-to-copy-logins 2022-09-23 12:19:36 -03:00
github-actions[bot]
28576bbf49 Autosync the updated translations (#2096)
Co-authored-by: github-actions <>
2022-09-23 02:46:20 +02:00
mp-bw
7f9dfd3dae Updated libs to latest stable (#2092)
* updated libs to latest stable

* testing rollback of test dependencies

* testing xunit restore

* bump all test libs except xunit

* remove AndroidX.Migration package
2022-09-22 11:17:17 -04:00
github-actions[bot]
115aee2026 Bump version to 2022.9.2 (#2090)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-21 14:23:27 -06:00
aj-rosado
86fee6f04e [PS-1312] Update NFCAdapter mutability flag (#2088)
* PS-1312 - Changed NFC Adapter PendingIntent to mutable
2022-09-21 17:43:48 +01:00
mp-bw
2deab59b35 [Android] Added shadowless version of monochrome launcher icon (#2084)
* Added shadowless version of monochrome launcher icon

* added missing project ref
2022-09-20 10:25:12 -04:00
aj-rosado
b4737457a8 PS-1404 Improved exception handling when saving an attachment (#2078)
* PS-1404 Improved exception handling when saving an attachment

* PS-1404 Reverted unnecessary catch change

* PS-1404 Added missing whitespace

* PS-1404 Improved code formatting

* PS-1404 removed unnecessary whitespace

* PS-1404 Using SubmitAsyncCommand on xaml and removed the click method from the cs file
2022-09-16 18:28:20 +01:00
aj-rosado
305c770c58 [PS-1312] Migration to android12 and new splashscreen (#2063)
* [PS-1312] Updated Android Target and Framework to Android12 and updated new SplashScreen for Android12

* PS-1312 Changed PendingIntents mutability

* PS-1312 Removed unused imports

* PS-1312 Added method to helper to add mutability option according to Android version

* PS-1312 Renamed helper method AddPendingIntentMutability and fixed validation

* PS-1312 Improved PendingIntentMutability method from helper readability and naming
2022-09-16 16:44:15 +01:00
github-actions[bot]
afa9e23707 Autosync the updated translations (#2077)
Co-authored-by: github-actions <>
2022-09-16 03:26:44 +02:00
André Filipe da Silva Bispo
87fb5cf2ae [SG-659] Fixed TOTP not showing for free user with classic plans. (#2071)
* [SG-659] Fixed TOTP not showing for free user with classic plans.

* [SG-659] Ran code format
2022-09-14 14:45:39 +01:00
Federico Andrés Maccaroni
9703e16419 EC-295 Fix format 2022-09-13 12:00:08 -03:00
Federico Andrés Maccaroni
e13977788b EC-295 Added swipe actions for Card and Note and improve some things 2022-09-13 11:41:56 -03:00
github-actions[bot]
3f8e00985c Bump version to 2022.9.1 (#2069)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-12 11:47:52 -06:00
github-actions[bot]
533928a4f1 Bump version to 2022.9.0 (#2068)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-09-12 10:58:58 -06:00
Federico Andrés Maccaroni
1987adc453 Merge branch 'master' into EC-295-swipe-to-copy-logins 2022-09-09 15:58:50 -03:00
Federico Maccaroni
b7048de2a1 [EC-528] Refactor Custom Fields into separate components (#1662)
* Refactored CustomFields to stop using RepeaterView and use BindableLayout and divided the different types on different files and added a factory to create them

* Fix formatting
2022-09-09 15:58:11 -03:00
github-actions[bot]
2016eadb0d Autosync the updated translations (#2057)
Co-authored-by: github-actions <>
2022-09-09 20:25:28 +02:00
Federico Andrés Maccaroni
ef96692f94 Merge branch 'master' into EC-295-swipe-to-copy-logins 2022-09-09 15:11:11 -03:00
Federico Maccaroni
68b5bc0964 EC-540 fix AppResources encoding to be UTF-8 with BOM (#2065) 2022-09-09 14:41:16 -03:00
Federico Andrés Maccaroni
4e5619d5bb Merge branch 'master' into EC-295-swipe-to-copy-logins 2022-09-07 17:41:31 -03:00
Michał Chęciński
119fc5812b Update deprecated Azure Key Vault in workflows (#2059) 2022-09-05 11:39:22 +02:00
aj-rosado
b628c1990e [PS-191] Improve support for larger fonts Android (#2053)
* PS-191 Removed android native font scaling

* PS-191 Added exception handling when disabling android font scale
2022-08-31 12:08:36 +01:00
Thomas Rittson
183bfa0ab2 Update PR template (#2051) 2022-08-30 10:36:59 +10:00
Carlos Gonçalves
b1fb867b6e [SG-223] Mobile username generator (#2033)
* SG-223 - Changed page title and password title

* SG-223 - Refactored generated field
* Changed position of generated field
* Replaced buttons generate and copy for icons

* SG-223 - Refactor type to passwordType

* SG-223 - Added password or username selector
* Added string for label type selection

* SG-223 - Added logic for different types of username
* Added strings of new types

* [SG-223] - Added UI components for different username types
* Added static strings for new labels
* Added viewmodel properties to support username generation and their respective options

* [SG-223] Added control over type picker visibility

* [SG-223] Refactored username entry on add edit page and added generate icon
* Added GenerateUsername command

* [SG-223] - Implemented service for username generation

* [SG-223] - Added support for username generation for item creation flow
* Implemented cache for username options
* Added exception handling for api calls

* [SG-223] - Remove unused code

* [SG-223] - Added a new display field for username generated and respective command
* Added description label for each type of username
* Changed defautl value of username from string.Empty to -

* [SG-223] - Removed some StackLayouts and refactored some controls

* [SG-223] - Refactored properties name

* [SG-223] - Added visibility toggle icon for api keys of forwarded email username types

* [SG-223] - Refactored nested StackLayouts into grids.

* [SG-223] - Refactor and pr fixing

* [SG-223] - Removed string keys from Resolve
- Added static string to resources

* [SG-223] - Refactored Copy_Clicked as AsyncCommand
- Improved exception handling
- Refactored TypeSelected as GeneratorTypeSelected

* [SG-223] - Renamed PasswordFormatter

* [SG-223] - Refactored VM properties to use UsernameGenerationOptions
* Removed LoadUsernameOptions

* [SG-223] - Refactored added pickers to use SelectedItem instead SelectedIndex
* Deleted PickerIndexToBoolConverter as it isn't needed anymore

* [SG-223] -  Refactored and simplified Grid row and column definitions

* [SG-223] - Refactored Command into async command
* Added exception handling and feedback to the user

* [SG-223] - Refactored GeneratorType picker to use Enum GeneratorType instead of string

* [SG-223] - Changed some resource keys

* [SG-223] - Refactor method name

* [SG-223] - Refactored code and added logs for switch default cases

* [SG-223] - Added flag to control visibility when in edit mode

* [SG-223] - Added suffix Parenthesis to keys to prevent future conflicts

* [SG-223] - Refactored multiple methods into one, GetUsernameFromAsync
* Removed unused Extensions from enums

* [SG-223] - Added exception message

* [SG-223] - Added localizable enum values through LocalizableEnumConverter

* [SG-223] - Fixed space between controls

* [SG-223] - Removed unused code and refactored some variables and methods names

* [SG-223] - Removed unused code and refactored constant name to be more elucidative

* [SG-223] - Removed unused variable
2022-08-26 19:32:02 +01:00
Federico Andrés Maccaroni
8c4e3c33f9 Merge branch 'master' into EC-295-swipe-to-copy-logins 2022-08-26 11:53:33 -03:00
Federico Andrés Maccaroni
778d543e8e Merge branch 'master' into EC-295-swipe-to-copy-logins
# Conflicts:
#	src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml
#	src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs
#	src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
2022-08-26 11:49:14 -03:00
manofthepeace
673ba9f3cc Fix Content Type for file upload (#2031) 2022-08-26 14:58:54 +01:00
github-actions[bot]
cdd9a5ff4d Autosync the updated translations (#2050)
Co-authored-by: github-actions <>
2022-08-26 10:07:32 +02:00
Federico Maccaroni
d204e812e1 EC-487 Added helper to localize enum values and also a converter to use in xaml (#2048) 2022-08-23 12:34:29 -03:00
André Filipe da Silva Bispo
9163b9e4de [SG-599] Cannot read authenticator key if you don't include URI before TOTP Secret. (#2047)
Removed unnecessary code when adding a TOTP auth key secret manually
2022-08-23 15:48:45 +01:00
André Filipe da Silva Bispo
ecd4da08ee [SG-598] Removed space from copied totp code (#2046)
Removed space from copied totp code
2022-08-23 15:04:17 +01:00
Federico Andrés Maccaroni
a5061df2a7 EC-295 Fix format 2022-07-29 12:40:28 -03:00
Federico Andrés Maccaroni
cd3415039d Merge branch 'master' into EC-295-swipe-to-copy-logins
# Conflicts:
#	src/Android/Properties/AndroidManifest.xml
#	src/App/Utilities/AppHelpers.cs
2022-07-29 12:38:49 -03:00
Federico Andrés Maccaroni
3878ea4e09 EC-295 Fixed Android swipe and added vibration when cipher username/password gets copied 2022-07-25 15:40:57 -03:00
Federico Andrés Maccaroni
9e7c462153 Merge branch 'master' into EC-295-swipe-to-copy-logins 2022-07-15 13:15:50 -03:00
Federico Andrés Maccaroni
52ab676b5f EC-295 merged master into swipe-to-copy-logins 2022-07-15 11:25:49 -03:00
Federico Andrés Maccaroni
602f3a7ea4 EC-295 format 2022-07-04 11:21:32 -03:00
Federico Andrés Maccaroni
2be5208b8f EC-295 Added swipe to copy on vault login items 2022-07-04 11:17:47 -03:00
279 changed files with 26873 additions and 8595 deletions

View File

@@ -22,7 +22,7 @@
## Before you submit
- [ ] I have checked for formatting errors (`dotnet tool run dotnet-format --check`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)
- Please check for formatting errors (`dotnet format --verify-no-changes`) (required)
- Please add **unit tests** where it makes sense to do so (encouraged but not required)
- If this change requires a **documentation update** - notify the documentation team
- If this change has particular **deployment requirements** - notify the DevOps team

View File

@@ -441,10 +441,17 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
with:
keyvault: "bitwarden-prod-kv"
secrets: "appcenter-ios-token"
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
appcenter-ios-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Decrypt secrets
env:
@@ -635,10 +642,17 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
crowdin-api-token
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Upload Sources
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
@@ -695,11 +709,18 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
env:
KEYVAULT: bitwarden-prod-kv
SECRETS: |
devops-alerts-slack-webhook-url
run: |
for i in ${SECRETS//,/ }
do
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$i::$VALUE"
done
- name: Notify Slack on failure
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33

View File

@@ -24,10 +24,10 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations
uses: crowdin/github-action@12143a68c213f3c6d9913c9e5023224f7231face
@@ -40,10 +40,12 @@ jobs:
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "github-actions"
github_user_email: "<>"
github_user_name: "bitwarden-devops-bot"
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
commit_message: "Autosync the updated translations"
localization_branch_name: crowdin-auto-sync
create_pull_request: true
pull_request_title: "Autosync Crowdin Translations"
pull_request_body: "Autosync the updated translations"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@@ -2,39 +2,38 @@
name: Version Auto Bump
on:
release:
types: [published]
push:
tags:
- v**
jobs:
setup:
name: "Setup"
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
version_number: ${{ steps.version.outputs.new-version }}
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Get version to bump
- name: Calculate bumped version
id: version
env:
RELEASE_TAG: ${{ github.event.release.tag }}
RELEASE_TAG: ${{ github.ref }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\1/')
CURR_VER=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]\.)([0-9])/\2/')
echo $CURR_VER
((CURR_VER++))
NEW_VER=$CURR_MAJOR$CURR_VER
echo $NEW_VER
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
echo "Current Major: $CURR_MAJOR"
echo "Current Patch: $CURR_PATCH"
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-20.04
runs-on: ubuntu-22.04
needs:
- setup
steps:
@@ -45,13 +44,10 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
env:
KEYVAULT: bitwarden-prod-kv
SECRET: "github-pat-bitwarden-devops-bot-repo-scope"
run: |
VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $SECRET --query value --output tsv)
echo "::add-mask::$VALUE"
echo "::set-output name=$SECRET::$VALUE"
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Call GitHub API to trigger workflow bump
env:

View File

@@ -16,6 +16,26 @@ jobs:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
@@ -52,8 +72,8 @@ jobs:
- name: Setup git
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Check if version changed
id: version-changed

View File

@@ -367,7 +367,7 @@ namespace Bit.Droid.Accessibility
public static string GetUri(AccessibilityNodeInfo root)
{
var uri = string.Concat(Constants.AndroidAppProtocol, root.PackageName);
var uri = string.Concat(Core.Constants.AndroidAppProtocol, root.PackageName);
if (SupportedBrowsers.ContainsKey(root.PackageName))
{
var browser = SupportedBrowsers[root.PackageName];

View File

@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
namespace Bit.Droid.Accessibility
{
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden")]
[Service(Permission = Android.Manifest.Permission.BindAccessibilityService, Label = "Bitwarden", Exported = true)]
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]

View File

@@ -15,7 +15,7 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v12.1</TargetFrameworkVersion>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
@@ -75,24 +75,24 @@
<Version>2.1.0.4</Version>
</PackageReference>
<PackageReference Include="Portable.BouncyCastle">
<Version>1.8.10</Version>
<Version>1.9.0</Version>
</PackageReference>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.9" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.11" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.10" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.2.5.2" />
<PackageReference Include="Xamarin.AndroidX.Migration" Version="1.0.8" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.5.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.13" />
<PackageReference Include="Xamarin.AndroidX.CardView" Version="1.0.0.16" />
<PackageReference Include="Xamarin.AndroidX.Core" Version="1.9.0" />
<PackageReference Include="Xamarin.AndroidX.Legacy.Support.V4" Version="1.0.0.14" />
<PackageReference Include="Xamarin.AndroidX.MediaRouter" Version="1.3.1" />
<PackageReference Include="Xamarin.Essentials">
<Version>1.7.3</Version>
</PackageReference>
<PackageReference Include="Xamarin.Firebase.Messaging">
<Version>122.0.0</Version>
<Version>123.0.8</Version>
</PackageReference>
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.4.0.4" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.37.0" />
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.6.1.1" />
<PackageReference Include="Xamarin.Google.Dagger" Version="2.41.0.2" />
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
<Version>117.0.1</Version>
<Version>118.0.1.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -152,6 +152,12 @@
<Compile Include="Utilities\IntentExtensions.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" />
<Compile Include="Renderers\CollectionView\ExtendedCollectionViewRenderer.cs" />
<Compile Include="Utilities\RecyclerSwipeItemTouchCallback.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\bwi-font.ttf" />
@@ -176,6 +182,7 @@
<AndroidResource Include="Resources\drawable\cog_settings.xml" />
<AndroidResource Include="Resources\drawable\icon.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\id.xml" />
<AndroidResource Include="Resources\drawable\info.xml" />
@@ -213,6 +220,13 @@
<AndroidResource Include="Resources\values\colors.xml" />
<AndroidResource Include="Resources\values\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>
<AndroidResource Include="Resources\drawable\splash_screen.xml" />
@@ -280,6 +294,9 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\values-v30\" />
<Folder Include="Resources\drawable-v26\" />
<Folder Include="Resources\drawable-night-v26\" />
<Folder Include="Renderers\CollectionView\" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

View File

@@ -19,6 +19,7 @@ using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Bit.Core.Abstractions;
using SaveFlags = Android.Service.Autofill.SaveFlags;
using Bit.Droid.Utilities;
namespace Bit.Droid.Autofill
{
@@ -270,8 +271,7 @@ namespace Bit.Droid.Autofill
return null;
}
intent.PutExtra("autofillFrameworkUri", uri);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
PendingIntentFlags.CancelCurrent);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.CancelCurrent, true));
var overlayPresentation = BuildOverlayPresentation(
AppResources.AutofillWithBitwarden,
@@ -324,7 +324,7 @@ namespace Bit.Droid.Autofill
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the
// "my vault" presentation) so we're including an empty one here
pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent);
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true));
}
var slice = CreateInlinePresentationSlice(
inlinePresentationSpec,

View File

@@ -15,7 +15,7 @@ using Bit.Core.Utilities;
namespace Bit.Droid.Autofill
{
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden")]
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "Bitwarden", Exported = true)]
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
@@ -134,7 +134,7 @@ namespace Bit.Droid.Autofill
{
case CipherType.Login:
intent.PutExtra("autofillFrameworkName", parser.Uri
.Replace(Constants.AndroidAppProtocol, string.Empty)
.Replace(Core.Constants.AndroidAppProtocol, string.Empty)
.Replace("https://", string.Empty)
.Replace("http://", string.Empty));
intent.PutExtra("autofillFrameworkUri", parser.Uri);

View File

@@ -48,7 +48,7 @@ namespace Bit.Droid.Autofill
}
else
{
_uri = string.Concat(Constants.AndroidAppProtocol, PackageName);
_uri = string.Concat(Core.Constants.AndroidAppProtocol, PackageName);
}
return _uri;
}

7
src/Android/Constants.cs Normal file
View File

@@ -0,0 +1,7 @@
namespace Bit.Droid
{
public static class Constants
{
public const string PACKAGE_NAME = "com.x8bit.bitwarden";
}
}

View File

@@ -5,12 +5,14 @@ using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.Nfc;
using Android.OS;
using Android.Runtime;
using AndroidX.Core.Content;
using Android.Views;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
@@ -18,7 +20,11 @@ using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xamarin.Essentials;
using ZXing.Net.Mobile.Android;
using FileProvider = AndroidX.Core.Content.FileProvider;
namespace Bit.Droid
{
@@ -30,11 +36,14 @@ namespace Bit.Droid
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
private IDeviceActionService _deviceActionService;
private IFileService _fileService;
private IMessagingService _messagingService;
private IBroadcasterService _broadcasterService;
private IStateService _stateService;
private IAppIdService _appIdService;
private IEventService _eventService;
private IPushNotificationListenerService _pushNotificationListenerService;
private ILogger _logger;
private PendingIntent _eventUploadPendingIntent;
private AppOptions _appOptions;
private string _activityKey = $"{nameof(MainActivity)}_{Java.Lang.JavaSystem.CurrentTimeMillis().ToString()}";
@@ -45,17 +54,20 @@ namespace Bit.Droid
{
var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver));
_eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent,
PendingIntentFlags.UpdateCurrent);
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false));
var policy = new StrictMode.ThreadPolicy.Builder().PermitAll().Build();
StrictMode.SetThreadPolicy(policy);
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_pushNotificationListenerService = ServiceContainer.Resolve<IPushNotificationListenerService>();
_logger = ServiceContainer.Resolve<ILogger>("logger");
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
@@ -70,7 +82,7 @@ namespace Bit.Droid
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
});
ServiceContainer.Resolve<ILogger>("logger").InitAsync();
_logger.InitAsync();
var toplayout = Window?.DecorView?.RootView;
if (toplayout != null)
@@ -81,8 +93,9 @@ namespace Bit.Droid
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
Xamarin.Forms.Forms.Init(this, savedInstanceState);
_appOptions = GetOptions();
CreateNotificationChannel();
LoadApplication(new App.App(_appOptions));
DisableAndroidFontScale();
_broadcasterService.Subscribe(_activityKey, (message) =>
{
@@ -138,6 +151,15 @@ namespace Bit.Droid
AndroidHelpers.SetPreconfiguredRestrictionSettingsAsync(this)
.GetAwaiter()
.GetResult();
if (Intent?.GetStringExtra(Core.Constants.NotificationData) is string notificationDataJson)
{
var notificationType = JToken.Parse(notificationDataJson).SelectToken(Core.Constants.NotificationDataType);
if (notificationType.ToString() == PasswordlessNotificationData.TYPE)
{
_pushNotificationListenerService.OnNotificationTapped(JsonConvert.DeserializeObject<PasswordlessNotificationData>(notificationDataJson)).FireAndForget();
}
}
}
protected override void OnNewIntent(Intent intent)
@@ -191,13 +213,13 @@ namespace Bit.Droid
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
[GeneratedEnum] Permission[] grantResults)
{
if (requestCode == Constants.SelectFilePermissionRequestCode)
if (requestCode == Core.Constants.SelectFilePermissionRequestCode)
{
if (grantResults.Any(r => r != Permission.Granted))
{
_messagingService.Send("selectFileCameraPermissionDenied");
}
await _deviceActionService.SelectFileAsync();
await _fileService.SelectFileAsync();
}
else
{
@@ -210,7 +232,7 @@ namespace Bit.Droid
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (resultCode == Result.Ok &&
(requestCode == Constants.SelectFileRequestCode || requestCode == Constants.SaveFileRequestCode))
(requestCode == Core.Constants.SelectFileRequestCode || requestCode == Core.Constants.SaveFileRequestCode))
{
Android.Net.Uri uri = null;
string fileName = null;
@@ -232,7 +254,7 @@ namespace Bit.Droid
return;
}
if (requestCode == Constants.SaveFileRequestCode)
if (requestCode == Core.Constants.SaveFileRequestCode)
{
_messagingService.Send("selectSaveFileResult",
new Tuple<string, string>(uri.ToString(), fileName));
@@ -273,7 +295,7 @@ namespace Bit.Droid
{
var intent = new Intent(this, Class);
intent.AddFlags(ActivityFlags.SingleTop);
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, AndroidHelpers.AddPendingIntentMutabilityFlag(0, true));
// register for all NDEF tags starting with http och https
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
ndef.AddDataScheme("http");
@@ -401,5 +423,38 @@ namespace Bit.Droid
alarmManager.Cancel(_eventUploadPendingIntent);
await _eventService.UploadEventsAsync();
}
private void CreateNotificationChannel()
{
#if !FDROID
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
// Notification channels are new in API 26 (and not a part of the
// support library). There is no need to create a notification
// channel on older versions of Android.
return;
}
var channel = new NotificationChannel(Core.Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default);
if(GetSystemService(NotificationService) is NotificationManager notificationManager)
{
notificationManager.CreateNotificationChannel(channel);
}
#endif
}
private void DisableAndroidFontScale()
{
try
{
//As we are using NamedSizes the xamarin will change the font size. So we are disabling the Android scaling.
Resources.Configuration.FontScale = 1f;
BaseContext.Resources.DisplayMetrics.ScaledDensity = Resources.Configuration.FontScale * (float)DeviceDisplay.MainDisplayInfo.Density;
}
catch (Exception e)
{
_logger.Exception(e);
}
}
}
}

View File

@@ -20,6 +20,7 @@ using System.Net;
using Bit.App.Utilities;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
using Bit.App.Utilities.Helpers;
using Bit.App.Controls;
#if !FDROID
using Android.Gms.Security;
@@ -46,8 +47,9 @@ namespace Bit.Droid
{
RegisterLocalServices();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Constants.ClearCiphersCacheKey,
Constants.AndroidAllClearCipherCacheKeys);
ServiceContainer.Init(deviceActionService.DeviceUserAgent, Core.Constants.ClearCiphersCacheKey,
Core.Constants.AndroidAllClearCipherCacheKeys);
InitializeAppSetup();
// TODO: Update when https://github.com/bitwarden/mobile/pull/1662 gets merged
var deleteAccountActionFlowExecutioner = new DeleteAccountActionFlowExecutioner(
@@ -71,8 +73,18 @@ namespace Bit.Droid
ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
var cipherHelper = new CipherHelper(
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IEventService>("eventService"),
ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"),
ServiceContainer.Resolve<IClipboardService>("clipboardService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService")
);
ServiceContainer.Register<ICipherHelper>("cipherHelper", cipherHelper);
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
@@ -137,8 +149,9 @@ namespace Bit.Droid
var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService);
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
var deviceActionService = new DeviceActionService(stateService, messagingService);
var fileService = new FileService(stateService, broadcasterService);
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, new LazyResolve<IEventService>());
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
var biometricService = new BiometricService();
@@ -157,6 +170,8 @@ namespace Bit.Droid
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IFileService>(fileService);
ServiceContainer.Register<IAutofillHandler>(autofillHandler);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
@@ -193,5 +208,12 @@ namespace Bit.Droid
{
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
}
private void InitializeAppSetup()
{
var appSetup = new AppSetup();
appSetup.InitializeServicesLastChance();
ServiceContainer.Register<IAppSetup>("appSetup", appSetup);
}
}
}

View File

@@ -1,6 +1,6 @@
<?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.9.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<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-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" 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">

View File

@@ -1,7 +1,9 @@
#if !FDROID
using System;
using Android.App;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Firebase.Messaging;
using Newtonsoft.Json;
@@ -16,34 +18,41 @@ namespace Bit.Droid.Push
{
public async override void OnNewToken(string token)
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
try {
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
await stateService.SetPushRegisteredTokenAsync(token);
await pushNotificationService.RegisterAsync();
await stateService.SetPushRegisteredTokenAsync(token);
await pushNotificationService.RegisterAsync();
}
catch (Exception ex)
{
Logger.Instance.Exception(ex);
}
}
public async override void OnMessageReceived(RemoteMessage message)
{
if (message?.Data == null)
{
return;
}
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
if (data == null)
{
return;
}
try
{
if (message?.Data == null)
{
return;
}
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
if (data == null)
{
return;
}
var obj = JObject.Parse(data);
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
"pushNotificationListenerService");
await listener.OnMessageAsync(obj, Device.Android);
}
catch (JsonReaderException ex)
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
Logger.Instance.Exception(ex);
}
}
}

View 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);
}
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using Android.Content;
using AndroidX.RecyclerView.Widget;
using Bit.App.Controls;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Droid.Renderers.CollectionView;
using Bit.Droid.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using static Android.Content.ClipData;
using static AndroidX.RecyclerView.Widget.RecyclerView;
[assembly: ExportRenderer(typeof(ExtendedCollectionView), typeof(ExtendedCollectionViewRenderer))]
namespace Bit.Droid.Renderers.CollectionView
{
public class CustomGroupableItemsViewAdapter<TItemsView, TItemsViewSource> : GroupableItemsViewAdapter<TItemsView, TItemsViewSource>
where TItemsView : GroupableItemsView
where TItemsViewSource : IGroupableItemsViewSource
{
protected internal CustomGroupableItemsViewAdapter(TItemsView groupableItemsView, Func<View, Context, ItemContentView> createView = null)
: base(groupableItemsView, createView)
{
}
public object GetItemAt(int position)
{
return ItemsSource.GetItem(position);
}
}
public class ExtendedCollectionViewRenderer : GroupableItemsViewRenderer<ExtendedCollectionView, CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource>, IGroupableItemsViewSource>
{
ItemTouchHelper _itemTouchHelper;
public ExtendedCollectionViewRenderer(Context context) : base(context)
{
}
protected override CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource> CreateAdapter()
{
return new CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource>(ItemsView);
}
protected override void SetUpNewElement(ExtendedCollectionView newElement)
{
base.SetUpNewElement(newElement);
if (newElement is null)
{
return;
}
var itemTouchCallback = new RecyclerSwipeItemTouchCallback<CipherViewCellViewModel>(ItemTouchHelper.Right, this.Context, new CipherViewModelSwipeableItem(),
viewHolder =>
{
if (viewHolder is TemplatedItemViewHolder templatedViewHolder
&&
templatedViewHolder.View?.BindingContext is CipherViewCellViewModel vm)
{
return vm;
}
return null;
});
itemTouchCallback.OnSwipedCommand = new Command<ViewHolder>(viewHolder =>
{
ItemsViewAdapter.NotifyItemChanged(viewHolder.LayoutPosition);
ItemsView.OnSwipeCommand?.Execute(ItemsViewAdapter.GetItemAt(viewHolder.BindingAdapterPosition));
});
_itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
_itemTouchHelper.AttachToRecyclerView(this);
}
protected override void TearDownOldElement(ItemsView oldElement)
{
base.TearDownOldElement(oldElement);
if (oldElement is null)
{
return;
}
_itemTouchHelper.AttachToRecyclerView(null);
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Bit.Droid.Renderers
protected override void OnElementChanged(ElementChangedEventArgs<View> elementChangedEvent)
{
base.OnElementChanged(elementChangedEvent);
if (elementChangedEvent.NewElement != null)
if (elementChangedEvent.NewElement is ExtendedGrid extGrid && extGrid.ApplyRipple)
{
SetBackgroundResource(Resource.Drawable.list_item_bg);
}

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

View File

@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -4,6 +4,7 @@
<style name="LaunchTheme" parent="BaseTheme">
<item name="android:windowBackground">@drawable/splash_screen_dark</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_screen_round</item>
</style>
<style name="BaseTheme" parent="Theme.AppCompat">

View File

@@ -4,6 +4,8 @@
<style name="LaunchTheme" parent="BaseTheme">
<item name="android:windowBackground">@drawable/splash_screen</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 name="BaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">

View File

@@ -6,4 +6,5 @@
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"/>
android:canRetrieveWindowContent="true"
android:isAccessibilityTool="false"/>

View File

@@ -1,10 +1,21 @@
#if !FDROID
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Newtonsoft.Json;
using Xamarin.Forms;
using static Xamarin.Essentials.Platform;
using Intent = Android.Content.Intent;
namespace Bit.Droid.Services
{
@@ -23,6 +34,11 @@ namespace Bit.Droid.Services
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
public Task<bool> AreNotificationsSettingsEnabledAsync()
{
return Task.FromResult(IsRegisteredForPush);
}
public async Task<string> GetTokenAsync()
{
return await _stateService.GetPushCurrentTokenAsync();
@@ -47,6 +63,50 @@ namespace Bit.Droid.Services
// Do we ever need to unregister?
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

View 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);
}
}
}
}
}
}

View File

@@ -5,6 +5,7 @@ using Android.Content;
using Android.OS;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Xamarin.Essentials;
@@ -23,7 +24,7 @@ namespace Bit.Droid.Services
PendingIntent.GetBroadcast(CrossCurrentActivity.Current.Activity,
0,
new Intent(CrossCurrentActivity.Current.Activity, typeof(ClearClipboardAlarmReceiver)),
PendingIntentFlags.UpdateCurrent));
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, false)));
}
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)

View File

@@ -1,11 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android;
using Android.App;
using Android.App.Assist;
using Android.Content;
using Android.Content.PM;
using Android.Nfc;
@@ -14,20 +9,13 @@ using Android.Provider;
using Android.Text;
using Android.Text.Method;
using Android.Views;
using Android.Views.Autofill;
using Android.Views.InputMethods;
using Android.Webkit;
using Android.Widget;
using AndroidX.Core.App;
using AndroidX.Core.Content;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Bit.Droid.Autofill;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
@@ -35,38 +23,20 @@ namespace Bit.Droid.Services
{
public class DeviceActionService : IDeviceActionService
{
private readonly IClipboardService _clipboardService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
private readonly Func<IEventService> _eventServiceFunc;
private AlertDialog _progressDialog;
object _progressDialogLock = new object();
private bool _cameraPermissionsDenied;
private Toast _toast;
private string _userAgent;
public DeviceActionService(
IClipboardService clipboardService,
IStateService stateService,
IMessagingService messagingService,
IBroadcasterService broadcasterService,
Func<IEventService> eventServiceFunc)
IMessagingService messagingService)
{
_clipboardService = clipboardService;
_stateService = stateService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
_eventServiceFunc = eventServiceFunc;
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
{
if (message.Command == "selectFileCameraPermissionDenied")
{
_cameraPermissionsDenied = true;
}
});
}
public string DeviceUserAgent
@@ -212,184 +182,6 @@ namespace Bit.Droid.Services
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,
string text = null, string okButtonText = null, string cancelButtonText = null,
bool numericKeyboard = false, bool autofocus = true, bool password = false)
@@ -467,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()
{
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
@@ -526,25 +290,6 @@ namespace Bit.Droid.Services
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()
{
return (int)Build.VERSION.SdkInt;
@@ -635,112 +380,6 @@ namespace Bit.Droid.Services
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()
{
@@ -771,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()
{
try
@@ -848,61 +468,6 @@ namespace Bit.Droid.Services
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)
{
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
@@ -920,24 +485,6 @@ namespace Bit.Droid.Services
return intent;
}
private async Task CopyTotpAsync(CipherView cipher)
{
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
{
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (totp != null)
{
await _clipboardService.CopyTextAsync(totp);
}
}
}
}
public float GetSystemFontSizeScale()
{
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
@@ -964,5 +511,14 @@ namespace Bit.Droid.Services
}
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);
}
}
}

View 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;
}
}
}

View File

@@ -12,7 +12,7 @@ using Java.Lang;
namespace Bit.Droid.Tile
{
[Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/AutoFillTile",
Icon = "@drawable/shield")]
Icon = "@drawable/shield", Exported = true)]
[IntentFilter(new string[] { ActionQsTile })]
[Register("com.x8bit.bitwarden.AutofillTileService")]
public class AutofillTileService : TileService

View File

@@ -14,7 +14,7 @@ using Java.Lang;
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")]
[IntentFilter(new string[] { ActionQsTile })]
[Register("com.x8bit.bitwarden.GeneratorTileService")]

View File

@@ -15,7 +15,8 @@ using Java.Lang;
namespace Bit.Droid.Tile
{
[Service(Permission = Android.Manifest.Permission.BindQuickSettingsTile, Label = "@string/MyVault",
Icon = "@drawable/shield")]
Icon = "@drawable/shield",
Exported = true)]
[IntentFilter(new string[] { ActionQsTile })]
[Register("com.x8bit.bitwarden.MyVaultTileService")]
public class MyVaultTileService : TileService

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Provider;
using Bit.App.Utilities;
@@ -47,5 +49,22 @@ namespace Bit.Droid.Utilities
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;
}
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Util;
using Android.Views;
using AndroidX.RecyclerView.Widget;
using Bit.App.Abstractions;
using Xamarin.Forms.Platform.Android;
using FontImageSource = Xamarin.Forms.FontImageSource;
namespace Bit.Droid.Utilities
{
public class RecyclerSwipeItemTouchCallback<TItem> : ItemTouchHelper.SimpleCallback
{
private Paint _clearPaint;
private readonly ColorDrawable _background = new ColorDrawable();
private readonly Android.Content.Context _context;
private readonly ISwipeableItem<TItem> _swipeableItem;
private readonly Func<RecyclerView.ViewHolder, TItem> _viewHolderToTItem;
private Dictionary<string, Typeface> _fontFamilyTypefaceCache = new Dictionary<string, Typeface>();
public RecyclerSwipeItemTouchCallback(int swipeDir, Android.Content.Context context, ISwipeableItem<TItem> swipeableItem, Func<RecyclerView.ViewHolder, TItem> viewHolderToTItem)
: base(0, swipeDir)
{
_context = context;
_swipeableItem = swipeableItem;
_viewHolderToTItem = viewHolderToTItem;
_clearPaint = new Paint();
_clearPaint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.Clear));
}
public ICommand OnSwipedCommand { get; set; }
public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
{
return false;
}
public override void OnChildDrawOver(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, float dX, float dY,
int actionState, bool isCurrentlyActive)
{
var itemView = viewHolder.ItemView;
int itemHeight = itemView.Bottom - itemView.Top;
var isCanceled = (dX == 0f) && !isCurrentlyActive;
if (isCanceled)
{
ClearCanvas(c, itemView.Right + dX, itemView.Top, itemView.Right, itemView.Bottom);
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, false);
return;
}
if (!(_swipeableItem.GetSwipeIcon(_viewHolderToTItem(viewHolder)) is FontImageSource fontSource))
{
return;
}
using var paint = GetIconPaint(itemView, fontSource);
var width = (int)(paint.MeasureText(fontSource.Glyph) + .5f);
var baseline = (int)(-paint.Ascent() + .5f);
var height = (int)(baseline + paint.Descent() + .5f);
int itemTop = itemView.Top + (itemHeight - height) / 2;
int itemMargin = (itemHeight - height) / 2;
int itemBottom = itemTop + height;
_background.Color = _swipeableItem.GetBackgroundColor(_viewHolderToTItem(viewHolder)).ToAndroid();
if (dX < 0)
{
_background.SetBounds((int)(itemView.Right + dX), itemView.Top, itemView.Right, itemView.Bottom);
_background.Draw(c);
int itemLeft = itemView.Right - itemMargin - width;
int itemRight = itemView.Right - itemMargin;
c.DrawText(fontSource.Glyph, itemLeft, itemBottom, paint);
}
else
{
_background.SetBounds((int)(itemView.Left + dX), itemView.Top, itemView.Left, itemView.Bottom);
_background.Draw(c);
int itemLeft = itemView.Left + itemMargin;
int itemRight = itemView.Left + itemMargin + width;
c.DrawText(fontSource.Glyph, itemLeft, itemBottom, paint);
}
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
private Paint GetIconPaint(View itemView, FontImageSource fontSource)
{
var paint = new Paint
{
TextSize = TypedValue.ApplyDimension(ComplexUnitType.Dip, (float)fontSource.Size, _context.Resources.DisplayMetrics),
Color = fontSource.Color.ToAndroid(),
TextAlign = Paint.Align.Left,
AntiAlias = true,
};
if (fontSource.FontFamily != null)
{
if (!_fontFamilyTypefaceCache.TryGetValue(fontSource.FontFamily, out var typeface))
{
var font = new Xamarin.Forms.Font();
// HACK: there is no way to set the font family of Font
// and the only public extension method to get the typeface is thorugh a Xamarin.Forms.Font
// so we use reflection here to set the font family and take advantage of ToTypeface method
// Also, we need to box the font in order to use reflection to set the property because it's a struct
object fontBoxed = font;
var pinfo = typeof(Xamarin.Forms.Font)
.GetProperty(nameof(Xamarin.Forms.Font.FontFamily));
pinfo.SetValue(fontBoxed, fontSource.FontFamily, null);
typeface = ((Xamarin.Forms.Font)fontBoxed).ToTypeface();
_fontFamilyTypefaceCache.Add(fontSource.FontFamily, typeface);
}
paint.SetTypeface(typeface);
}
int alpha = Math.Abs(((int)((itemView.TranslationX / itemView.Width) * 510)));
paint.Alpha = Math.Min(alpha, 255);
return paint;
}
public override int GetSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
{
if (viewHolder is TemplatedItemViewHolder templatedViewHolder
&&
_swipeableItem.CanSwipe(_viewHolderToTItem(viewHolder)))
{
return base.GetSwipeDirs(recyclerView, viewHolder);
}
return 0;
}
private void ClearCanvas(Canvas c, float left, float top, float right, float bottom)
{
if (c != null)
c.DrawRect(left, top, right, bottom, _clearPaint);
}
public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
{
OnSwipedCommand?.Execute(viewHolder);
}
}
}

View File

@@ -7,7 +7,8 @@ namespace Bit.Droid
{
[Activity(
NoHistory = true,
LaunchMode = LaunchMode.SingleTop)]
LaunchMode = LaunchMode.SingleTop,
Exported = true)]
[IntentFilter(new[] { Android.Content.Intent.ActionView },
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
DataScheme = "bitwarden")]

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Abstractions
{
@@ -8,46 +7,35 @@ namespace Bit.App.Abstractions
{
string DeviceUserAgent { get; }
DeviceType DeviceType { get; }
int SystemMajorVersion();
string SystemModel();
string GetBuildNumber();
void Toast(string text, bool longDuration = false);
bool LaunchApp(string appName);
Task ShowLoadingAsync(string text);
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,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = 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();
Task<bool> SupportsFaceBiometricAsync();
bool SupportsNfc();
bool SupportsCamera();
bool SupportsAutofillService();
int SystemMajorVersion();
string SystemModel();
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
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();
bool SupportsFido2();
bool LaunchApp(string appName);
void RateApp();
void OpenAccessibilitySettings();
void OpenAccessibilityOverlayPermissionSettings();
void OpenAutofillSettings();
long GetActiveTime();
void CloseMainApp();
bool SupportsFido2();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
}
}

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Bit.App.Models;
using Newtonsoft.Json.Linq;
namespace Bit.App.Abstractions
@@ -9,6 +10,8 @@ namespace Bit.App.Abstractions
Task OnRegisteredAsync(string token, string device);
void OnUnregistered(string device);
void OnError(string message, string device);
Task OnNotificationTapped(BaseNotificationData data);
Task OnNotificationDismissed(BaseNotificationData data);
bool ShouldShowNotification();
}
}

View File

@@ -1,12 +1,17 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
namespace Bit.App.Abstractions
{
public interface IPushNotificationService
{
bool IsRegisteredForPush { get; }
Task<bool> AreNotificationsSettingsEnabledAsync();
Task<string> GetTokenAsync();
Task RegisterAsync();
Task UnregisterAsync();
void SendLocalNotification(string title, string message, BaseNotificationData data);
void DismissLocalNotification(string notificationId);
}
}

View File

@@ -0,0 +1,11 @@
using Xamarin.Forms;
namespace Bit.App.Abstractions
{
public interface ISwipeableItem<TItem>
{
bool CanSwipe(TItem item);
FontImageSource GetSwipeIcon(TItem item);
Color GetBackgroundColor(TItem item);
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>Bit.App</RootNamespace>
<AssemblyName>BitwardenApp</AssemblyName>
<Configurations>Debug;Release;FDroid</Configurations>
@@ -14,11 +14,11 @@
<ItemGroup>
<PackageReference Include="Plugin.Fingerprint" Version="2.1.5" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.2" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.88.2" />
<PackageReference Include="Xamarin.CommunityToolkit" Version="2.0.5" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.3" />
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2478" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2515" />
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.4.1" />
</ItemGroup>
@@ -122,13 +122,23 @@
<SubType>Code</SubType>
</Compile>
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
<Folder Include="Behaviors\" />
<Folder Include="Lists\" />
<Folder Include="Lists\ItemLayouts\" />
<Folder Include="Lists\DataTemplateSelectors\" />
<Folder Include="Lists\ItemLayouts\CustomFields\" />
<Folder Include="Lists\ItemViewModels\" />
<Folder Include="Lists\ItemViewModels\CustomFields\" />
<Folder Include="Controls\AccountSwitchingOverlay\" />
<Folder Include="Utilities\AccountManagement\" />
<Folder Include="Utilities\Helpers\" />
<Folder Include="Controls\DateTime\" />
</ItemGroup>
@@ -412,8 +422,15 @@
<ItemGroup>
<None Remove="Behaviors\" />
<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="Utilities\AccountManagement\" />
<None Remove="Utilities\Helpers\" />
<None Remove="Controls\DateTime\" />
</ItemGroup>
</Project>

View File

@@ -7,9 +7,11 @@ using Bit.App.Resources;
using Bit.App.Services;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -25,13 +27,14 @@ namespace Bit.App
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ISyncService _syncService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService;
private readonly IStorageService _secureStorageService;
private readonly IDeviceActionService _deviceActionService;
private readonly IFileService _fileService;
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
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)
{
@@ -47,10 +50,10 @@ namespace Bit.App
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_accountsManager.Init(() => Options, this);
@@ -140,6 +143,10 @@ namespace Bit.App
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
}
catch (Exception ex)
{
@@ -148,11 +155,80 @@ namespace Bit.App
});
}
private async Task CheckPasswordlessLoginRequestsAsync()
{
if (!_isResumed)
{
_pendingCheckPasswordlessLoginRequests = true;
return;
}
_pendingCheckPasswordlessLoginRequests = false;
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
if (notification == null)
{
return;
}
if (await CheckShouldSwitchActiveUserAsync(notification))
{
return;
}
// Delay to wait for the vault page to appear
await Task.Delay(2000);
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; }
protected async override void OnStart()
{
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
_isResumed = true;
await ClearCacheIfNeededAsync();
Prime();
if (string.IsNullOrWhiteSpace(Options.Uri))
@@ -164,6 +240,10 @@ namespace Bit.App
SyncIfNeeded();
}
}
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
if (Device.RuntimePlatform == Device.Android)
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
@@ -196,6 +276,10 @@ namespace Bit.App
{
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
_isResumed = true;
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
if (Device.RuntimePlatform == Device.Android)
{
ResumedAsync().FireAndForget();
@@ -245,7 +329,7 @@ namespace Bit.App
var lastClear = await _stateService.GetLastFileCacheClearAsync();
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
{
var task = Task.Run(() => _deviceActionService.ClearCacheAsync());
var task = Task.Run(() => _fileService.ClearCacheAsync());
}
}

View File

@@ -0,0 +1,89 @@
using System.Collections.Generic;
using Bit.App.Abstractions;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Enums;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class CipherViewModelSwipeableItem : ISwipeableItem<CipherViewCellViewModel>
{
readonly Dictionary<CipherType, FontImageSource> _imageCache = new Dictionary<CipherType, FontImageSource>();
public bool CanSwipe(CipherViewCellViewModel item)
{
if (item?.Cipher is null)
{
return false;
}
return item.Cipher.Type == CipherType.Login
||
item.Cipher.Type == CipherType.Card
||
item.Cipher.Type == CipherType.SecureNote;
}
public Xamarin.Forms.Color GetBackgroundColor(CipherViewCellViewModel item)
{
if (item?.Cipher is null)
{
return ThemeManager.GetResourceColor("PrimaryColor");
}
if (item.Cipher.Type == CipherType.Login
&&
string.IsNullOrEmpty(item.Cipher.Login?.Password))
{
return ThemeManager.GetResourceColor("SeparatorColor");
}
if (item.Cipher.Type == CipherType.Card
&&
string.IsNullOrEmpty(item.Cipher.Card?.Number))
{
return ThemeManager.GetResourceColor("SeparatorColor");
}
if (item.Cipher.Type == CipherType.SecureNote
&&
string.IsNullOrEmpty(item.Cipher.Notes))
{
return ThemeManager.GetResourceColor("SeparatorColor");
}
return ThemeManager.GetResourceColor("PrimaryColor");
}
public FontImageSource GetSwipeIcon(CipherViewCellViewModel item)
{
if (item?.Cipher is null)
{
return null;
}
if (!_imageCache.TryGetValue(item.Cipher.Type, out var image))
{
image = new IconFontImageSource { Color = ThemeManager.GetResourceColor("BackgroundColor") };
switch (item.Cipher.Type)
{
case CipherType.Login:
image.Glyph = BitwardenIcons.Key;
break;
case CipherType.Card:
image.Glyph = BitwardenIcons.Hashtag;
break;
case CipherType.SecureNote:
image.Glyph = BitwardenIcons.Clone;
break;
default:
return null;
}
_imageCache.Add(item.Cipher.Type, image);
}
return image;
}
}
}

View File

@@ -1,9 +1,19 @@
using Xamarin.Forms;
using System.Windows.Input;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class ExtendedCollectionView : CollectionView
{
public static BindableProperty OnSwipeCommandProperty =
BindableProperty.Create(nameof(OnSwipeCommand), typeof(ICommand), typeof(ExtendedCollectionView));
public ICommand OnSwipeCommand
{
get => (ICommand)GetValue(OnSwipeCommandProperty);
set => SetValue(OnSwipeCommandProperty, value);
}
public string ExtraDataForLogging { get; set; }
}
}

View File

@@ -4,5 +4,6 @@ namespace Bit.App.Controls
{
public class ExtendedGrid : Grid
{
public bool ApplyRipple { get; set; } = true;
}
}

View File

@@ -0,0 +1,20 @@
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class IconFontImageSource : FontImageSource
{
public IconFontImageSource()
{
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontFamily = "bwi-font";
break;
case Device.Android:
FontFamily = "bwi-font.ttf#bwi-font";
break;
}
}
}
}

View File

@@ -1,9 +1,11 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Specialized;
using Xamarin.Forms;
namespace Bit.App.Controls
{
[Obsolete]
public class RepeaterView : StackLayout
{
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(

View File

@@ -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;
}
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class BooleanCustomFieldItemLayout : StackLayout
{
public BooleanCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class HiddenCustomFieldItemLayout : StackLayout
{
public HiddenCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class LinkedCustomFieldItemLayout : StackLayout
{
public LinkedCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
using Xamarin.Forms;
namespace Bit.App.Lists.ItemLayouts.CustomFields
{
public partial class TextCustomFieldItemLayout : StackLayout
{
public TextCustomFieldItemLayout()
{
InitializeComponent();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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; }
}
}

View 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; }
}
}

View 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>

View File

@@ -0,0 +1,43 @@
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;
if (Device.RuntimePlatform == Device.iOS)
{
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();
}
}
}

View 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; }
}
}

View File

@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<?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"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="Bit.App.Pages.GeneratorPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
xmlns:enums="clr-namespace:Bit.Core.Enums;assembly=BitwardenCore"
x:DataType="pages:GeneratorPageViewModel"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
@@ -16,6 +19,8 @@
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:LocalizableEnumConverter x:Key="localizableEnum" />
<xct:EnumToBoolConverter x:Key="enumToBool"/>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<ToolbarItem Text="{u:I18n Select}"
@@ -42,60 +47,302 @@
in ContentView.-->
<ContentView>
<ScrollView Padding="0, 0, 0, 20">
<StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box">
<Grid IsVisible="{Binding IsPolicyInEffect}"
Margin="0, 12, 0, 0"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Padding="10"
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
</Grid>
<StackLayout Spacing="0"
Padding="10,0">
<Grid IsVisible="{Binding IsPolicyInEffect}"
Margin="0, 12, 0, 0"
Padding="10,0"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Frame Padding="10"
Margin="0"
HasShadow="False"
BackgroundColor="Transparent"
BorderColor="Accent">
<Label
Text="{u:I18n PasswordGeneratorPolicyInEffect}"
StyleClass="text-muted, text-sm, text-bold"
HorizontalTextAlignment="Center" />
</Frame>
</Grid>
<Grid IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}"
StyleClass="box-row"
RowDefinitions="Auto"
ColumnDefinitions="*,Auto,Auto">
<controls:MonoLabel
x:Name="lblPassword"
StyleClass="text-lg, text-html"
Text="{Binding ColoredPassword, Mode=OneWay}"
Margin="0, 20" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyPassword}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateCommand}"
Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GeneratePassword}" />
</Grid>
<Grid IsVisible="{Binding IsUsername}"
StyleClass="box-row"
RowDefinitions="Auto"
ColumnDefinitions="*,Auto,Auto">
<controls:MonoLabel
x:Name="lblUsername"
StyleClass="text-lg, text-html"
Text="{Binding ColoredUsername, Mode=OneWay}"
Margin="0, 20"
HorizontalTextAlignment="Center"
HorizontalOptions="CenterAndExpand"
LineBreakMode="CharacterWrap" />
<Button Text="{u:I18n RegeneratePassword}"
StyleClass="btn-primary"
HorizontalOptions="FillAndExpand"
Clicked="Regenerate_Clicked"></Button>
<Button Text="{u:I18n CopyPassword}"
HorizontalOptions="FillAndExpand"
Clicked="Copy_Clicked"></Button>
HorizontalOptions="Start" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
Command="{Binding CopyCommand}"
Grid.Column="1"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyUsername}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding RegenerateUsernameCommand}"
Grid.Column="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" />
</Grid>
<BoxView StyleClass="box-row-separator"/>
<StackLayout StyleClass="box"
IsVisible="{Binding ShowTypePicker}"
Padding="0,10">
<Label
Text="{u:I18n WhatWouldYouLikeToGenerate}"
StyleClass="box-label" />
<Picker
x:Name="_typePicker"
ItemsSource="{Binding GeneratorTypeOptions, Mode=OneTime}"
SelectedItem="{Binding GeneratorTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
</StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n Options, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-input">
<Label Text="{u:I18n Options, Header=True}"
StyleClass="box-header, box-header-platform"
Margin="0,10,0,0"/>
<!--USERNAME OPTIONS-->
<StackLayout IsVisible="{Binding IsUsername}">
<StackLayout Orientation="Horizontal">
<Label
Text="{u:I18n Type}"
Text="{u:I18n UsernameType}"
StyleClass="box-label"
VerticalOptions="Center"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.QuestionCircle}}"
Command="{Binding UsernameTypePromptHelpCommand}"
TextColor="{DynamicResource HyperlinkColor}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n UsernamePromptHelpLink}"
VerticalOptions="Center"/>
</StackLayout>
<Picker
x:Name="_usernameTypePicker"
ItemsSource="{Binding UsernameTypeOptions, Mode=OneTime}"
SelectedItem="{Binding UsernameTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label
StyleClass="box-footer-label"
Text="{Binding UsernameTypeDescriptionLabel}" />
<!--PLUS ADDRESSED EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.PlusAddressedEmail}}">
<Label Text="{u:I18n EmailRequiredParenthesis}"
StyleClass="box-label" />
<Entry x:Name="_plusAddressedEmailEntry"
Text="{Binding PlusAddressedEmail}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Picker IsVisible="{Binding ShowUsernameEmailType}"
x:Name="_plusAddressedEmailTypePicker"
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
SelectedItem="{Binding PlusAddressedEmailTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n Website}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{Binding EmailWebsite}"
StyleClass="box-value" />
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
StyleClass="box-row-separator"
Margin="0,10,0,0" />
</StackLayout>
<!--CATCH-ALL EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.CatchAllEmail}}">
<Label
Text="{u:I18n DomainNameRequiredParenthesis}"
StyleClass="box-label" />
<Entry
x:Name="_catchAllEmailDomainNameEntry"
Text="{Binding CatchAllEmailDomain}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n EmailType}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Picker IsVisible="{Binding ShowUsernameEmailType}"
x:Name="_catchallEmailTypePicker"
ItemsSource="{Binding UsernameEmailTypeOptions, Mode=OneTime}"
SelectedItem="{Binding CatchAllEmailTypeSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{u:I18n Website}"
StyleClass="box-label"
Margin="0,10,0,0" />
<Label IsVisible="{Binding ShowUsernameEmailType}"
Text="{Binding EmailWebsite}"
StyleClass="box-value"/>
<BoxView IsVisible="{Binding ShowUsernameEmailType}"
StyleClass="box-row-separator"
Margin="0,10,0,0"/>
</StackLayout>
<!--FORWARDED EMAIL OPTIONS-->
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.ForwardedEmailAlias}}">
<Label
Text="{u:I18n Service}"
StyleClass="box-label" />
<Picker
x:Name="_typePicker"
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
SelectedIndex="{Binding TypeSelectedIndex}"
x:Name="_serviceTypePicker"
ItemsSource="{Binding ForwardedEmailServiceTypeOptions, Mode=OneTime}"
SelectedItem="{Binding ForwardedEmailServiceSelected}"
ItemDisplayBinding="{Binding ., Converter={StaticResource localizableEnum}}"
StyleClass="box-value" />
<!--ANONADDY OPTIONS-->
<Grid IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Margin="0,10,0,0"
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_anonAddyApiAccessTokenEntry"
Text="{Binding AnonAddyApiAccessToken}"
IsPassword="{Binding ShowAnonAddyApiAccessToken, Converter={StaticResource inverseBool}}"
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowAnonAddyHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<Label IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
Text="{u:I18n DomainNameRequiredParenthesis}"
StyleClass="box-label"
Margin="0,10,0,0"/>
<Entry IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.AnonAddy}}"
x:Name="_anonAddyDomainNameEntry"
Text="{Binding AnonAddyDomainName}"
StyleClass="box-value"/>
<!--FIREFOX RELAY OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.FirefoxRelay}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIAccessToken}"
StyleClass="box-label"/>
<Entry
x:Name="_firefoxRelayApiAccessTokenEntry"
Text="{Binding FirefoxRelayApiAccessToken}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowFirefoxRelayApiAccessToken, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowFirefoxRelayHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
<!--SIMPLELOGIN OPTIONS-->
<Grid StyleClass="box-row, box-row-input"
IsVisible="{Binding ForwardedEmailServiceSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:ForwardedEmailServiceType.SimpleLogin}}"
Grid.RowDefinitions="Auto,*"
Grid.ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n APIKeyRequiredParenthesis}"
StyleClass="box-label"/>
<Entry
x:Name="_simpleLoginApiKeyEntry"
Text="{Binding SimpleLoginApiKey}"
StyleClass="box-value"
Grid.Row="1"
IsPassword="{Binding ShowSimpleLoginApiKey, Converter={StaticResource inverseBool}}"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowSimpleLoginHiddenValueIcon}"
Command="{Binding ToggleForwardedEmailHiddenValueCommand}"
Grid.Row="1"
Grid.Column="1"/>
</Grid>
</StackLayout>
<!--RANDOM WORD OPTIONS-->
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
<Label
Text="{u:I18n Capitalize}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding CapitalizeRandomWordUsername}"
StyleClass="box-value"
HorizontalOptions="End" />
</Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" />
<Grid IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}">
<Label
Text="{u:I18n IncludeNumber}"
StyleClass="box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding IncludeNumberRandomWordUsername}"
StyleClass="box-value"
HorizontalOptions="End" />
</Grid>
<BoxView IsVisible="{Binding UsernameTypeSelected, Converter={StaticResource enumToBool}, ConverterParameter={x:Static enums:UsernameType.RandomWord}}"
StyleClass="box-row-separator" />
</StackLayout>
<!--PASSWORD OPTIONS-->
<StackLayout IsVisible="{Binding IsUsername, Converter={StaticResource inverseBool}}">
<StackLayout StyleClass="box-row, box-row-input">
<Label
Text="{u:I18n PasswordType}"
StyleClass="box-label" />
<Picker
x:Name="_passwordTypePicker"
ItemsSource="{Binding PasswordTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding PasswordTypeSelectedIndex}"
StyleClass="box-value" />
</StackLayout>
<StackLayout Spacing="0"

View File

@@ -18,7 +18,7 @@ namespace Bit.App.Pages
private readonly Action<string> _selectAction;
private readonly TabsPage _tabsPage;
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null)
public GeneratorPage(bool fromTabPage, Action<string> selectAction = null, TabsPage tabsPage = null, bool isUsernameGenerator = false, string emailWebsite = null, bool editMode = false)
{
_tabsPage = tabsPage;
InitializeComponent();
@@ -27,6 +27,10 @@ namespace Bit.App.Pages
_vm.Page = this;
_fromTabPage = fromTabPage;
_selectAction = selectAction;
_vm.ShowTypePicker = fromTabPage;
_vm.IsUsername = isUsernameGenerator;
_vm.EmailWebsite = emailWebsite;
_vm.EditMode = editMode;
var isIos = Device.RuntimePlatform == Device.iOS;
if (selectAction != null)
{
@@ -47,10 +51,12 @@ namespace Bit.App.Pages
ToolbarItems.Add(_historyItem);
}
}
if (isIos)
{
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
}
_typePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_passwordTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_usernameTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_serviceTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_plusAddressedEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_catchallEmailTypePicker.On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUpdateMode(UpdateMode.WhenFinished);
}
public async Task InitAsync()
@@ -97,16 +103,6 @@ namespace Bit.App.Pages
return base.OnBackButtonPressed();
}
private async void Regenerate_Clicked(object sender, EventArgs e)
{
await _vm.RegenerateAsync();
}
private async void Copy_Clicked(object sender, EventArgs e)
{
await _vm.CopyAsync();
}
private async void More_Clicked(object sender, EventArgs e)
{
if (!DoOnce())
@@ -124,7 +120,7 @@ namespace Bit.App.Pages
private void Select_Clicked(object sender, EventArgs e)
{
_selectAction?.Invoke(_vm.Password);
_selectAction?.Invoke(_vm.IsUsername ? _vm.Username : _vm.Password);
}
private async void History_Clicked(object sender, EventArgs e)
@@ -150,7 +146,20 @@ namespace Bit.App.Pages
{
await base.UpdateOnThemeChanged();
await Device.InvokeOnMainThreadAsync(() => _vm?.RedrawPassword());
await Device.InvokeOnMainThreadAsync(() =>
{
if (_vm != null)
{
if (_vm.IsUsername)
{
_vm.RedrawUsername();
}
else
{
_vm.RedrawPassword();
}
}
});
}
}
}

View File

@@ -1,10 +1,16 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
{
@@ -13,11 +19,16 @@ namespace Bit.App.Pages
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options;
private UsernameGenerationOptions _usernameOptions;
private PasswordGeneratorPolicyOptions _enforcedPolicyOptions;
private string _password;
private bool _isPassword;
private bool _isUsername;
private bool _uppercase;
private bool _lowercase;
private bool _number;
@@ -30,21 +41,69 @@ namespace Bit.App.Pages
private string _wordSeparator;
private bool _capitalize;
private bool _includeNumber;
private int _typeSelectedIndex;
private string _username;
private GeneratorType _generatorTypeSelected;
private int _passwordTypeSelectedIndex;
private bool _doneIniting;
private bool _showTypePicker;
private string _emailWebsite;
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private bool _editMode;
public GeneratorPageViewModel()
{
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
"passwordGenerationService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
_tokenService = ServiceContainer.Resolve<ITokenService>();
PageTitle = AppResources.PasswordGenerator;
TypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> {
GeneratorType.Password,
GeneratorType.Username
};
PasswordTypeOptions = new List<string> { AppResources.Password, AppResources.Passphrase };
UsernameTypeOptions = new List<UsernameType> {
UsernameType.PlusAddressedEmail,
UsernameType.CatchAllEmail,
UsernameType.ForwardedEmailAlias,
UsernameType.RandomWord
};
ForwardedEmailServiceTypeOptions = new List<ForwardedEmailServiceType> {
ForwardedEmailServiceType.AnonAddy,
ForwardedEmailServiceType.FirefoxRelay,
ForwardedEmailServiceType.SimpleLogin
};
UsernameEmailTypeOptions = new List<UsernameEmailType>
{
UsernameEmailType.Random,
UsernameEmailType.Website
};
UsernameTypePromptHelpCommand = new Command(UsernameTypePromptHelp);
RegenerateCommand = new AsyncCommand(RegenerateAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
RegenerateUsernameCommand = new AsyncCommand(RegenerateUsernameAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
ToggleForwardedEmailHiddenValueCommand = new AsyncCommand(ToggleForwardedEmailHiddenValueAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
CopyCommand = new AsyncCommand(CopyAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
}
public List<string> TypeOptions { get; set; }
public List<GeneratorType> GeneratorTypeOptions { get; set; }
public List<string> PasswordTypeOptions { get; set; }
public List<UsernameType> UsernameTypeOptions { get; set; }
public List<ForwardedEmailServiceType> ForwardedEmailServiceTypeOptions { get; set; }
public List<UsernameEmailType> UsernameEmailTypeOptions { get; set; }
public Command UsernameTypePromptHelpCommand { get; set; }
public ICommand RegenerateCommand { get; set; }
public ICommand RegenerateUsernameCommand { get; set; }
public ICommand ToggleForwardedEmailHiddenValueCommand { get; set; }
public ICommand CopyCommand { get; set; }
public string Password
{
@@ -56,7 +115,18 @@ namespace Bit.App.Pages
});
}
public string ColoredPassword => PasswordFormatter.FormatPassword(Password);
public string Username
{
get => _username;
set => SetProperty(ref _username, value,
additionalPropertyNames: new string[]
{
nameof(ColoredUsername)
});
}
public string ColoredPassword => GeneratedValueFormatter.Format(Password);
public string ColoredUsername => GeneratedValueFormatter.Format(Username);
public bool IsPassword
{
@@ -64,6 +134,32 @@ namespace Bit.App.Pages
set => SetProperty(ref _isPassword, value);
}
public bool IsUsername
{
get => _isUsername;
set => SetProperty(ref _isUsername, value);
}
public bool ShowTypePicker
{
get => _showTypePicker;
set => SetProperty(ref _showTypePicker, value);
}
public bool EditMode
{
get => _editMode;
set => SetProperty(ref _editMode, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public bool ShowUsernameEmailType
{
get => !string.IsNullOrWhiteSpace(EmailWebsite);
}
public int Length
{
get => _length;
@@ -235,6 +331,20 @@ namespace Bit.App.Pages
}
}
public string PlusAddressedEmail
{
get => _usernameOptions.PlusAddressedEmail;
set
{
if (_usernameOptions.PlusAddressedEmail != value)
{
_usernameOptions.PlusAddressedEmail = value;
TriggerPropertyChanged(nameof(PlusAddressedEmail));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public PasswordGeneratorPolicyOptions EnforcedPolicyOptions
{
get => _enforcedPolicyOptions;
@@ -247,24 +357,276 @@ namespace Bit.App.Pages
public bool IsPolicyInEffect => _enforcedPolicyOptions.InEffect();
public int TypeSelectedIndex
public GeneratorType GeneratorTypeSelected
{
get => _typeSelectedIndex;
get => _generatorTypeSelected;
set
{
if (SetProperty(ref _typeSelectedIndex, value))
if (SetProperty(ref _generatorTypeSelected, value))
{
IsPassword = value == 0;
var task = SaveOptionsAsync();
IsUsername = value == GeneratorType.Username;
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public int PasswordTypeSelectedIndex
{
get => _passwordTypeSelectedIndex;
set
{
if (SetProperty(ref _passwordTypeSelectedIndex, value))
{
IsPassword = value == 0;
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameType UsernameTypeSelected
{
get => _usernameOptions.Type;
set
{
if (_usernameOptions.Type != value)
{
_usernameOptions.Type = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public string UsernameTypeDescriptionLabel => GetUsernameTypeLabelDescription(UsernameTypeSelected);
public ForwardedEmailServiceType ForwardedEmailServiceSelected
{
get => _usernameOptions.ServiceType;
set
{
if (_usernameOptions.ServiceType != value)
{
_usernameOptions.ServiceType = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string CatchAllEmailDomain
{
get => _usernameOptions.CatchAllEmailDomain;
set
{
if (_usernameOptions.CatchAllEmailDomain != value)
{
_usernameOptions.CatchAllEmailDomain = value;
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string AnonAddyApiAccessToken
{
get => _usernameOptions.AnonAddyApiAccessToken;
set
{
if (_usernameOptions.AnonAddyApiAccessToken != value)
{
_usernameOptions.AnonAddyApiAccessToken = value;
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowAnonAddyApiAccessToken
{
get
{
return _showAnonAddyApiAccessToken;
}
set => SetProperty(ref _showAnonAddyApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowAnonAddyHiddenValueIcon)
});
}
public string ShowAnonAddyHiddenValueIcon => _showAnonAddyApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string AnonAddyDomainName
{
get => _usernameOptions.AnonAddyDomainName;
set
{
if (_usernameOptions.AnonAddyDomainName != value)
{
_usernameOptions.AnonAddyDomainName = value;
TriggerPropertyChanged(nameof(AnonAddyDomainName));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public string FirefoxRelayApiAccessToken
{
get => _usernameOptions.FirefoxRelayApiAccessToken;
set
{
if (_usernameOptions.FirefoxRelayApiAccessToken != value)
{
_usernameOptions.FirefoxRelayApiAccessToken = value;
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowFirefoxRelayApiAccessToken
{
get
{
return _showFirefoxRelayApiAccessToken;
}
set => SetProperty(ref _showFirefoxRelayApiAccessToken, value,
additionalPropertyNames: new string[]
{
nameof(ShowFirefoxRelayHiddenValueIcon)
});
}
public string ShowFirefoxRelayHiddenValueIcon => _showFirefoxRelayApiAccessToken ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public string SimpleLoginApiKey
{
get => _usernameOptions.SimpleLoginApiKey;
set
{
if (_usernameOptions.SimpleLoginApiKey != value)
{
_usernameOptions.SimpleLoginApiKey = value;
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
SaveUsernameOptionsAsync(false).FireAndForget();
}
}
}
public bool ShowSimpleLoginApiKey
{
get
{
return _showSimpleLoginApiKey;
}
set => SetProperty(ref _showSimpleLoginApiKey, value,
additionalPropertyNames: new string[]
{
nameof(ShowSimpleLoginHiddenValueIcon)
});
}
public string ShowSimpleLoginHiddenValueIcon => _showSimpleLoginApiKey ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool CapitalizeRandomWordUsername
{
get => _usernameOptions.CapitalizeRandomWordUsername;
set
{
if (_usernameOptions.CapitalizeRandomWordUsername != value)
{
_usernameOptions.CapitalizeRandomWordUsername = value;
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public bool IncludeNumberRandomWordUsername
{
get => _usernameOptions.IncludeNumberRandomWordUsername;
set
{
if (_usernameOptions.IncludeNumberRandomWordUsername != value)
{
_usernameOptions.IncludeNumberRandomWordUsername = value;
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType PlusAddressedEmailTypeSelected
{
get => _usernameOptions.PlusAddressedEmailType;
set
{
if (_usernameOptions.PlusAddressedEmailType != value)
{
_usernameOptions.PlusAddressedEmailType = value;
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType CatchAllEmailTypeSelected
{
get => _usernameOptions.CatchAllEmailType;
set
{
if (_usernameOptions.CatchAllEmailType != value)
{
_usernameOptions.CatchAllEmailType = value;
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public string EmailWebsite
{
get => _emailWebsite;
set => SetProperty(ref _emailWebsite, value, additionalPropertyNames: new string[]
{
nameof(ShowUsernameEmailType)
});
}
public async Task InitAsync()
{
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
LoadFromOptions();
await RegenerateAsync();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync();
_usernameOptions.PlusAddressedEmail = _tokenService.GetEmail();
_usernameOptions.EmailWebsite = EmailWebsite;
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website;
if (!IsUsername)
{
await RegenerateAsync();
}
else
{
if (UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{
await RegenerateUsernameAsync();
}
else
{
Username = Constants.DefaultUsernameGenerated;
}
}
TriggerUsernamePropertiesChanged();
_doneIniting = true;
}
@@ -274,6 +636,11 @@ namespace Bit.App.Pages
await _passwordGenerationService.AddHistoryAsync(Password);
}
public async Task RegenerateUsernameAsync()
{
Username = await _usernameGenerationService.GenerateAsync(_usernameOptions);
}
public void RedrawPassword()
{
if (!string.IsNullOrEmpty(_password))
@@ -282,6 +649,14 @@ namespace Bit.App.Pages
}
}
public void RedrawUsername()
{
if (!string.IsNullOrEmpty(_username))
{
TriggerPropertyChanged(nameof(ColoredUsername));
}
}
public async Task SaveOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
@@ -291,6 +666,7 @@ namespace Bit.App.Pages
SetOptions();
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
await _passwordGenerationService.SaveOptionsAsync(_options);
LoadFromOptions();
if (regenerate)
{
@@ -298,6 +674,25 @@ namespace Bit.App.Pages
}
}
public async Task SaveUsernameOptionsAsync(bool regenerate = true)
{
if (!_doneIniting)
{
return;
}
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{
await RegenerateUsernameAsync();
}
else
{
Username = Constants.DefaultUsernameGenerated;
}
}
public async Task SliderChangedAsync()
{
await SaveOptionsAsync(false);
@@ -317,15 +712,28 @@ namespace Bit.App.Pages
public async Task CopyAsync()
{
await _clipboardService.CopyTextAsync(Password);
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
await _clipboardService.CopyTextAsync(IsUsername ? Username : Password);
_platformUtilsService.ShowToastForCopiedValue(IsUsername ? AppResources.Username : AppResources.Password);
}
public void UsernameTypePromptHelp()
{
try
{
_platformUtilsService.LaunchUri("https://bitwarden.com/help/generator/#username-types");
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
private void LoadFromOptions()
{
AllowAmbiguousChars = _options.AllowAmbiguousChar.GetValueOrDefault();
TypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = TypeSelectedIndex == 0;
PasswordTypeSelectedIndex = _options.Type == "passphrase" ? 1 : 0;
IsPassword = PasswordTypeSelectedIndex == 0;
MinNumber = _options.MinNumber.GetValueOrDefault();
MinSpecial = _options.MinSpecial.GetValueOrDefault();
Special = _options.Special.GetValueOrDefault();
@@ -339,10 +747,30 @@ namespace Bit.App.Pages
IncludeNumber = _options.IncludeNumber.GetValueOrDefault();
}
private void TriggerUsernamePropertiesChanged()
{
TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
TriggerPropertyChanged(nameof(IncludeNumberRandomWordUsername));
TriggerPropertyChanged(nameof(CapitalizeRandomWordUsername));
TriggerPropertyChanged(nameof(SimpleLoginApiKey));
TriggerPropertyChanged(nameof(FirefoxRelayApiAccessToken));
TriggerPropertyChanged(nameof(AnonAddyDomainName));
TriggerPropertyChanged(nameof(AnonAddyApiAccessToken));
TriggerPropertyChanged(nameof(CatchAllEmailDomain));
TriggerPropertyChanged(nameof(ForwardedEmailServiceSelected));
TriggerPropertyChanged(nameof(UsernameTypeSelected));
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(PlusAddressedEmail));
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
TriggerPropertyChanged(nameof(UsernameTypeDescriptionLabel));
}
private void SetOptions()
{
_options.AllowAmbiguousChar = AllowAmbiguousChars;
_options.Type = TypeSelectedIndex == 1 ? "passphrase" : "password";
_options.Type = PasswordTypeSelectedIndex == 1 ? "passphrase" : "password";
_options.MinNumber = MinNumber;
_options.MinSpecial = MinSpecial;
_options.Special = Special;
@@ -355,5 +783,51 @@ namespace Bit.App.Pages
_options.Capitalize = Capitalize;
_options.IncludeNumber = IncludeNumber;
}
private async void OnSubmitException(Exception ex)
{
_logger.Value.Exception(ex);
if (IsUsername && UsernameTypeSelected == UsernameType.ForwardedEmailAlias)
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(
AppResources.AnErrorHasOccurred, string.Format(AppResources.UnknownXErrorMessage, ForwardedEmailServiceSelected), AppResources.Ok));
}
else
{
await Device.InvokeOnMainThreadAsync(() => Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok));
}
}
private string GetUsernameTypeLabelDescription(UsernameType value)
{
switch (value)
{
case UsernameType.PlusAddressedEmail:
return AppResources.PlusAddressedEmailDescription;
case UsernameType.CatchAllEmail:
return AppResources.CatchAllEmailDescription;
case UsernameType.ForwardedEmailAlias:
return AppResources.ForwardedEmailDescription;
default:
return string.Empty;
}
}
private async Task ToggleForwardedEmailHiddenValueAsync()
{
switch (ForwardedEmailServiceSelected)
{
case ForwardedEmailServiceType.AnonAddy:
ShowAnonAddyApiAccessToken = !ShowAnonAddyApiAccessToken;
break;
case ForwardedEmailServiceType.FirefoxRelay:
ShowFirefoxRelayApiAccessToken = !ShowFirefoxRelayApiAccessToken;
break;
case ForwardedEmailServiceType.SimpleLogin:
ShowSimpleLoginApiKey = !ShowSimpleLoginApiKey;
break;
}
}
}
}

View File

@@ -19,6 +19,7 @@ namespace Bit.App.Pages
public class SendAddEditPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IFileService _fileService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private readonly IStateService _stateService;
@@ -51,6 +52,7 @@ namespace Bit.App.Pages
public SendAddEditPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
@@ -292,7 +294,7 @@ namespace Bit.App.Pages
public async Task ChooseFileAsync()
{
await _deviceActionService.SelectFileAsync();
await _fileService.SelectFileAsync();
}
public void ClearExpirationDate()

View File

@@ -144,7 +144,7 @@ namespace Bit.App.Pages
{
await LoadDataAsync();
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
if (MainPage)
{
groupedSends.Add(new SendGroupingsPageListGroup(

View File

@@ -83,7 +83,7 @@
StyleClass="box-value"
HorizontalOptions="End" />
<Button
Clicked="ToggleAccessibility"
Command="{Binding ToggleAccessibilityCommand}"
StyleClass="box-overlay"
RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0"

View File

@@ -55,14 +55,6 @@ namespace Bit.App.Pages
_vm.ToggleInlineAutofill();
}
private void ToggleAccessibility(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.ToggleAccessibility();
}
}
private void ToggleDrawOver(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -1,17 +1,22 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Services;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages
{
public class AutofillServicesPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IStateService _stateService;
private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private bool _autofillServiceToggled;
private bool _inlineAutofillToggled;
@@ -22,9 +27,14 @@ namespace Bit.App.Pages
public AutofillServicesPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
PageTitle = AppResources.AutofillServices;
ToggleAccessibilityCommand = new AsyncCommand(ToggleAccessibilityAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
}
#region Autofill Service
@@ -74,6 +84,8 @@ namespace Bit.App.Pages
#region Accessibility
public ICommand ToggleAccessibilityCommand { get; }
public string AccessibilityDescriptionLabel
{
get
@@ -163,7 +175,7 @@ namespace Bit.App.Pages
}
else
{
_deviceActionService.DisableAutofillService();
_autofillHandler.DisableAutofillService();
}
}
@@ -176,8 +188,18 @@ namespace Bit.App.Pages
InlineAutofillToggled = !InlineAutofillToggled;
}
public void ToggleAccessibility()
public async Task ToggleAccessibilityAsync()
{
if (!_autofillHandler.AutofillAccessibilityServiceRunning())
{
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
AppResources.Decline);
if (!accept)
{
return;
}
}
_deviceActionService.OpenAccessibilitySettings();
}
@@ -193,9 +215,9 @@ namespace Bit.App.Pages
public void UpdateEnabled()
{
AutofillServiceToggled =
_deviceActionService.HasAutofillService() && _deviceActionService.AutofillServiceEnabled();
AccessibilityToggled = _deviceActionService.AutofillAccessibilityServiceRunning();
DrawOverToggled = _deviceActionService.AutofillAccessibilityOverlayPermitted();
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
AccessibilityToggled = _autofillHandler.AutofillAccessibilityServiceRunning();
DrawOverToggled = _autofillHandler.AutofillAccessibilityOverlayPermitted();
}
private async Task UpdateInlineAutofillToggledAsync()

View File

@@ -16,6 +16,7 @@ namespace Bit.App.Pages
public class ExportVaultPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IFileService _fileService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly II18nService _i18nService;
private readonly IExportService _exportService;
@@ -39,6 +40,7 @@ namespace Bit.App.Pages
public ExportVaultPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
@@ -182,7 +184,7 @@ namespace Bit.App.Pages
_defaultFilename = _exportService.GetFileName(null, fileFormat);
_exportResult = Encoding.UTF8.GetBytes(data);
if (!_deviceActionService.SaveFile(_exportResult, null, _defaultFilename, null))
if (!_fileService.SaveFile(_exportResult, null, _defaultFilename, null))
{
ClearResult();
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
@@ -220,7 +222,7 @@ namespace Bit.App.Pages
public async void SaveFileSelected(string contentUri, string filename)
{
if (_deviceActionService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
if (_fileService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
{
ClearResult();
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));

View File

@@ -1,5 +1,6 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
@@ -9,12 +10,12 @@ namespace Bit.App.Pages
{
public partial class OptionsPage : BaseContentPage
{
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly OptionsPageViewModel _vm;
public OptionsPage()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
InitializeComponent();
_vm = BindingContext as OptionsPageViewModel;
_vm.Page = this;
@@ -25,7 +26,7 @@ namespace Bit.App.Pages
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);
_vm.ShowAndroidAutofillSettings = _deviceActionService.SupportsAutofillService();
_vm.ShowAndroidAutofillSettings = _autofillHandler.SupportsAutofillService();
}
else
{

View File

@@ -56,12 +56,14 @@ namespace Bit.App.Pages
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
new KeyValuePair<string, string>(ThemeManager.SolarizedDark, AppResources.SolarizedDark),
};
AutoDarkThemeOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(ThemeManager.Dark, AppResources.Dark),
new KeyValuePair<string, string>(ThemeManager.Black, AppResources.Black),
new KeyValuePair<string, string>(ThemeManager.Nord, AppResources.Nord),
new KeyValuePair<string, string>(ThemeManager.SolarizedDark, AppResources.SolarizedDark),
};
UriMatchOptions = new List<KeyValuePair<UriMatchType?, string>>
{

View File

@@ -20,6 +20,7 @@ namespace Bit.App.Pages
private readonly ICryptoService _cryptoService;
private readonly IStateService _stateService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IEnvironmentService _environmentService;
private readonly IMessagingService _messagingService;
private readonly IVaultTimeoutService _vaultTimeoutService;
@@ -30,7 +31,7 @@ namespace Bit.App.Pages
private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private readonly IPushNotificationService _pushNotificationService;
private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric;
@@ -42,6 +43,7 @@ namespace Bit.App.Pages
private string _vaultTimeoutActionDisplayValue;
private bool _showChangeMasterPassword;
private bool _reportLoggingEnabled;
private bool _approvePasswordlessLoginRequests;
private List<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
@@ -73,6 +75,7 @@ namespace Bit.App.Pages
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
@@ -83,6 +86,7 @@ namespace Bit.App.Pages
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
PageTitle = AppResources.Settings;
@@ -133,6 +137,7 @@ namespace Bit.App.Pages
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
!await _keyConnectorService.GetUsesKeyConnector();
_reportLoggingEnabled = await _loggerService.IsEnabled();
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
BuildList();
}
@@ -326,6 +331,38 @@ namespace Bit.App.Pages
BuildList();
}
public async Task ApproveLoginRequestsAsync()
{
var options = new[]
{
CreateSelectableOption(AppResources.Yes, _approvePasswordlessLoginRequests),
CreateSelectableOption(AppResources.No, !_approvePasswordlessLoginRequests),
};
var selection = await Page.DisplayActionSheet(AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Cancel, null, options);
if (selection == null || selection == AppResources.Cancel)
{
return;
}
_approvePasswordlessLoginRequests = CompareSelection(selection, AppResources.Yes);
await _stateService.SetApprovePasswordlessLoginsAsync(_approvePasswordlessLoginRequests);
BuildList();
if (!_approvePasswordlessLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
{
return;
}
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(AppResources.ReceivePushNotificationsForNewLoginRequests, title: string.Empty, confirmText: AppResources.Settings, cancelText: AppResources.NoThanks);
if (openAppSettingsResult)
{
_deviceActionService.OpenAppSettings();
}
}
public async Task VaultTimeoutActionAsync()
{
var options = _vaultTimeoutActions.Select(o =>
@@ -419,7 +456,7 @@ namespace Bit.App.Pages
else if (await _platformUtilsService.SupportsBiometricAsync())
{
_biometric = await _platformUtilsService.AuthenticateBiometricAsync(null,
_deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
Device.RuntimePlatform == Device.Android ? "." : null);
}
if (_biometric == current)
{
@@ -450,7 +487,7 @@ namespace Bit.App.Pages
autofillItems.Add(new SettingsPageListItem
{
Name = AppResources.AutofillServices,
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
SubLabel = _autofillHandler.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
});
}
@@ -504,6 +541,12 @@ namespace Bit.App.Pages
ExecuteAsync = () => UpdatePinAsync()
},
new SettingsPageListItem
{
Name = AppResources.ApproveLoginRequests,
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
ExecuteAsync = () => ApproveLoginRequestsAsync()
},
new SettingsPageListItem
{
Name = AppResources.LockNow,
ExecuteAsync = () => LockAsync()

View File

@@ -17,7 +17,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" />
<ToolbarItem Text="{u:I18n Save}" Command="{Binding SubmitAsyncCommand}" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>

View File

@@ -51,14 +51,6 @@ namespace Bit.App.Pages
}
}
private async void Save_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async void ChooseFile_Clicked(object sender, EventArgs e)
{
if (DoOnce())

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.Core.Abstractions;
@@ -8,6 +10,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@@ -15,11 +18,13 @@ namespace Bit.App.Pages
public class AttachmentsPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IFileService _fileService;
private readonly ICipherService _cipherService;
private readonly ICryptoService _cryptoService;
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ILogger _logger;
private CipherView _cipher;
private Cipher _cipherDomain;
private bool _hasAttachments;
@@ -30,13 +35,16 @@ namespace Bit.App.Pages
public AttachmentsPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_logger = ServiceContainer.Resolve<ILogger>();
Attachments = new ExtendedObservableCollection<AttachmentView>();
DeleteAttachmentCommand = new Command<AttachmentView>(DeleteAsync);
SubmitAsyncCommand = new AsyncCommand(SubmitAsync, allowsMultipleExecutions: false);
PageTitle = AppResources.Attachments;
}
@@ -59,6 +67,7 @@ namespace Bit.App.Pages
}
public byte[] FileData { get; set; }
public Command DeleteAttachmentCommand { get; set; }
public ICommand SubmitAsyncCommand { get; }
public async Task InitAsync()
{
@@ -125,6 +134,7 @@ namespace Bit.App.Pages
}
catch (ApiException e)
{
_logger.Exception(e);
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
@@ -132,6 +142,12 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred);
}
}
catch (Exception e)
{
_logger.Exception(e);
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
}
return false;
}
@@ -142,7 +158,7 @@ namespace Bit.App.Pages
{
_vaultTimeoutService.DelayLockAndLogoutMs = 60000;
}
await _deviceActionService.SelectFileAsync();
await _fileService.SelectFileAsync();
}
private async void DeleteAsync(AttachmentView attachment)

View File

@@ -6,6 +6,7 @@ using Bit.App.Controls;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Helpers;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@@ -21,10 +22,12 @@ namespace Bit.App.Pages
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly ICipherService _cipherService;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IMessagingService _messagingService;
private readonly ICipherHelper _cipherHelper;
private readonly ILogger _logger;
private bool _showNoData;
@@ -37,9 +40,11 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_cipherHelper = ServiceContainer.Resolve<ICipherHelper>("cipherHelper");
_logger = ServiceContainer.Resolve<ILogger>("logger");
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
@@ -180,7 +185,7 @@ namespace Bit.App.Pages
}
if (_deviceActionService.SystemMajorVersion() < 21)
{
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
}
else
{
@@ -232,7 +237,7 @@ namespace Bit.App.Pages
}
if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
{
_deviceActionService.Autofill(cipher);
_autofillHandler.Autofill(cipher);
}
}
}
@@ -241,7 +246,7 @@ namespace Bit.App.Pages
{
if ((Page as BaseContentPage).DoOnce())
{
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
}
}
}

View File

@@ -14,6 +14,7 @@ namespace Bit.App.Pages
{
private readonly IAuditService _auditService;
protected readonly IDeviceActionService _deviceActionService;
protected readonly IFileService _fileService;
protected readonly ILogger _logger;
protected readonly IPlatformUtilsService _platformUtilsService;
private CipherView _cipher;
@@ -22,6 +23,7 @@ namespace Bit.App.Pages
public BaseCipherViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_fileService = ServiceContainer.Resolve<IFileService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
@@ -73,4 +75,3 @@ namespace Bit.App.Pages
}
}
}

View File

@@ -9,6 +9,8 @@
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:behaviors="clr-namespace:Bit.App.Behaviors"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:dts="clr-namespace:Bit.App.Lists.DataTemplateSelectors"
xmlns:il="clr-namespace:Bit.App.Lists.ItemLayouts.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:CipherAddEditPageViewModel"
x:Name="_page"
@@ -53,6 +55,25 @@
IsDestructive="True"
x:Name="_deleteItem"
x:Key="deleteItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
TextTemplate="{StaticResource TextCustomFieldDataTemplate}"
BooleanTemplate="{StaticResource BooleanCustomFieldDataTemplate}"
HiddenTemplate="{StaticResource HiddenCustomFieldDataTemplate}"
LinkedTemplate="{StaticResource LinkedCustomFieldDataTemplate}"/>
</ResourceDictionary>
</ContentPage.Resources>
@@ -107,15 +128,26 @@
StyleClass="box-value" />
</StackLayout>
<StackLayout IsVisible="{Binding IsLogin}" Spacing="0" Padding="0">
<StackLayout StyleClass="box-row, box-row-input">
<Label
<Grid StyleClass="box-row, box-row-input"
RowDefinitions="Auto,*"
ColumnDefinitions="*,Auto">
<Label
Text="{u:I18n Username}"
StyleClass="box-label" />
StyleClass="box-label"/>
<Entry
x:Name="_loginUsernameEntry"
Text="{Binding Cipher.Login.Username}"
StyleClass="box-value" />
</StackLayout>
StyleClass="box-value"
Grid.Row="1"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding Source={x:Static core:BitwardenIcons.Generate}}"
Command="{Binding GenerateUsernameCommand}"
Grid.Column="1"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n GenerateUsername}" />
</Grid>
<Grid StyleClass="box-row, box-row-input">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -636,101 +668,10 @@
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CipherAddEditPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row, box-row-input">
<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}"
IsVisible="{Binding IsBooleanType, Mode=OneWay, Converter={StaticResource inverseBool}}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Field.Name, Mode=OneWay}"
IsVisible="{Binding IsBooleanType, Mode=OneWay}"
StyleClass="box-value"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2" />
<Entry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:MonoEntry
Text="{Binding Field.Value}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsHiddenType}"
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>
<StackLayout
StyleClass="box-row, box-row-input"
IsVisible="{Binding IsLinkedType}">
<Picker
x:Name="_linkedFieldOptionPicker"
ItemsSource="{Binding LinkedFieldOptions, Mode=OneTime}"
SelectedIndex="{Binding LinkedFieldOptionSelectedIndex}"
ItemDisplayBinding="{Binding Key}"
StyleClass="box-value" />
</StackLayout>
<Switch
IsToggled="{Binding BooleanValue}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
IsVisible="{Binding IsBooleanType}" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
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.Cog}}"
Command="{Binding BindingContext.FieldOptionsCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
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 IsBooleanType}" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
<StackLayout
Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
<Button Text="{u:I18n NewCustomField}" StyleClass="box-button-row"
Clicked="NewField_Clicked"></Button>
</StackLayout>

View File

@@ -19,6 +19,7 @@ namespace Bit.App.Pages
private readonly AppOptions _appOptions;
private readonly IStateService _stateService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
@@ -40,6 +41,7 @@ namespace Bit.App.Pages
{
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
@@ -350,8 +352,8 @@ namespace Bit.App.Pages
}
}
else if (Device.RuntimePlatform == Device.Android &&
!_deviceActionService.AutofillAccessibilityServiceRunning() &&
!_deviceActionService.AutofillServiceEnabled())
!_autofillHandler.AutofillAccessibilityServiceRunning() &&
!_autofillHandler.AutofillServiceEnabled())
{
await DisplayAlert(AppResources.BitwardenAutofillService,
AppResources.BitwardenAutofillServiceAlert2, AppResources.Ok);

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Lists.ItemViewModels.CustomFields;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.Core;
@@ -25,7 +26,9 @@ namespace Bit.App.Pages
private readonly IMessagingService _messagingService;
private readonly IEventService _eventService;
private readonly IPolicyService _policyService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
private readonly IAutofillHandler _autofillHandler;
private bool _showNotesSeparator;
private bool _showPassword;
@@ -74,18 +77,21 @@ namespace Bit.App.Pages
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
ToggleCardNumberCommand = new Command(ToggleCardNumber);
ToggleCardCodeCommand = new Command(ToggleCardCode);
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
FieldOptionsCommand = new Command<ICustomFieldItemViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
GenerateUsernameCommand = new AsyncCommand(GenerateUsernameAsync, onException: ex => OnGenerateUsernameException(ex), allowsMultipleExecutions: false);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
AllowPersonal = true;
@@ -145,6 +151,7 @@ namespace Bit.App.Pages
public Command FieldOptionsCommand { get; set; }
public Command PasswordPromptHelpCommand { get; set; }
public AsyncCommand CopyCommand { get; set; }
public AsyncCommand GenerateUsernameCommand { get; set; }
public string CipherId { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; }
@@ -159,7 +166,7 @@ namespace Bit.App.Pages
public List<KeyValuePair<string, string>> FolderOptions { get; set; }
public List<KeyValuePair<string, string>> OwnershipOptions { get; set; }
public ExtendedObservableCollection<LoginUriView> Uris { get; set; }
public ExtendedObservableCollection<CipherAddEditPageFieldViewModel> Fields { get; set; }
public ExtendedObservableCollection<ICustomFieldItemViewModel> Fields { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
public int TypeSelectedIndex
@@ -412,7 +419,7 @@ namespace Bit.App.Pages
}
if (Cipher.Fields != null)
{
Fields.ResetWithRange(Cipher.Fields?.Select(f => new CipherAddEditPageFieldViewModel(Cipher, f)));
Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
}
}
@@ -503,7 +510,7 @@ namespace Bit.App.Pages
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
{
// Close and go back to app
_deviceActionService.CloseAutofill();
_autofillHandler.CloseAutofill();
}
else
{
@@ -592,6 +599,32 @@ namespace Bit.App.Pages
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
public async Task GenerateUsernameAsync()
{
if (!string.IsNullOrWhiteSpace(Cipher?.Login?.Username)
&& !await _platformUtilsService.ShowDialogAsync(AppResources.AreYouSureYouWantToOverwriteTheCurrentUsername, null, AppResources.Yes, AppResources.No))
{
return;
}
var website = Cipher?.Login?.Uris?.FirstOrDefault()?.Host;
var page = new GeneratorPage(false, async (username) =>
{
try
{
Cipher.Login.Username = username;
TriggerCipherChanged();
await Page.Navigation.PopModalAsync();
}
catch (Exception ex)
{
OnGenerateUsernameException(ex);
}
}, isUsernameGenerator: true, emailWebsite: website, editMode: true);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
public async void UriOptions(LoginUriView uri)
{
if (!(Page as CipherAddEditPage).DoOnce())
@@ -630,7 +663,7 @@ namespace Bit.App.Pages
Uris.Add(new LoginUriView());
}
public async void FieldOptions(CipherAddEditPageFieldViewModel field)
public async void FieldOptions(ICustomFieldItemViewModel field)
{
if (!(Page as CipherAddEditPage).DoOnce())
{
@@ -692,15 +725,15 @@ namespace Bit.App.Pages
}
if (Fields == null)
{
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
Fields = new ExtendedObservableCollection<ICustomFieldItemViewModel>();
}
var type = fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key;
Fields.Add(new CipherAddEditPageFieldViewModel(Cipher, new FieldView
Fields.Add(_customFieldItemFactory.CreateCustomFieldItem(new FieldView
{
Type = type,
Name = string.IsNullOrWhiteSpace(name) ? null : name,
NewField = true,
}));
}, true, Cipher, null, null, FieldOptionsCommand));
}
}
@@ -762,7 +795,7 @@ namespace Bit.App.Pages
TriggerCipherChanged();
// Linked Custom Fields only apply to a specific item type
foreach (var field in Fields.Where(f => f.IsLinkedType).ToList())
foreach (var field in Fields.OfType<LinkedCustomFieldItemViewModel>().ToList())
{
Fields.Remove(field);
}
@@ -838,114 +871,11 @@ namespace Bit.App.Pages
_logger.Exception(ex);
}
}
}
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
private bool _booleanValue;
private int _linkedFieldOptionSelectedIndex;
private string[] _additionalFieldProperties = new string[]
private async void OnGenerateUsernameException(Exception ex)
{
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(IsLinkedType),
};
public CipherAddEditPageFieldViewModel(CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
BooleanValue = IsBooleanType && field.Value == "true";
LinkedFieldOptionSelectedIndex = !Field.LinkedId.HasValue ? 0 :
LinkedFieldOptions.FindIndex(lfo => lfo.Value == Field.LinkedId.Value);
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value, additionalPropertyNames: _additionalFieldProperties);
}
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value,
additionalPropertyNames: new string[]
{
nameof(ShowHiddenValueIcon)
});
}
public bool BooleanValue
{
get => _booleanValue;
set
{
SetProperty(ref _booleanValue, value);
if (IsBooleanType)
{
Field.Value = value ? "true" : "false";
}
}
}
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();
}
public Command ToggleHiddenValueCommand { get; set; }
public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool IsTextType => _field.Type == FieldType.Text;
public bool IsBooleanType => _field.Type == FieldType.Boolean;
public bool IsHiddenType => _field.Type == FieldType.Hidden;
public bool IsLinkedType => _field.Type == FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && (_cipher.ViewPassword || _field.NewField);
public void ToggleHiddenValue()
{
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue && _cipher?.Id != null)
{
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var task = eventService.CollectAsync(EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
public void TriggerFieldChanged()
{
TriggerPropertyChanged(nameof(Field), _additionalFieldProperties);
}
private void LinkedFieldValueChanged()
{
if (Field != null && LinkedFieldOptionSelectedIndex > -1)
{
Field.LinkedId = LinkedFieldOptions.Find(lfo =>
lfo.Value == LinkedFieldOptions[LinkedFieldOptionSelectedIndex].Value).Value;
}
_logger.Exception(ex);
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
}

View File

@@ -8,6 +8,8 @@
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:dts="clr-namespace:Bit.App.Lists.DataTemplateSelectors"
xmlns:il="clr-namespace:Bit.App.Lists.ItemLayouts.CustomFields"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:CipherDetailsPageViewModel"
x:Name="_page"
@@ -46,6 +48,25 @@
<ToolbarItem Text="{u:I18n Clone}" Clicked="Clone_Clicked" Order="Secondary"
x:Name="_cloneItem" x:Key="cloneItem" />
<DataTemplate x:Key="TextCustomFieldDataTemplate">
<il:TextCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="BooleanCustomFieldDataTemplate">
<il:BooleanCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="HiddenCustomFieldDataTemplate">
<il:HiddenCustomFieldItemLayout />
</DataTemplate>
<DataTemplate x:Key="LinkedCustomFieldDataTemplate">
<il:LinkedCustomFieldItemLayout />
</DataTemplate>
<dts:CustomFieldItemTemplateSelector x:Key="CustomFieldItemTemplateSelector"
TextTemplate="{StaticResource TextCustomFieldDataTemplate}"
BooleanTemplate="{StaticResource BooleanCustomFieldDataTemplate}"
HiddenTemplate="{StaticResource HiddenCustomFieldDataTemplate}"
LinkedTemplate="{StaticResource LinkedCustomFieldDataTemplate}"/>
<ScrollView x:Key="scrollView" x:Name="_scrollView">
<StackLayout Spacing="20" x:Name="_mainLayout">
<StackLayout StyleClass="box">
@@ -559,85 +580,10 @@
<Label Text="{u:I18n CustomFields, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<controls:RepeaterView ItemsSource="{Binding Fields}">
<controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CipherDetailsPageFieldViewModel">
<StackLayout Spacing="0" Padding="0">
<Grid StyleClass="box-row">
<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" />
<Label
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsTextType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsLinkedType}" />
<controls:IconLabel
Text="{Binding ValueText, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="true"
AutomationProperties.Name="{Binding ValueAccessibilityText, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding IsBooleanType}"
Margin="0, 5, 0, 0" />
<StackLayout IsVisible="{Binding IsHiddenType}"
Grid.Row="1"
Grid.Column="0">
<controls:MonoLabel
Text="{Binding ColoredHiddenValue, Mode=OneWay}"
StyleClass="box-value, text-html"
IsVisible="{Binding ShowHiddenValue}" />
<controls:MonoLabel
Text="{Binding Field.MaskedValue, Mode=OneWay}"
StyleClass="box-value"
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
</StackLayout>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowHiddenValueIcon}"
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 BindingContext.CopyFieldCommand, Source={x:Reference _page}}"
CommandParameter="{Binding Field}"
IsVisible="{Binding ShowCopyButton}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Copy}" />
</Grid>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</DataTemplate>
</controls:RepeaterView.ItemTemplate>
</controls:RepeaterView>
<StackLayout
Spacing="0"
BindableLayout.ItemsSource="{Binding Fields}"
BindableLayout.ItemTemplateSelector="{StaticResource CustomFieldItemTemplateSelector}" />
</StackLayout>
<StackLayout StyleClass="box" IsVisible="{Binding ShowAttachments}">
<StackLayout StyleClass="box-row-header">

View File

@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Lists.ItemViewModels.CustomFields;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
@@ -18,7 +19,7 @@ using Xamarin.Forms;
namespace Bit.App.Pages
{
public class CipherDetailsPageViewModel : BaseCipherViewModel
public class CipherDetailsPageViewModel : BaseCipherViewModel, IPasswordPromptable
{
private readonly ICipherService _cipherService;
private readonly IStateService _stateService;
@@ -28,9 +29,10 @@ namespace Bit.App.Pages
private readonly IEventService _eventService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly ILocalizeService _localizeService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
private List<CipherDetailsPageFieldViewModel> _fields;
private List<ICustomFieldItemViewModel> _fields;
private bool _canAccessPremium;
private bool _showPassword;
private bool _showCardNumber;
@@ -38,8 +40,8 @@ namespace Bit.App.Pages
private string _totpCode;
private string _totpCodeFormatted;
private string _totpSec;
private double _totpInterval = Constants.TotpDefaultTimer;
private bool _totpLow;
private DateTime? _totpInterval = null;
private string _previousCipherId;
private byte[] _attachmentData;
private string _attachmentFilename;
@@ -58,6 +60,7 @@ namespace Bit.App.Pages
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
@@ -99,7 +102,7 @@ namespace Bit.App.Pages
nameof(CanEdit),
nameof(ShowUpgradePremiumTotpText)
};
public List<CipherDetailsPageFieldViewModel> Fields
public List<ICustomFieldItemViewModel> Fields
{
get => _fields;
set => SetProperty(ref _fields, value);
@@ -141,7 +144,7 @@ namespace Bit.App.Pages
public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity;
public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card;
public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote;
public FormattedString ColoredPassword => PasswordFormatter.FormatPassword(Cipher.Login.Password);
public FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password);
public FormattedString UpdatedText
{
get
@@ -199,7 +202,7 @@ namespace Bit.App.Pages
}
}
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && ShowTotp;
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && !Cipher.OrganizationUseTotp && ShowTotp;
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
public bool ShowIdentityAddress => IsIdentity && (
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
@@ -213,7 +216,7 @@ namespace Bit.App.Pages
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public string TotpCodeFormatted
{
get => _canAccessPremium ? _totpCodeFormatted : string.Empty;
get => ShowUpgradePremiumTotpText ? string.Empty : _totpCodeFormatted;
set => SetProperty(ref _totpCodeFormatted, value,
additionalPropertyNames: new string[]
{
@@ -238,7 +241,7 @@ namespace Bit.App.Pages
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
}
}
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / 30;
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / _totpInterval;
public bool IsDeleted => Cipher.IsDeleted;
public bool CanEdit => !Cipher.IsDeleted;
@@ -252,13 +255,17 @@ namespace Bit.App.Pages
}
Cipher = await cipher.DecryptAsync();
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
Fields = Cipher.Fields?.Select(f => new CipherDetailsPageFieldViewModel(this, Cipher, f)).ToList();
Fields = Cipher.Fields?
.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, false, Cipher, this, CopyFieldCommand, null))
.ToList();
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
(Cipher.OrganizationUseTotp || CanAccessPremium))
{
_totpTickHelper = new TotpHelper(Cipher);
_totpTickCancellationToken?.Cancel();
_totpInterval = _totpTickHelper.Interval;
_totpTickCancellationToken = new CancellationTokenSource();
_totpTickTask = new TimerTask(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
}
@@ -278,6 +285,7 @@ namespace Bit.App.Pages
await _totpTickHelper.GenerateNewTotpValues();
TotpSec = _totpTickHelper.TotpSec;
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
_totpInterval = _totpTickHelper.Interval;
}
catch (Exception ex)
{
@@ -423,7 +431,6 @@ namespace Bit.App.Pages
{
if (Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null)
{
_totpInterval = null;
return;
}
_totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp);
@@ -443,7 +450,6 @@ namespace Bit.App.Pages
else
{
TotpCodeFormatted = null;
_totpInterval = null;
}
}
@@ -487,7 +493,7 @@ namespace Bit.App.Pages
}
var canOpenFile = true;
if (!_deviceActionService.CanOpenFile(attachment.FileName))
if (!_fileService.CanOpenFile(attachment.FileName))
{
if (Device.RuntimePlatform == Device.iOS)
{
@@ -556,7 +562,7 @@ namespace Bit.App.Pages
public async void OpenAttachment(byte[] data, AttachmentView attachment)
{
if (!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName))
if (!_fileService.OpenFile(data, attachment.Id, attachment.FileName))
{
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
return;
@@ -567,7 +573,7 @@ namespace Bit.App.Pages
{
_attachmentData = data;
_attachmentFilename = attachment.FileName;
if (!_deviceActionService.SaveFile(_attachmentData, null, _attachmentFilename, null))
if (!_fileService.SaveFile(_attachmentData, null, _attachmentFilename, null))
{
ClearAttachmentData();
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment);
@@ -576,7 +582,7 @@ namespace Bit.App.Pages
public async void SaveFileSelected(string contentUri, string filename)
{
if (_deviceActionService.SaveFile(_attachmentData, null, filename ?? _attachmentFilename, contentUri))
if (_fileService.SaveFile(_attachmentData, null, filename ?? _attachmentFilename, contentUri))
{
ClearAttachmentData();
_platformUtilsService.ShowToast("success", null, AppResources.SaveAttachmentSuccess);
@@ -665,7 +671,7 @@ namespace Bit.App.Pages
}
}
internal async Task<bool> PromptPasswordAsync()
public async Task<bool> PromptPasswordAsync()
{
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
{
@@ -675,110 +681,4 @@ namespace Bit.App.Pages
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
}
}
public class CipherDetailsPageFieldViewModel : ExtendedViewModel
{
private II18nService _i18nService;
private CipherDetailsPageViewModel _vm;
private FieldView _field;
private CipherView _cipher;
private bool _showHiddenValue;
public CipherDetailsPageFieldViewModel(CipherDetailsPageViewModel vm, CipherView cipher, FieldView field)
{
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_vm = vm;
_cipher = cipher;
Field = field;
ToggleHiddenValueCommand = new Command(ToggleHiddenValue);
}
public FieldView Field
{
get => _field;
set => SetProperty(ref _field, value,
additionalPropertyNames: new string[]
{
nameof(ValueText),
nameof(ValueAccessibilityText),
nameof(IsBooleanType),
nameof(IsHiddenType),
nameof(IsTextType),
nameof(ShowCopyButton),
});
}
public bool ShowHiddenValue
{
get => _showHiddenValue;
set => SetProperty(ref _showHiddenValue, value,
additionalPropertyNames: new string[]
{
nameof(ShowHiddenValueIcon)
});
}
public string ValueText
{
get
{
if (IsBooleanType)
{
return _field.BoolValue ? BitwardenIcons.CheckSquare : BitwardenIcons.Square;
}
else if (IsLinkedType)
{
var i18nKey = _cipher.LinkedFieldI18nKey(Field.LinkedId.GetValueOrDefault());
return BitwardenIcons.Link + _i18nService.T(i18nKey);
}
else
{
return _field.Value;
}
}
}
public string ValueAccessibilityText
{
get
{
if (IsBooleanType)
{
return _field.BoolValue ? AppResources.Enabled : AppResources.Disabled;
}
return ValueText;
}
}
public FormattedString ColoredHiddenValue => PasswordFormatter.FormatPassword(_field.Value);
public Command ToggleHiddenValueCommand { get; set; }
public string ShowHiddenValueIcon => _showHiddenValue ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
public bool IsTextType => _field.Type == Core.Enums.FieldType.Text;
public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean;
public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden;
public bool IsLinkedType => _field.Type == Core.Enums.FieldType.Linked;
public bool ShowViewHidden => IsHiddenType && _cipher.ViewPassword;
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
!string.IsNullOrWhiteSpace(_field.Value) &&
!(IsHiddenType && !_cipher.ViewPassword) &&
_field.Type != FieldType.Linked;
public async void ToggleHiddenValue()
{
if (!await _vm.PromptPasswordAsync())
{
return;
}
ShowHiddenValue = !ShowHiddenValue;
if (ShowHiddenValue)
{
var eventService = ServiceContainer.Resolve<IEventService>("eventService");
var task = eventService.CollectAsync(
Core.Enums.EventType.Cipher_ClientToggledHiddenFieldVisible, _cipher.Id);
}
}
}
}

View File

@@ -1,8 +1,8 @@
using System;
using System.Linq;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -12,7 +12,7 @@ namespace Bit.App.Pages
public partial class CiphersPage : BaseContentPage
{
private readonly string _autofillUrl;
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private CiphersPageViewModel _vm;
private bool _hasFocused;
@@ -48,7 +48,7 @@ namespace Bit.App.Pages
{
NavigationPage.SetTitleView(this, _titleLayout);
}
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
}
public SearchBar SearchBar => _searchBar;
@@ -107,7 +107,7 @@ namespace Bit.App.Pages
}
else
{
_deviceActionService.CloseAutofill();
_autofillHandler.CloseAutofill();
}
}

View File

@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities.Helpers;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -20,10 +21,12 @@ namespace Bit.App.Pages
private readonly ICipherService _cipherService;
private readonly ISearchService _searchService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAutofillHandler _autofillHandler;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private readonly ICipherHelper _cipherHelper;
private CancellationTokenSource _searchCancellationTokenSource;
private readonly ILogger _logger;
@@ -37,10 +40,12 @@ namespace Bit.App.Pages
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_cipherHelper = ServiceContainer.Resolve<ICipherHelper>("cipherHelper");
_logger = ServiceContainer.Resolve<ILogger>("logger");
Ciphers = new ExtendedObservableCollection<CipherView>();
@@ -192,11 +197,11 @@ namespace Bit.App.Pages
}
if (_deviceActionService.SystemMajorVersion() < 21)
{
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
}
else
{
_deviceActionService.Autofill(cipher);
_autofillHandler.Autofill(cipher);
}
}
}
@@ -218,7 +223,7 @@ namespace Bit.App.Pages
{
if ((Page as BaseContentPage).DoOnce())
{
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
}
}
}

View File

@@ -33,7 +33,9 @@
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<xct:IsNotNullOrEmptyConverter x:Key="isNotNullOrEmptyConverter" />
<u:CipherTypeToSwipeActionGlyphConverter x:Key="cipherTypeToSwipeActionGlyphConverter" />
<u:CipherToSwipeBackgroundColor x:Key="cipherToSwipeBackgroundColor" />
<ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}"
Clicked="Sync_Clicked" Order="Secondary" />
@@ -53,6 +55,27 @@
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
</DataTemplate>
<DataTemplate x:Key="swipeableCipherTemplate"
x:DataType="pages:GroupingsPageListItem">
<SwipeView Threshold="{Binding Source={x:Reference _page}, Path=SwipeThreshold}">
<SwipeView.LeftItems>
<SwipeItems Mode="Execute">
<SwipeItem
BackgroundColor="{Binding Cipher, Converter={StaticResource cipherToSwipeBackgroundColor}}"
Command="{Binding Source={x:Reference _page}, Path=BindingContext.SwipeItemActionCommand}"
CommandParameter="{Binding .}"
IconImageSource="{Binding Cipher.Type, Converter={StaticResource cipherTypeToSwipeActionGlyphConverter}}">
</SwipeItem>
</SwipeItems>
</SwipeView.LeftItems>
<controls:CipherViewCell
Cipher="{Binding Cipher}"
ButtonCommand="{Binding BindingContext.CipherOptionsCommand, Source={x:Reference _page}}"
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
ApplyRipple="False"
BackgroundColor="{DynamicResource BackgroundColor}"/>
</SwipeView>
</DataTemplate>
<DataTemplate x:Key="authenticatorTemplate"
x:DataType="pages:GroupingsPageTOTPListItem">
@@ -113,8 +136,9 @@
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
HeaderTemplate="{StaticResource headerTemplate}"
CipherTemplate="{StaticResource cipherTemplate}"
GroupTemplate="{StaticResource groupTemplate}"
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
GroupTemplate="{StaticResource groupTemplate}" />
SwipeableCipherTemplate="{StaticResource swipeableCipherTemplate}" />
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
<StackLayout
@@ -165,7 +189,8 @@
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform"
ExtraDataForLogging="Groupings Page" />
ExtraDataForLogging="Groupings Page"
OnSwipeCommand="{Binding SwipeItemActionCommand}"/>
</RefreshView>
</StackLayout>
</ResourceDictionary>

View File

@@ -28,6 +28,19 @@ namespace Bit.App.Pages
private PreviousPageInfo _previousPage;
double _swipeThreshold;
public double SwipeThreshold
{
get
{
if (_swipeThreshold == default)
{
_swipeThreshold = (Content?.Width ?? 500) / 2;
}
return _swipeThreshold;
}
}
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)

View File

@@ -7,6 +7,7 @@ namespace Bit.App.Pages
public DataTemplate HeaderTemplate { get; set; }
public DataTemplate CipherTemplate { get; set; }
public DataTemplate GroupTemplate { get; set; }
public DataTemplate SwipeableCipherTemplate { get; set; }
public DataTemplate AuthenticatorTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
@@ -23,7 +24,16 @@ namespace Bit.App.Pages
if (item is GroupingsPageListItem listItem)
{
return listItem.Cipher != null ? CipherTemplate : GroupTemplate;
if (listItem.Cipher is null)
{
return GroupTemplate;
}
return CipherTemplate;
//return listItem.Cipher.Type != Core.Enums.CipherType.Identity && SwipeableCipherTemplate != null
// ? SwipeableCipherTemplate
// : CipherTemplate;
}
return null;

View File

@@ -22,7 +22,6 @@ namespace Bit.App.Pages
private bool _websiteIconsEnabled;
private string _iconImageSource = string.Empty;
public int interval { get; set; }
private double _progress;
private string _totpSec;
private string _totpCodeFormatted;
@@ -37,7 +36,6 @@ namespace Bit.App.Pages
Cipher = cipherView;
WebsiteIconsEnabled = websiteIconsEnabled;
interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);

View File

@@ -8,6 +8,7 @@ using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Utilities.Helpers;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
@@ -50,9 +51,9 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private readonly ICipherHelper _cipherHelper;
private readonly ILogger _logger;
public GroupingsPageViewModel()
@@ -66,9 +67,9 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_cipherHelper = ServiceContainer.Resolve<ICipherHelper>("cipherHelper");
_logger = ServiceContainer.Resolve<ILogger>("logger");
Loading = true;
@@ -79,6 +80,7 @@ namespace Bit.App.Pages
await LoadAsync();
});
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
SwipeItemActionCommand = new AsyncCommand<IGroupingsPageListItem>(SwipeItemActionAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
@@ -169,6 +171,8 @@ namespace Bit.App.Pages
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
public Command RefreshCommand { get; set; }
public Command<CipherView> CipherOptionsCommand { get; set; }
public IAsyncCommand<IGroupingsPageListItem> SwipeItemActionCommand { get; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
@@ -200,7 +204,6 @@ namespace Bit.App.Pages
{
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
}
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
_doingLoad = true;
LoadedOnce = true;
ShowNoData = false;
@@ -221,7 +224,7 @@ namespace Bit.App.Pages
NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1);
}
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
var hasFavorites = FavoriteCiphers?.Any() ?? false;
if (hasFavorites)
{
@@ -231,7 +234,7 @@ namespace Bit.App.Pages
}
if (MainPage)
{
AddTotpGroupItem(canAccessPremium, groupedItems, uppercaseGroupNames);
AddTotpGroupItem(groupedItems, uppercaseGroupNames);
groupedItems.Add(new GroupingsPageListGroup(
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
@@ -382,9 +385,9 @@ namespace Bit.App.Pages
}
}
private void AddTotpGroupItem(bool canAccessPremium, List<GroupingsPageListGroup> groupedItems, bool uppercaseGroupNames)
private void AddTotpGroupItem(List<GroupingsPageListGroup> groupedItems, bool uppercaseGroupNames)
{
if (canAccessPremium && TOTPCiphers?.Any() == true)
if (TOTPCiphers?.Any() == true)
{
groupedItems.Insert(0, new GroupingsPageListGroup(
AppResources.Totp, 1, uppercaseGroupNames, false)
@@ -401,7 +404,7 @@ namespace Bit.App.Pages
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
{
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
_totpTickCts?.Cancel();
if (ShowTotp)
{
@@ -537,10 +540,11 @@ namespace Bit.App.Pages
private async Task LoadDataAsync()
{
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
NoDataText = AppResources.NoItems;
_allCiphers = await GetAllCiphersAsync();
HasCiphers = _allCiphers.Any();
TOTPCiphers = _allCiphers.Where(c => c.IsDeleted == Deleted && c.Type == CipherType.Login && !string.IsNullOrEmpty(c.Login?.Totp)).ToList();
TOTPCiphers = _allCiphers.Where(c => c.IsDeleted == Deleted && c.Type == CipherType.Login && !string.IsNullOrEmpty(c.Login?.Totp) && (c.OrganizationUseTotp || canAccessPremium)).ToList();
FavoriteCiphers?.Clear();
NoFolderCiphers?.Clear();
_folderCounts.Clear();
@@ -713,7 +717,43 @@ namespace Bit.App.Pages
{
if ((Page as BaseContentPage).DoOnce())
{
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
await _cipherHelper.ShowCipherOptionsAsync(Page, cipher);
}
}
private async Task SwipeItemActionAsync(IGroupingsPageListItem listItem)
{
if (listItem is GroupingsPageListItem groupPageListItem && groupPageListItem.Cipher is CipherView cipher)
{
switch (cipher.Type)
{
case CipherType.Login:
if (string.IsNullOrEmpty(cipher.Login?.Password) || !await _cipherHelper.CopyPasswordAsync(cipher))
{
return;
}
break;
case CipherType.Card:
if (!await _cipherHelper.CopyCardNumberAsync(cipher))
{
return;
}
break;
case CipherType.SecureNote:
await _cipherHelper.CopyNotesAsync(cipher);
break;
default:
_logger.Error($"The cipher type {cipher.Type} does not have any swipe action associated");
return;
}
try
{
Xamarin.Essentials.Vibration.Vibrate();
}
catch (Xamarin.Essentials.FeatureNotSupportedException)
{
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More