1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

..

119 Commits

Author SHA1 Message Date
Kyle Spearrin
056bce3dd9 New Crowdin translations (#197)
* New translations AppResources.resx (Chinese Simplified)

* New translations AppResources.resx (Russian)

* New translations AppResources.resx (Romanian)

* New translations AppResources.resx (Portuguese, Brazilian)

* New translations AppResources.resx (Portuguese)

* New translations AppResources.resx (Polish)

* New translations AppResources.resx (Slovak)

* New translations copy.resx (Japanese)

* New translations AppResources.resx (Turkish)

* New translations AppResources.resx (Vietnamese)

* New translations AppResources.resx (Ukrainian)

* New translations AppResources.resx (Spanish)

* New translations AppResources.resx (Thai)

* New translations AppResources.resx (Swedish)

* New translations copy.resx (Japanese)

* New translations AppResources.resx (Japanese)

* New translations AppResources.resx (Dutch)

* New translations AppResources.resx (Danish)

* New translations AppResources.resx (Czech)

* New translations AppResources.resx (Croatian)

* New translations AppResources.resx (Chinese Traditional)

* New translations AppResources.resx (Finnish)

* New translations AppResources.resx (Hungarian)

* New translations AppResources.resx (Italian)

* New translations AppResources.resx (Indonesian)

* New translations AppResources.resx (French)

* New translations AppResources.resx (Hindi)

* New translations AppResources.resx (German)
2017-12-06 09:23:35 -05:00
Kyle Spearrin
5d6575e97b version bump 2017-12-06 09:17:49 -05:00
Kyle Spearrin
f092d4ffc3 handle timers more appropriately 2017-12-05 13:58:09 -05:00
Kyle Spearrin
5bae15831b update fingerprint to beta4 2017-12-05 10:04:53 -05:00
Kyle Spearrin
cf19bd88f0 summary desc update for accessibility service 2017-12-04 08:19:22 -05:00
Kyle Spearrin
38ac6a1082 desc updates 2017-12-04 08:15:21 -05:00
Kyle Spearrin
b88e2bd3ce update desc copy 2017-12-03 22:09:24 -05:00
Kyle Spearrin
fad24c4308 autofill summary/desc updates 2017-12-03 22:00:14 -05:00
Kyle Spearrin
018fd83dba update to beta4 2017-12-02 20:33:42 -05:00
Kyle Spearrin
aa95da167f escape apostrophe 2017-12-02 06:27:40 -05:00
Kyle Spearrin
24e6a0be68 new summary/description for autofill service 2017-12-01 22:10:54 -05:00
Kyle Spearrin
a2c962c2f6 adjust faceid check logic 2017-11-29 23:29:13 -05:00
Kyle Spearrin
aa61331181 user dialogs removed from DI on auth service 2017-11-29 16:55:55 -05:00
Kyle Spearrin
00f0a7589c app missing ios extension services 2017-11-29 16:39:43 -05:00
Kyle Spearrin
d39609351a noop device action service for ios 2017-11-29 16:28:58 -05:00
Kyle Spearrin
6985ccf076 fix missing smile image refs 2017-11-29 16:11:07 -05:00
Kyle Spearrin
b448cad4de faceid support on extension lock page 2017-11-29 16:05:50 -05:00
Kyle Spearrin
14540b4cc0 support for faceid labels 2017-11-29 15:47:43 -05:00
Kyle Spearrin
898b76a549 thicker plus sign 2017-11-29 15:18:01 -05:00
Kyle Spearrin
5cf6e382d8 New Crowdin translations (#191)
* New translations AppResources.resx (Chinese Simplified)

* New translations AppResources.resx (Russian)

* New translations AppResources.resx (Romanian)

* New translations AppResources.resx (Portuguese, Brazilian)

* New translations AppResources.resx (Portuguese)

* New translations copy.resx (Polish)

* New translations copy.resx (Polish)

* New translations AppResources.resx (Polish)

* New translations AppResources.resx (Slovak)

* New translations AppResources.resx (Turkish)

* New translations copy.resx (Vietnamese)

* New translations AppResources.resx (Vietnamese)

* New translations AppResources.resx (Ukrainian)

* New translations copy.resx (Turkish)

* New translations copy.resx (Turkish)

* New translations AppResources.resx (Spanish)

* New translations AppResources.resx (Thai)

* New translations AppResources.resx (Swedish)

* New translations AppResources.resx (Japanese)

* New translations AppResources.resx (Dutch)

* New translations AppResources.resx (Danish)

* New translations AppResources.resx (Czech)

* New translations AppResources.resx (Croatian)

* New translations AppResources.resx (Chinese Traditional)

* New translations AppResources.resx (Finnish)

* New translations AppResources.resx (Hungarian)

* New translations AppResources.resx (Italian)

* New translations AppResources.resx (Indonesian)

* New translations AppResources.resx (French)

* New translations AppResources.resx (Hindi)

* New translations AppResources.resx (German)

* New translations copy.resx (Vietnamese)
2017-11-29 15:00:16 -05:00
Kyle Spearrin
e2ba56a227 images for autofill tools pages 2017-11-29 14:49:28 -05:00
Kyle Spearrin
ec9960e28e update fingerprint for UWP 2017-11-29 13:48:26 -05:00
Kyle Spearrin
dc59283160 resource designer update 2017-11-29 12:11:57 -05:00
Kyle Spearrin
d255d44be5 update fingerprint library 2017-11-29 11:51:07 -05:00
Kyle Spearrin
b2f68a5a7e search vault capitalized 2017-11-29 11:26:21 -05:00
Kyle Spearrin
ec32679ab1 change options autofill label to include accessib. 2017-11-29 11:18:33 -05:00
Kyle Spearrin
8b2471c128 rename features to options 2017-11-29 09:20:45 -05:00
Kyle Spearrin
022eba2c05 fixes for UWP 2017-11-28 21:27:57 -05:00
Kyle Spearrin
029c6fcfe3 Fix UWP errors 2017-11-28 21:08:45 -05:00
Kyle Spearrin
faaa0b2488 null check on field ctor 2017-11-28 13:39:31 -05:00
Kyle Spearrin
daa2ca876b update title of accessibility service page 2017-11-28 08:32:17 -05:00
Kyle Spearrin
81700cfb44 Revert "update fingerprint library"
This reverts commit b670280688.
2017-11-28 07:55:32 -05:00
Kyle Spearrin
6e58db95ed consistent font size 2017-11-27 23:01:01 -05:00
Kyle Spearrin
9b54862450 origin padding since it doesnt seem to work 2017-11-27 22:58:16 -05:00
Kyle Spearrin
f79efadd82 minimum padding for ios header 2017-11-27 22:53:01 -05:00
Kyle Spearrin
615a7670bd focus search after content results are set 2017-11-27 22:49:27 -05:00
Kyle Spearrin
155b8b472f back to old search icon 2017-11-27 22:44:51 -05:00
Kyle Spearrin
b35e3454f0 search updates 2017-11-27 22:39:28 -05:00
Kyle Spearrin
51b4716d45 ios buttons for list ciphers page 2017-11-27 22:34:42 -05:00
Kyle Spearrin
b62803a03a group is case insensitive 2017-11-27 22:25:51 -05:00
Kyle Spearrin
616893955f 0-9 is now # 2017-11-27 22:16:06 -05:00
Kyle Spearrin
0f387a139b set color of listview table index 2017-11-27 22:14:13 -05:00
Kyle Spearrin
219c81aac5 header adjustments 2017-11-27 22:09:00 -05:00
Kyle Spearrin
083003d34f empty string header for iOS 2017-11-27 21:58:52 -05:00
Kyle Spearrin
699f76c29e revert endpoint change 2017-11-27 21:58:32 -05:00
Kyle Spearrin
b670280688 update fingerprint library 2017-11-27 20:53:43 -05:00
Kyle Spearrin
37ea84ffe9 rename autofill pages 2017-11-27 20:45:09 -05:00
Kyle Spearrin
40b861acbe autofill service tools page labels 2017-11-27 19:23:26 -05:00
Kyle Spearrin
783c4d104c add tools page for autofill service 2017-11-27 17:27:11 -05:00
Kyle Spearrin
9bbddd6aeb show loading indicator if syncing an no items 2017-11-27 15:42:36 -05:00
Kyle Spearrin
e753acbc3f clear cache on logout 2017-11-27 15:11:06 -05:00
Kyle Spearrin
92b7b1d603 handle conditions when no data 2017-11-27 15:05:12 -05:00
Kyle Spearrin
b07dc8443e default to "My Vault" option 2017-11-27 14:41:15 -05:00
Kyle Spearrin
3f99c513f3 rename pages 2017-11-27 14:26:07 -05:00
Kyle Spearrin
793241523d Rename pages 2017-11-27 14:24:47 -05:00
Kyle Spearrin
7cff22fb9e cleanup old list page 2017-11-27 14:23:42 -05:00
Kyle Spearrin
214f308027 Revert "disable fingerprint test"
This reverts commit c1ce971adb.
2017-11-27 13:56:11 -05:00
Kyle Spearrin
c1ce971adb disable fingerprint test 2017-11-27 13:25:19 -05:00
Kyle Spearrin
f5896be699 add uri and add buttons of search page 2017-11-27 13:22:42 -05:00
Kyle Spearrin
186f839569 exclude search fields from password filter 2017-11-27 11:54:31 -05:00
Kyle Spearrin
4879d906d9 filtered results for groupings and favorites 2017-11-27 09:47:49 -05:00
Kyle Spearrin
09412f0b78 no upper on autofill section headers 2017-11-25 23:33:50 -05:00
Kyle Spearrin
2f2d85576f consolidating section header models 2017-11-25 23:32:20 -05:00
Kyle Spearrin
362ddd0339 centralize some helpers 2017-11-25 23:04:14 -05:00
Kyle Spearrin
9499b7f562 search page with name groups 2017-11-25 15:43:43 -05:00
Kyle Spearrin
d8bb12b5f1 folder_o for "No Folder" 2017-11-25 14:06:44 -05:00
Kyle Spearrin
5d464f4477 increment index alter comparison, resolves #185 2017-11-25 13:49:54 -05:00
Kyle Spearrin
aaea0b2659 vault list grouping page 2017-11-24 23:15:25 -05:00
Kyle Spearrin
c9ceb09906 add collection syncing 2017-11-24 16:11:40 -05:00
Kyle Spearrin
3b44ede67e refactor message center use to services 2017-11-21 23:08:45 -05:00
Kyle Spearrin
b48e8eeb0e set notification channel to low priority 2017-11-21 17:52:23 -05:00
Kyle Spearrin
1fafc29ec3 remove unsubs 2017-11-21 14:31:46 -05:00
Kyle Spearrin
1a9d0576c8 cleanup subscriptions when autofilling 2017-11-21 13:28:02 -05:00
Kyle Spearrin
bc04211b79 autofill from vault with specified dataset 2017-11-21 11:29:00 -05:00
Kyle Spearrin
cfe34355bd helper for empty header value 2017-11-20 22:39:49 -05:00
Kyle Spearrin
e3e833d8c0 no savetype var 2017-11-20 22:39:33 -05:00
Kyle Spearrin
5606a0a968 fix test 2017-11-20 16:32:23 -05:00
Kyle Spearrin
f0358f1da8 run android script from web download 2017-11-20 16:25:02 -05:00
Kyle Spearrin
a3129e9e17 install android before_build 2017-11-20 16:20:24 -05:00
Kyle Spearrin
7435ede254 install android 26 2017-11-20 16:17:28 -05:00
Kyle Spearrin
84e79e92b4 add other items to autofill from app page 2017-11-20 16:07:33 -05:00
Kyle Spearrin
6268130998 add rdp info to build 2017-11-20 10:41:11 -05:00
Kyle Spearrin
7ad639599a added slash to folder route 2017-11-20 07:32:58 -05:00
Kyle Spearrin
caff67b77d added cards and other improvements to save 2017-11-18 23:04:21 -05:00
Kyle Spearrin
c45a77d538 add support for card filling 2017-11-18 15:09:09 -05:00
Kyle Spearrin
4b24fe1bf4 dont reset main page for autofill framework 2017-11-17 23:46:45 -05:00
Kyle Spearrin
73e5fb6314 FillableForLogin check last 2017-11-17 23:41:53 -05:00
Kyle Spearrin
84ea28adfa added hint detection to username/password fields 2017-11-17 23:38:09 -05:00
Kyle Spearrin
955fc97cb2 ignoreids 2017-11-17 23:26:51 -05:00
Kyle Spearrin
e4012e4f87 autofill cleanup 2017-11-17 23:00:57 -05:00
Kyle Spearrin
2c662c428c better detection for username/passwords 2017-11-17 22:47:08 -05:00
Kyle Spearrin
da199deed1 only show autofills if a fillable login form 2017-11-17 17:46:55 -05:00
Kyle Spearrin
abf75cffd9 parse saved item info for save 2017-11-17 17:15:42 -05:00
Kyle Spearrin
184f13b148 save info from service to add cipher page 2017-11-17 14:38:56 -05:00
Kyle Spearrin
d1c7309b29 search goes to vault apge, not main page 2017-11-17 13:03:43 -05:00
Kyle Spearrin
62db6552d2 no androidapp://android package 2017-11-17 10:18:18 -05:00
Kyle Spearrin
a019b9e1d3 dont set uri if null 2017-11-17 10:09:27 -05:00
Kyle Spearrin
cb22572f2b dont offer autofill in bitwarden app 2017-11-17 10:05:13 -05:00
Kyle Spearrin
b52134e9ee cancel on lock page back button 2017-11-17 10:03:41 -05:00
Kyle Spearrin
44ef82219b flags 2017-11-17 09:52:14 -05:00
Kyle Spearrin
8c89b0e587 switch to main activity when locked 2017-11-17 09:21:12 -05:00
Kyle Spearrin
322b251def auth activity for locked vaults when autofilling 2017-11-17 00:16:45 -05:00
Kyle Spearrin
0a6767209d layout updates 2017-11-16 22:34:19 -05:00
Kyle Spearrin
1694b5d6fd renaming things 2017-11-16 21:58:04 -05:00
Kyle Spearrin
0dd9ad43e8 clear cache 2017-11-16 17:18:25 -05:00
Kyle Spearrin
c1ae3f1fb2 cache ciphers 2017-11-16 16:51:43 -05:00
Kyle Spearrin
d84627aa2c better detection based on IdEntry sniffing 2017-11-16 16:09:57 -05:00
Kyle Spearrin
0e020924ff refactor autofill classes. basic login support. 2017-11-14 23:13:55 -05:00
Kyle Spearrin
4f5e238685 build out supporting classes from old refs 2017-11-14 16:46:40 -05:00
Kyle Spearrin
72ff680114 remove hacks 2017-11-14 16:38:05 -05:00
Kyle Spearrin
849ec6fa8f add old autofill implementation for reference. 2017-11-14 16:31:03 -05:00
Kyle Spearrin
36ee3aaec6 Revert "use vs 2017 preview"
This reverts commit 497d4f50dd.
2017-11-14 16:21:55 -05:00
Kyle Spearrin
497d4f50dd use vs 2017 preview 2017-11-14 16:03:44 -05:00
Kyle Spearrin
74a40b2274 stub out autofill framework service 2017-11-14 16:00:32 -05:00
Alistair Francis
75e85541a6 UWP/Assets: Update to use the Bitwarden logo (#169)
* UWP/Assets: Update to use the Bitwarden logo

Signed-off-by: Alistair Francis <alistair@alistair23.me>

* Package.appxmanifest: Show title on UWP tiles

Signed-off-by: Alistair Francis <alistair@alistair23.me>
2017-11-13 07:48:45 -05:00
Alistair Francis
1d8fbac796 TokenService.cs: Check if key exists before deleting it (#168)
To avoid errors in Task<ApiResult<TokenResponse>> when logging in on
UWP apps ensure that we check that they key exists before we delete the
2FA key token.

Signed-off-by: Alistair Francis <alistair@alistair23.me>
2017-11-13 07:47:57 -05:00
Kyle Spearrin
daf6d1936f remove old keystore storage service 2017-11-10 16:49:13 -05:00
Kyle Spearrin
1768e8cb62 test 2017-11-09 23:07:13 -05:00
Kyle Spearrin
d2d6bfc065 dont export PackageReplacedReceiver 2017-11-09 20:33:01 -05:00
209 changed files with 7771 additions and 3691 deletions

View File

@@ -1,4 +1,5 @@
skip_tags: true
init:
- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/bitwarden/mobile/master/install-android26.ps1'))
before_build:
- nuget restore
- IF DEFINED keystore_dec_secret nuget install secure-file -ExcludeVersion
@@ -15,4 +16,5 @@ artifacts:
branches:
except:
- l10n_master
skip_tags: true
image: Visual Studio 2017

View File

@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Android", "src\Android\Android.csproj", "{04B18ED2-B76D-4947-8474-191F8FD2B5E0}"
EndProject
@@ -33,6 +33,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UWP", "src\UWP\UWP.csproj",
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "UWP.Images", "src\UWP.Images\UWP.Images.shproj", "{0BE54BBB-7772-4289-BD51-1FDBB0CC2446}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F0E2E596-C3DB-474A-9C88-7824662894FA}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
appveyor.yml = appveyor.yml
crowdin.yml = crowdin.yml
README.md = README.md
SECURITY.md = SECURITY.md
EndProjectSection
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\UWP.Images\UWP.Images.projitems*{0be54bbb-7772-4289-bd51-1fdbb0cc2446}*SharedItemsImports = 13

30
install-android26.ps1 Normal file
View File

@@ -0,0 +1,30 @@
$AndroidToolPath = "${env:ProgramFiles(x86)}\Android\android-sdk\tools\android"
Function Get-AndroidSDKs() {
$output = & $AndroidToolPath list sdk --all
$sdks = $output |% {
if ($_ -match '(?<index>\d+)- (?<sdk>.+), revision (?<revision>[\d\.]+)') {
$sdk = New-Object PSObject
Add-Member -InputObject $sdk -MemberType NoteProperty -Name Index -Value $Matches.index
Add-Member -InputObject $sdk -MemberType NoteProperty -Name Name -Value $Matches.sdk
Add-Member -InputObject $sdk -MemberType NoteProperty -Name Revision -Value $Matches.revision
$sdk
}
}
$sdks
}
Function Install-AndroidSDK() {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[PSObject[]]$sdks
)
$sdkIndexes = $sdks |% { $_.Index }
$sdkIndexArgument = [string]::Join(',', $sdkIndexes)
Echo 'y' | & $AndroidToolPath update sdk -u -a -t $sdkIndexArgument
}
# install android 26
$sdks = Get-AndroidSDKs |? { $_.name -like 'sdk platform*API 26*' }
Install-AndroidSDK -sdks $sdks

View File

@@ -17,7 +17,7 @@
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v7.1</TargetFrameworkVersion>
<TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
<AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<AndroidStoreUncompressedFileExtensions />
@@ -149,14 +149,14 @@
<HintPath>..\..\packages\Plugin.CurrentActivity.1.0.1\lib\MonoAndroid10\Plugin.CurrentActivity.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Plugin.Fingerprint, Version=1.4.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.5\lib\MonoAndroid\Plugin.Fingerprint.dll</HintPath>
<Reference Include="Plugin.Fingerprint, Version=1.4.6.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.6-beta4\lib\MonoAndroid\Plugin.Fingerprint.dll</HintPath>
</Reference>
<Reference Include="Plugin.Fingerprint.Abstractions, Version=1.4.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.5\lib\MonoAndroid\Plugin.Fingerprint.Abstractions.dll</HintPath>
<Reference Include="Plugin.Fingerprint.Abstractions, Version=1.4.6.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.6-beta4\lib\MonoAndroid\Plugin.Fingerprint.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Plugin.Fingerprint.Android.Samsung, Version=1.4.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.5\lib\MonoAndroid\Plugin.Fingerprint.Android.Samsung.dll</HintPath>
<Reference Include="Plugin.Fingerprint.Android.Samsung, Version=1.4.6.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.6-beta4\lib\MonoAndroid\Plugin.Fingerprint.Android.Samsung.dll</HintPath>
</Reference>
<Reference Include="Plugin.Settings, Version=3.0.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Xam.Plugins.Settings.3.0.1\lib\MonoAndroid10\Plugin.Settings.dll</HintPath>
@@ -287,6 +287,12 @@
<ItemGroup>
<Compile Include="AutofillActivity.cs" />
<Compile Include="AutofillCredentials.cs" />
<Compile Include="Autofill\Field.cs" />
<Compile Include="Autofill\FieldCollection.cs" />
<Compile Include="Autofill\AutofillService.cs" />
<Compile Include="Autofill\AutofillHelpers.cs" />
<Compile Include="Autofill\FilledItem.cs" />
<Compile Include="Autofill\SavedItem.cs" />
<Compile Include="Controls\CustomLabelRenderer.cs" />
<Compile Include="Controls\CustomSearchBarRenderer.cs" />
<Compile Include="Controls\CustomButtonRenderer.cs" />
@@ -313,7 +319,6 @@
<Compile Include="Services\AppInfoService.cs" />
<Compile Include="Services\DeviceActionService.cs" />
<Compile Include="Services\BouncyCastleKeyDerivationService.cs" />
<Compile Include="Services\KeyStoreStorageService.cs" />
<Compile Include="MainActivity.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\LogService.cs" />
@@ -323,6 +328,7 @@
<Compile Include="Services\SqlService.cs" />
<Compile Include="SplashActivity.cs" />
<Compile Include="PackageReplacedReceiver.cs" />
<Compile Include="Autofill\Parser.cs" />
<Compile Include="Utilities.cs" />
</ItemGroup>
<ItemGroup>
@@ -342,6 +348,9 @@
<AndroidResource Include="Resources\layout\toolbar.axml">
<SubType>AndroidResource</SubType>
</AndroidResource>
<AndroidResource Include="Resources\layout\autofill_listitem.axml">
<SubType>AndroidResource</SubType>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<None Include="Properties\AndroidManifest.xml" />
@@ -1063,6 +1072,63 @@
<ItemGroup>
<AndroidResource Include="Resources\drawable\apple.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\xml\autofillservice.xml" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\cube.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\cube.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\cube.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\cube.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxxhdpi\cube.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\folder_o.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\folder_o.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\folder_o.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\folder_o.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxxhdpi\folder_o.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\autofill_enable.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\autofill_enable.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\autofill_enable.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\autofill_enable.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\autofill_use.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\autofill_use.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\autofill_use.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\autofill_use.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets" Condition="Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Service.Autofill;
using Android.Widget;
using System.Linq;
using Android.App;
using Bit.App.Abstractions;
using System.Threading.Tasks;
using Bit.App.Resources;
using Bit.App.Enums;
using Android.Views.Autofill;
namespace Bit.Android.Autofill
{
public static class AutofillHelpers
{
private static int _pendingIntentId = 0;
public static async Task<List<FilledItem>> GetFillItemsAsync(Parser parser, ICipherService service)
{
var items = new List<FilledItem>();
if(parser.FieldCollection.FillableForLogin)
{
var ciphers = await service.GetAllAsync(parser.Uri);
if(ciphers.Item1.Any() || ciphers.Item2.Any())
{
var allCiphers = ciphers.Item1.ToList();
allCiphers.AddRange(ciphers.Item2.ToList());
foreach(var cipher in allCiphers)
{
items.Add(new FilledItem(cipher));
}
}
}
else if(parser.FieldCollection.FillableForCard)
{
var ciphers = await service.GetAllAsync();
foreach(var cipher in ciphers.Where(c => c.Type == CipherType.Card))
{
items.Add(new FilledItem(cipher));
}
}
return items;
}
public static FillResponse BuildFillResponse(Context context, Parser parser, List<FilledItem> items, bool locked)
{
var responseBuilder = new FillResponse.Builder();
if(items != null && items.Count > 0)
{
foreach(var item in items)
{
var dataset = BuildDataset(context, parser.FieldCollection, item);
if(dataset != null)
{
responseBuilder.AddDataset(dataset);
}
}
}
responseBuilder.AddDataset(BuildVaultDataset(context, parser.FieldCollection, parser.Uri, locked));
AddSaveInfo(responseBuilder, parser.FieldCollection);
responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
return responseBuilder.Build();
}
public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem)
{
var datasetBuilder = new Dataset.Builder(
BuildListView(context.PackageName, filledItem.Name, filledItem.Subtitle, filledItem.Icon));
if(filledItem.ApplyToFields(fields, datasetBuilder))
{
return datasetBuilder.Build();
}
return null;
}
public static Dataset BuildVaultDataset(Context context, FieldCollection fields, string uri, bool locked)
{
var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true);
if(fields.FillableForLogin)
{
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login);
}
else if(fields.FillableForCard)
{
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card);
}
else if(fields.FillableForIdentity)
{
intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity);
}
else
{
return null;
}
intent.PutExtra("autofillFrameworkUri", uri);
var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
PendingIntentFlags.CancelCurrent);
var view = BuildListView(context.PackageName, AppResources.AutofillWithBitwarden,
locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault, Resource.Drawable.icon);
var datasetBuilder = new Dataset.Builder(view);
datasetBuilder.SetAuthentication(pendingIntent.IntentSender);
// Dataset must have a value set. We will reset this in the main activity when the real item is chosen.
foreach(var autofillId in fields.AutofillIds)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
}
return datasetBuilder.Build();
}
public static RemoteViews BuildListView(string packageName, string text, string subtext, int iconId)
{
var view = new RemoteViews(packageName, Resource.Layout.autofill_listitem);
view.SetTextViewText(Resource.Id.text, text);
view.SetTextViewText(Resource.Id.text2, subtext);
view.SetImageViewResource(Resource.Id.icon, iconId);
return view;
}
public static void AddSaveInfo(FillResponse.Builder responseBuilder, FieldCollection fields)
{
var requiredIds = fields.GetRequiredSaveFields();
if(fields.SaveType == SaveDataType.Generic || requiredIds.Length == 0)
{
return;
}
var saveBuilder = new SaveInfo.Builder(fields.SaveType, requiredIds);
var optionalIds = fields.GetOptionalSaveIds();
if(optionalIds.Length > 0)
{
saveBuilder.SetOptionalIds(optionalIds);
}
responseBuilder.SetSaveInfo(saveBuilder.Build());
}
}
}

View File

@@ -0,0 +1,110 @@
using Android;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Service.Autofill;
using Android.Widget;
using Bit.App;
using Bit.App.Abstractions;
using Bit.App.Enums;
using System.Collections.Generic;
using System.Linq;
using XLabs.Ioc;
namespace Bit.Android.Autofill
{
[Service(Permission = Manifest.Permission.BindAutofillService, Label = "bitwarden")]
[IntentFilter(new string[] { "android.service.autofill.AutofillService" })]
[MetaData("android.autofill", Resource = "@xml/autofillservice")]
[Register("com.x8bit.bitwarden.Autofill.AutofillService")]
public class AutofillService : global::Android.Service.Autofill.AutofillService
{
private ICipherService _cipherService;
private ILockService _lockService;
public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)
{
var structure = request.FillContexts?.LastOrDefault()?.Structure;
if(structure == null)
{
return;
}
var parser = new Parser(structure);
parser.Parse();
if(string.IsNullOrWhiteSpace(parser.Uri) || parser.Uri == "androidapp://com.x8bit.bitwarden" ||
parser.Uri == "androidapp://android" || !parser.FieldCollection.Fillable)
{
return;
}
if(_lockService == null)
{
_lockService = Resolver.Resolve<ILockService>();
}
List<FilledItem> items = null;
var locked = (await _lockService.GetLockTypeAsync(false)) != LockType.None;
if(!locked)
{
if(_cipherService == null)
{
_cipherService = Resolver.Resolve<ICipherService>();
}
items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
}
// build response
var response = AutofillHelpers.BuildFillResponse(this, parser, items, locked);
callback.OnSuccess(response);
}
public override void OnSaveRequest(SaveRequest request, SaveCallback callback)
{
var structure = request.FillContexts?.LastOrDefault()?.Structure;
if(structure == null)
{
return;
}
var parser = new Parser(structure);
parser.Parse();
var savedItem = parser.FieldCollection.GetSavedItem();
if(savedItem == null)
{
Toast.MakeText(this, "Unable to save this form.", ToastLength.Short).Show();
return;
}
var intent = new Intent(this, typeof(MainActivity));
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop);
intent.PutExtra("autofillFramework", true);
intent.PutExtra("autofillFrameworkSave", true);
intent.PutExtra("autofillFrameworkType", (int)savedItem.Type);
switch(savedItem.Type)
{
case CipherType.Login:
intent.PutExtra("autofillFrameworkName", parser.Uri.Replace(Constants.AndroidAppProtocol, string.Empty));
intent.PutExtra("autofillFrameworkUri", parser.Uri);
intent.PutExtra("autofillFrameworkUsername", savedItem.Login.Username);
intent.PutExtra("autofillFrameworkPassword", savedItem.Login.Password);
break;
case CipherType.Card:
intent.PutExtra("autofillFrameworkCardName", savedItem.Card.Name);
intent.PutExtra("autofillFrameworkCardNumber", savedItem.Card.Number);
intent.PutExtra("autofillFrameworkCardExpMonth", savedItem.Card.ExpMonth);
intent.PutExtra("autofillFrameworkCardExpYear", savedItem.Card.ExpYear);
intent.PutExtra("autofillFrameworkCardCode", savedItem.Card.Code);
break;
default:
Toast.MakeText(this, "Unable to save this type of form.", ToastLength.Short).Show();
return;
}
StartActivity(intent);
}
}
}

View File

@@ -0,0 +1,192 @@
using System.Collections.Generic;
using System.Linq;
using Android.Service.Autofill;
using Android.Views;
using Android.Views.Autofill;
using static Android.App.Assist.AssistStructure;
using Android.Text;
namespace Bit.Android.Autofill
{
public class Field
{
private List<string> _hints;
public Field(ViewNode node)
{
Id = node.Id;
IdEntry = node.IdEntry;
AutofillId = node.AutofillId;
AutofillType = node.AutofillType;
InputType = node.InputType;
Focused = node.IsFocused;
Selected = node.IsSelected;
Clickable = node.IsClickable;
Visible = node.Visibility == ViewStates.Visible;
Hints = FilterForSupportedHints(node.GetAutofillHints());
AutofillOptions = node.GetAutofillOptions()?.ToList();
Node = node;
if(node.AutofillValue != null)
{
if(node.AutofillValue.IsList)
{
var autofillOptions = node.GetAutofillOptions();
if(autofillOptions != null && autofillOptions.Length > 0)
{
ListValue = node.AutofillValue.ListValue;
TextValue = autofillOptions[node.AutofillValue.ListValue];
}
}
else if(node.AutofillValue.IsDate)
{
DateValue = node.AutofillValue.DateValue;
}
else if(node.AutofillValue.IsText)
{
TextValue = node.AutofillValue.TextValue;
}
else if(node.AutofillValue.IsToggle)
{
ToggleValue = node.AutofillValue.ToggleValue;
}
}
}
public SaveDataType SaveType { get; set; } = SaveDataType.Generic;
public List<string> Hints
{
get => _hints;
set
{
_hints = value;
UpdateSaveTypeFromHints();
}
}
public int Id { get; private set; }
public string IdEntry { get; set; }
public AutofillId AutofillId { get; private set; }
public AutofillType AutofillType { get; private set; }
public InputTypes InputType { get; private set; }
public bool Focused { get; private set; }
public bool Selected { get; private set; }
public bool Clickable { get; private set; }
public bool Visible { get; private set; }
public List<string> AutofillOptions { get; set; }
public string TextValue { get; set; }
public long? DateValue { get; set; }
public int? ListValue { get; set; }
public bool? ToggleValue { get; set; }
public ViewNode Node { get; private set; }
private void UpdateSaveTypeFromHints()
{
SaveType = SaveDataType.Generic;
if(_hints == null)
{
return;
}
foreach(var hint in _hints)
{
switch(hint)
{
case View.AutofillHintCreditCardExpirationDate:
case View.AutofillHintCreditCardExpirationDay:
case View.AutofillHintCreditCardExpirationMonth:
case View.AutofillHintCreditCardExpirationYear:
case View.AutofillHintCreditCardNumber:
case View.AutofillHintCreditCardSecurityCode:
SaveType |= SaveDataType.CreditCard;
break;
case View.AutofillHintEmailAddress:
SaveType |= SaveDataType.EmailAddress;
break;
case View.AutofillHintPhone:
case View.AutofillHintName:
SaveType |= SaveDataType.Generic;
break;
case View.AutofillHintPassword:
SaveType |= SaveDataType.Password;
SaveType &= ~SaveDataType.EmailAddress;
SaveType &= ~SaveDataType.Username;
break;
case View.AutofillHintPostalAddress:
case View.AutofillHintPostalCode:
SaveType |= SaveDataType.Address;
break;
case View.AutofillHintUsername:
SaveType |= SaveDataType.Username;
break;
}
}
}
public bool ValueIsNull()
{
return TextValue == null && DateValue == null && ToggleValue == null;
}
public override bool Equals(object obj)
{
if(this == obj)
{
return true;
}
if(obj == null || GetType() != obj.GetType())
{
return false;
}
var field = obj as Field;
if(TextValue != null ? !TextValue.Equals(field.TextValue) : field.TextValue != null)
{
return false;
}
if(DateValue != null ? !DateValue.Equals(field.DateValue) : field.DateValue != null)
{
return false;
}
return ToggleValue != null ? ToggleValue.Equals(field.ToggleValue) : field.ToggleValue == null;
}
public override int GetHashCode()
{
var result = TextValue != null ? TextValue.GetHashCode() : 0;
result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0);
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
return result;
}
private static List<string> FilterForSupportedHints(string[] hints)
{
return hints?.Where(h => IsValidHint(h)).ToList() ?? new List<string>();
}
private static bool IsValidHint(string hint)
{
switch(hint)
{
case View.AutofillHintCreditCardExpirationDate:
case View.AutofillHintCreditCardExpirationDay:
case View.AutofillHintCreditCardExpirationMonth:
case View.AutofillHintCreditCardExpirationYear:
case View.AutofillHintCreditCardNumber:
case View.AutofillHintCreditCardSecurityCode:
case View.AutofillHintEmailAddress:
case View.AutofillHintPhone:
case View.AutofillHintName:
case View.AutofillHintPassword:
case View.AutofillHintPostalAddress:
case View.AutofillHintPostalCode:
case View.AutofillHintUsername:
return true;
default:
return false;
}
}
}
}

View File

@@ -0,0 +1,310 @@
using System.Collections.Generic;
using Android.Service.Autofill;
using Android.Views.Autofill;
using System.Linq;
using Android.Text;
using Android.Views;
namespace Bit.Android.Autofill
{
public class FieldCollection
{
private List<Field> _passwordFields = null;
private List<Field> _usernameFields = null;
public HashSet<int> Ids { get; private set; } = new HashSet<int>();
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
public SaveDataType SaveType
{
get
{
if(FillableForLogin)
{
return SaveDataType.Password;
}
else if(FillableForCard)
{
return SaveDataType.CreditCard;
}
return SaveDataType.Generic;
}
}
public HashSet<string> Hints { get; private set; } = new HashSet<string>();
public HashSet<string> FocusedHints { get; private set; } = new HashSet<string>();
public List<Field> Fields { get; private set; } = new List<Field>();
public IDictionary<int, Field> IdToFieldMap { get; private set; } =
new Dictionary<int, Field>();
public IDictionary<string, List<Field>> HintToFieldsMap { get; private set; } =
new Dictionary<string, List<Field>>();
public List<AutofillId> IgnoreAutofillIds { get; private set; } = new List<AutofillId>();
public List<Field> PasswordFields
{
get
{
if(_passwordFields != null)
{
return _passwordFields;
}
if(Hints.Any())
{
_passwordFields = new List<Field>();
if(HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
{
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
}
}
else
{
_passwordFields = Fields
.Where(f =>
!f.IdEntry.ToLowerInvariant().Contains("search") &&
(!f.Node.Hint?.ToLowerInvariant().Contains("search") ?? true) &&
(
f.InputType.HasFlag(InputTypes.TextVariationPassword) ||
f.InputType.HasFlag(InputTypes.TextVariationVisiblePassword) ||
f.InputType.HasFlag(InputTypes.TextVariationWebPassword)
)
).ToList();
if(!_passwordFields.Any())
{
_passwordFields = Fields.Where(f => f.IdEntry?.ToLower().Contains("password") ?? false).ToList();
}
}
return _passwordFields;
}
}
public List<Field> UsernameFields
{
get
{
if(_usernameFields != null)
{
return _usernameFields;
}
_usernameFields = new List<Field>();
if(Hints.Any())
{
if(HintToFieldsMap.ContainsKey(View.AutofillHintEmailAddress))
{
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintEmailAddress]);
}
if(HintToFieldsMap.ContainsKey(View.AutofillHintUsername))
{
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
}
}
else
{
foreach(var passwordField in PasswordFields)
{
var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault();
if(usernameField != null)
{
_usernameFields.Add(usernameField);
}
}
}
return _usernameFields;
}
}
public bool FillableForLogin => FocusedHintsContain(
new string[] { View.AutofillHintUsername, View.AutofillHintEmailAddress, View.AutofillHintPassword }) ||
UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused);
public bool FillableForCard => FocusedHintsContain(
new string[] { View.AutofillHintCreditCardNumber, View.AutofillHintCreditCardExpirationMonth,
View.AutofillHintCreditCardExpirationYear, View.AutofillHintCreditCardSecurityCode});
public bool FillableForIdentity => FocusedHintsContain(
new string[] { View.AutofillHintName, View.AutofillHintPhone, View.AutofillHintPostalAddress,
View.AutofillHintPostalCode });
public bool Fillable => FillableForLogin || FillableForCard || FillableForIdentity;
public void Add(Field field)
{
if(Ids.Contains(field.Id))
{
return;
}
_passwordFields = _usernameFields = null;
Ids.Add(field.Id);
Fields.Add(field);
AutofillIds.Add(field.AutofillId);
IdToFieldMap.Add(field.Id, field);
if(field.Hints != null)
{
foreach(var hint in field.Hints)
{
Hints.Add(hint);
if(field.Focused)
{
FocusedHints.Add(hint);
}
if(!HintToFieldsMap.ContainsKey(hint))
{
HintToFieldsMap.Add(hint, new List<Field>());
}
HintToFieldsMap[hint].Add(field);
}
}
}
public SavedItem GetSavedItem()
{
if(SaveType == SaveDataType.Password)
{
var passwordField = PasswordFields.FirstOrDefault(f => !string.IsNullOrWhiteSpace(f.TextValue));
if(passwordField == null)
{
return null;
}
var savedItem = new SavedItem
{
Type = App.Enums.CipherType.Login,
Login = new SavedItem.LoginItem
{
Password = GetFieldValue(passwordField)
}
};
var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault();
savedItem.Login.Username = GetFieldValue(usernameField);
return savedItem;
}
else if(SaveType == SaveDataType.CreditCard)
{
var savedItem = new SavedItem
{
Type = App.Enums.CipherType.Card,
Card = new SavedItem.CardItem
{
Number = GetFieldValue(View.AutofillHintCreditCardNumber),
Name = GetFieldValue(View.AutofillHintName),
ExpMonth = GetFieldValue(View.AutofillHintCreditCardExpirationMonth, true),
ExpYear = GetFieldValue(View.AutofillHintCreditCardExpirationYear),
Code = GetFieldValue(View.AutofillHintCreditCardSecurityCode)
}
};
return savedItem;
}
return null;
}
public AutofillId[] GetOptionalSaveIds()
{
if(SaveType == SaveDataType.Password)
{
return UsernameFields.Select(f => f.AutofillId).ToArray();
}
else if(SaveType == SaveDataType.CreditCard)
{
var fieldList = new List<Field>();
if(HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardSecurityCode))
{
fieldList.AddRange(HintToFieldsMap[View.AutofillHintCreditCardSecurityCode]);
}
if(HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationYear))
{
fieldList.AddRange(HintToFieldsMap[View.AutofillHintCreditCardExpirationYear]);
}
if(HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationMonth))
{
fieldList.AddRange(HintToFieldsMap[View.AutofillHintCreditCardExpirationMonth]);
}
if(HintToFieldsMap.ContainsKey(View.AutofillHintName))
{
fieldList.AddRange(HintToFieldsMap[View.AutofillHintName]);
}
return fieldList.Select(f => f.AutofillId).ToArray();
}
return new AutofillId[0];
}
public AutofillId[] GetRequiredSaveFields()
{
if(SaveType == SaveDataType.Password)
{
return PasswordFields.Select(f => f.AutofillId).ToArray();
}
else if(SaveType == SaveDataType.CreditCard && HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardNumber))
{
return HintToFieldsMap[View.AutofillHintCreditCardNumber].Select(f => f.AutofillId).ToArray();
}
return new AutofillId[0];
}
private bool FocusedHintsContain(IEnumerable<string> hints)
{
return hints.Any(h => FocusedHints.Contains(h));
}
private string GetFieldValue(string hint, bool monthValue = false)
{
if(HintToFieldsMap.ContainsKey(hint))
{
foreach(var field in HintToFieldsMap[hint])
{
var val = GetFieldValue(field, monthValue);
if(!string.IsNullOrWhiteSpace(val))
{
return val;
}
}
}
return null;
}
private string GetFieldValue(Field field, bool monthValue = false)
{
if(field == null)
{
return null;
}
if(!string.IsNullOrWhiteSpace(field.TextValue))
{
if(field.AutofillType == AutofillType.List && field.ListValue.HasValue && monthValue)
{
if(field.AutofillOptions.Count == 13)
{
return field.ListValue.ToString();
}
else if(field.AutofillOptions.Count == 12)
{
return (field.ListValue + 1).ToString();
}
}
return field.TextValue;
}
else if(field.DateValue.HasValue)
{
return field.DateValue.Value.ToString();
}
else if(field.ToggleValue.HasValue)
{
return field.ToggleValue.Value.ToString();
}
return null;
}
}
}

View File

@@ -0,0 +1,273 @@
using System;
using Android.Service.Autofill;
using Android.Views.Autofill;
using System.Linq;
using Bit.App.Models;
using Bit.App.Enums;
using Android.Views;
namespace Bit.Android.Autofill
{
public class FilledItem
{
private Lazy<string> _password;
private Lazy<string> _cardName;
private string _cardNumber;
private Lazy<string> _cardExpMonth;
private Lazy<string> _cardExpYear;
private Lazy<string> _cardCode;
private Lazy<string> _idPhone;
private Lazy<string> _idEmail;
private Lazy<string> _idUsername;
private Lazy<string> _idAddress;
private Lazy<string> _idPostalCode;
public FilledItem(Cipher cipher)
{
Name = cipher.Name?.Decrypt(cipher.OrganizationId) ?? "--";
Type = cipher.Type;
switch(Type)
{
case CipherType.Login:
Subtitle = cipher.Login.Username?.Decrypt(cipher.OrganizationId) ?? string.Empty;
Icon = Resource.Drawable.login;
_password = new Lazy<string>(() => cipher.Login.Password?.Decrypt(cipher.OrganizationId));
break;
case CipherType.Card:
Subtitle = cipher.Card.Brand?.Decrypt(cipher.OrganizationId);
_cardNumber = cipher.Card.Number?.Decrypt(cipher.OrganizationId);
if(!string.IsNullOrWhiteSpace(_cardNumber) && _cardNumber.Length >= 4)
{
if(!string.IsNullOrWhiteSpace(_cardNumber))
{
Subtitle += ", ";
}
Subtitle += ("*" + _cardNumber.Substring(_cardNumber.Length - 4));
}
Icon = Resource.Drawable.card;
_cardName = new Lazy<string>(() => cipher.Card.CardholderName?.Decrypt(cipher.OrganizationId));
_cardCode = new Lazy<string>(() => cipher.Card.Code?.Decrypt(cipher.OrganizationId));
_cardExpMonth = new Lazy<string>(() => cipher.Card.ExpMonth?.Decrypt(cipher.OrganizationId));
_cardExpYear = new Lazy<string>(() => cipher.Card.ExpYear?.Decrypt(cipher.OrganizationId));
break;
case CipherType.Identity:
var firstName = cipher.Identity?.FirstName?.Decrypt(cipher.OrganizationId) ?? " ";
var lastName = cipher.Identity?.LastName?.Decrypt(cipher.OrganizationId) ?? " ";
Subtitle = " ";
if(!string.IsNullOrWhiteSpace(firstName))
{
Subtitle = firstName;
}
if(!string.IsNullOrWhiteSpace(lastName))
{
if(!string.IsNullOrWhiteSpace(Subtitle))
{
Subtitle += " ";
}
Subtitle += lastName;
}
Icon = Resource.Drawable.id;
_idPhone = new Lazy<string>(() => cipher.Identity.Phone?.Decrypt(cipher.OrganizationId));
_idEmail = new Lazy<string>(() => cipher.Identity.Email?.Decrypt(cipher.OrganizationId));
_idUsername = new Lazy<string>(() => cipher.Identity.Username?.Decrypt(cipher.OrganizationId));
_idAddress = new Lazy<string>(() =>
{
var address = cipher.Identity.Address1?.Decrypt(cipher.OrganizationId);
var address2 = cipher.Identity.Address2?.Decrypt(cipher.OrganizationId);
if(!string.IsNullOrWhiteSpace(address2))
{
if(!string.IsNullOrWhiteSpace(address))
{
address += ", ";
}
address += address2;
}
var address3 = cipher.Identity.Address3?.Decrypt(cipher.OrganizationId);
if(!string.IsNullOrWhiteSpace(address3))
{
if(!string.IsNullOrWhiteSpace(address))
{
address += ", ";
}
address += address3;
}
return address;
});
_idPostalCode = new Lazy<string>(() => cipher.Identity.PostalCode?.Decrypt(cipher.OrganizationId));
break;
default:
break;
}
}
public string Name { get; set; }
public string Subtitle { get; set; } = string.Empty;
public int Icon { get; set; } = Resource.Drawable.login;
public CipherType Type { get; set; }
public bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder)
{
if(!fieldCollection?.Fields.Any() ?? true)
{
return false;
}
var setValues = false;
if(Type == CipherType.Login)
{
if(fieldCollection.PasswordFields.Any() && !string.IsNullOrWhiteSpace(_password.Value))
{
foreach(var f in fieldCollection.PasswordFields)
{
var val = ApplyValue(f, _password.Value);
if(val != null)
{
setValues = true;
datasetBuilder.SetValue(f.AutofillId, val);
}
}
}
if(fieldCollection.UsernameFields.Any() && !string.IsNullOrWhiteSpace(Subtitle))
{
foreach(var f in fieldCollection.UsernameFields)
{
var val = ApplyValue(f, Subtitle);
if(val != null)
{
setValues = true;
datasetBuilder.SetValue(f.AutofillId, val);
}
}
}
}
else if(Type == CipherType.Card)
{
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardNumber,
new Lazy<string>(() => _cardNumber)))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardSecurityCode, _cardCode))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardExpirationMonth, _cardExpMonth, true))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintCreditCardExpirationYear, _cardExpYear))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintName, _cardName))
{
setValues = true;
}
}
else if(Type == CipherType.Identity)
{
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintPhone, _idPhone))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintEmailAddress, _idEmail))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintUsername, _idUsername))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintPostalAddress, _idAddress))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintPostalCode, _idPostalCode))
{
setValues = true;
}
if(ApplyValue(datasetBuilder, fieldCollection, View.AutofillHintName, new Lazy<string>(() => Subtitle)))
{
setValues = true;
}
}
return setValues;
}
private static bool ApplyValue(Dataset.Builder builder, FieldCollection fieldCollection,
string hint, Lazy<string> value, bool monthValue = false)
{
bool setValues = false;
if(fieldCollection.HintToFieldsMap.ContainsKey(hint) && !string.IsNullOrWhiteSpace(value.Value))
{
foreach(var f in fieldCollection.HintToFieldsMap[hint])
{
var val = ApplyValue(f, value.Value, monthValue);
if(val != null)
{
setValues = true;
builder.SetValue(f.AutofillId, val);
}
}
}
return setValues;
}
private static AutofillValue ApplyValue(Field field, string value, bool monthValue = false)
{
switch(field.AutofillType)
{
case AutofillType.Date:
if(long.TryParse(value, out long dateValue))
{
return AutofillValue.ForDate(dateValue);
}
break;
case AutofillType.List:
if(field.AutofillOptions != null)
{
if(monthValue && int.TryParse(value, out int monthIndex))
{
if(field.AutofillOptions.Count == 13)
{
return AutofillValue.ForList(monthIndex);
}
else if(field.AutofillOptions.Count >= monthIndex)
{
return AutofillValue.ForList(monthIndex - 1);
}
}
for(var i = 0; i < field.AutofillOptions.Count; i++)
{
if(field.AutofillOptions[i].Equals(value))
{
return AutofillValue.ForList(i);
}
}
}
break;
case AutofillType.Text:
return AutofillValue.ForText(value);
case AutofillType.Toggle:
if(bool.TryParse(value, out bool toggleValue))
{
return AutofillValue.ForToggle(toggleValue);
}
break;
default:
break;
}
return null;
}
}
}

View File

@@ -0,0 +1,86 @@
using static Android.App.Assist.AssistStructure;
using Android.App.Assist;
using Bit.App;
namespace Bit.Android.Autofill
{
public class Parser
{
private readonly AssistStructure _structure;
private string _uri;
private string _packageName;
public Parser(AssistStructure structure)
{
_structure = structure;
}
public FieldCollection FieldCollection { get; private set; } = new FieldCollection();
public string Uri
{
get
{
if(!string.IsNullOrWhiteSpace(_uri))
{
return _uri;
}
if(string.IsNullOrWhiteSpace(PackageName))
{
_uri = null;
}
else
{
_uri = string.Concat(Constants.AndroidAppProtocol, PackageName);
}
return _uri;
}
}
public string PackageName
{
get => _packageName;
set
{
if(string.IsNullOrWhiteSpace(value))
{
_packageName = _uri = null;
}
_packageName = value;
}
}
public void Parse()
{
for(var i = 0; i < _structure.WindowNodeCount; i++)
{
var node = _structure.GetWindowNodeAt(i);
ParseNode(node.RootViewNode);
}
}
private void ParseNode(ViewNode node)
{
var hints = node.GetAutofillHints();
var isEditText = node.ClassName == "android.widget.EditText";
if(isEditText || (hints?.Length ?? 0) > 0)
{
if(PackageName == null)
{
PackageName = node.IdPackage;
}
FieldCollection.Add(new Field(node));
}
else
{
FieldCollection.IgnoreAutofillIds.Add(node.AutofillId);
}
for(var i = 0; i < node.ChildCount; i++)
{
ParseNode(node.GetChildAt(i));
}
}
}
}

View File

@@ -0,0 +1,26 @@
using Bit.App.Enums;
namespace Bit.Android.Autofill
{
public class SavedItem
{
public CipherType Type { get; set; }
public LoginItem Login { get; set; }
public CardItem Card { get; set; }
public class LoginItem
{
public string Username { get; set; }
public string Password { get; set; }
}
public class CardItem
{
public string Name { get; set; }
public string Number { get; set; }
public string ExpMonth { get; set; }
public string ExpYear { get; set; }
public string Code { get; set; }
}
}
}

View File

@@ -8,6 +8,7 @@ using Android.OS;
using Android.Views.Accessibility;
using Bit.App.Abstractions;
using XLabs.Ioc;
using Bit.App.Resources;
namespace Bit.Android
{
@@ -16,6 +17,8 @@ namespace Bit.Android
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
public class AutofillService : AccessibilityService
{
private NotificationChannel _notificationChannel;
private const int AutoFillNotificationId = 34573;
private const string SystemUiPackage = "com.android.systemui";
private const string BitwardenPackage = "com.x8bit.bitwarden";
@@ -344,12 +347,12 @@ namespace Bit.Android
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.UpdateCurrent);
var notificationContent = Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch ?
App.Resources.AppResources.BitwardenAutofillServiceNotificationContent :
App.Resources.AppResources.BitwardenAutofillServiceNotificationContentOld;
AppResources.BitwardenAutofillServiceNotificationContent :
AppResources.BitwardenAutofillServiceNotificationContentOld;
var builder = new Notification.Builder(this);
builder.SetSmallIcon(Resource.Drawable.notification_sm)
.SetContentTitle(App.Resources.AppResources.BitwardenAutofillService)
.SetContentTitle(AppResources.BitwardenAutofillService)
.SetContentText(notificationContent)
.SetTicker(notificationContent)
.SetWhen(now)
@@ -362,6 +365,17 @@ namespace Bit.Android
Resource.Color.primary));
}
if(Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
if(_notificationChannel == null)
{
_notificationChannel = new NotificationChannel("bitwarden_autofill_service",
AppResources.AutofillService, NotificationImportance.Low);
notificationManager.CreateNotificationChannel(_notificationChannel);
}
builder.SetChannelId(_notificationChannel.Id);
}
if(/*Build.VERSION.SdkInt <= BuildVersionCodes.N && */_appSettings.AutofillPersistNotification)
{
builder.SetPriority(-2);

View File

@@ -13,12 +13,12 @@ using System.Reflection;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
using System.Threading.Tasks;
using Bit.App.Models.Page;
using Bit.App;
using Android.Nfc;
using Android.Views.InputMethods;
using System.IO;
using System.Linq;
using Bit.App.Models;
using Bit.App.Enums;
namespace Bit.Android
{
@@ -29,14 +29,13 @@ namespace Bit.Android
public class MainActivity : FormsAppCompatActivity
{
private const string HockeyAppId = "d3834185b4a643479047b86c65293d42";
private DateTime? _lastAction;
private Java.Util.Regex.Pattern _otpPattern = Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$");
private IDeviceActionService _deviceActionService;
private ISettings _settings;
private AppOptions _appOptions;
protected override void OnCreate(Bundle bundle)
{
var uri = Intent.GetStringExtra("uri");
if(!Resolver.IsSet)
{
MainApplication.SetIoc(Application);
@@ -73,9 +72,9 @@ namespace Bit.Android
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_settings = Resolver.Resolve<ISettings>();
_appOptions = GetOptions();
LoadApplication(new App.App(
uri,
Intent.GetBooleanExtra("myVaultTile", false),
_appOptions,
Resolver.Resolve<IAuthService>(),
Resolver.Resolve<IConnectivity>(),
Resolver.Resolve<IUserDialogs>(),
@@ -89,77 +88,14 @@ namespace Bit.Android
Resolver.Resolve<IAppSettingsService>(),
_deviceActionService));
MessagingCenter.Subscribe<Xamarin.Forms.Application>(
Xamarin.Forms.Application.Current, "DismissKeyboard", (sender) =>
if(_appOptions?.Uri == null)
{
DismissKeyboard();
});
MessagingCenter.Subscribe<Xamarin.Forms.Application, bool>(Xamarin.Forms.Application.Current,
"ListenYubiKeyOTP", (sender, listen) => ListenYubiKey(listen));
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "RateApp", (sender) =>
{
RateApp();
});
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "Accessibility", (sender) =>
{
OpenAccessibilitySettings();
});
MessagingCenter.Subscribe<Xamarin.Forms.Application, VaultListPageModel.Cipher>(
Xamarin.Forms.Application.Current, "Autofill", (sender, args) =>
{
ReturnCredentials(args);
});
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current, "BackgroundApp", (sender) =>
{
MoveTaskToBack(true);
});
MessagingCenter.Subscribe<Xamarin.Forms.Application, string>(
Xamarin.Forms.Application.Current, "LaunchApp", (sender, args) =>
{
LaunchApp(args);
});
MessagingCenter.Subscribe<Xamarin.Forms.Application, bool>(
Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender, listen) =>
{
ListenYubiKey(listen);
});
}
private void ReturnCredentials(VaultListPageModel.Cipher cipher)
{
Intent data = new Intent();
if(cipher == null)
{
data.PutExtra("canceled", "true");
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current,
"FinishMainActivity", (sender) => Finish());
}
else
{
var isPremium = Resolver.Resolve<ITokenService>()?.TokenPremium ?? false;
var autoCopyEnabled = !_settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false);
if(isPremium && autoCopyEnabled && _deviceActionService != null && cipher.LoginTotp?.Value != null)
{
_deviceActionService.CopyToClipboard(App.Utilities.Crypto.Totp(cipher.LoginTotp.Value));
}
data.PutExtra("uri", cipher.LoginUri);
data.PutExtra("username", cipher.LoginUsername);
data.PutExtra("password", cipher.LoginPassword?.Value ?? null);
}
if(Parent == null)
{
SetResult(Result.Ok, data);
}
else
{
Parent.SetResult(Result.Ok, data);
}
Finish();
}
protected override void OnPause()
@@ -265,65 +201,6 @@ namespace Bit.Android
}
}
public void RateApp()
{
try
{
var rateIntent = RateIntentForUrl("market://details");
StartActivity(rateIntent);
}
catch(ActivityNotFoundException)
{
var rateIntent = RateIntentForUrl("https://play.google.com/store/apps/details");
StartActivity(rateIntent);
}
}
private Intent RateIntentForUrl(string url)
{
var intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse($"{url}?id={PackageName}"));
var flags = ActivityFlags.NoHistory | ActivityFlags.MultipleTask;
if((int)Build.VERSION.SdkInt >= 21)
{
flags |= ActivityFlags.NewDocument;
}
else
{
// noinspection deprecation
flags |= ActivityFlags.ClearWhenTaskReset;
}
intent.AddFlags(flags);
return intent;
}
private void OpenAccessibilitySettings()
{
var intent = new Intent(global::Android.Provider.Settings.ActionAccessibilitySettings);
StartActivity(intent);
}
private void LaunchApp(string packageName)
{
if(_lastAction.LastActionWasRecent())
{
return;
}
_lastAction = DateTime.UtcNow;
packageName = packageName.Replace("androidapp://", string.Empty);
var launchIntent = PackageManager.GetLaunchIntentForPackage(packageName);
if(launchIntent == null)
{
var dialog = Resolver.Resolve<IUserDialogs>();
dialog.Alert(string.Format(App.Resources.AppResources.CannotOpenApp, packageName));
}
else
{
StartActivity(launchIntent);
}
}
private void ListenYubiKey(bool listen)
{
if(!Utilities.NfcEnabled())
@@ -368,14 +245,35 @@ namespace Bit.Android
}
}
private void DismissKeyboard()
private AppOptions GetOptions()
{
try
var options = new AppOptions
{
var imm = (InputMethodManager)GetSystemService(InputMethodService);
imm.HideSoftInputFromWindow(CurrentFocus.WindowToken, 0);
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"),
MyVault = Intent.GetBooleanExtra("myVaultTile", false),
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false)
};
var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0);
if(fillType > 0)
{
options.FillType = (CipherType)fillType;
}
catch { }
if(Intent.GetBooleanExtra("autofillFrameworkSave", false))
{
options.SaveType = (CipherType)Intent.GetIntExtra("autofillFrameworkType", 0);
options.SaveName = Intent.GetStringExtra("autofillFrameworkName");
options.SaveUsername = Intent.GetStringExtra("autofillFrameworkUsername");
options.SavePassword = Intent.GetStringExtra("autofillFrameworkPassword");
options.SaveCardName = Intent.GetStringExtra("autofillFrameworkCardName");
options.SaveCardNumber = Intent.GetStringExtra("autofillFrameworkCardNumber");
options.SaveCardExpMonth = Intent.GetStringExtra("autofillFrameworkCardExpMonth");
options.SaveCardExpYear = Intent.GetStringExtra("autofillFrameworkCardExpYear");
options.SaveCardCode = Intent.GetStringExtra("autofillFrameworkCardCode");
}
return options;
}
}
}

View File

@@ -195,6 +195,7 @@ namespace Bit.Android
container.RegisterSingleton<IKeyDerivationService, BouncyCastleKeyDerivationService>();
container.RegisterSingleton<IAuthService, AuthService>();
container.RegisterSingleton<IFolderService, FolderService>();
container.RegisterSingleton<ICollectionService, CollectionService>();
container.RegisterSingleton<ICipherService, CipherService>();
container.RegisterSingleton<ISyncService, SyncService>();
container.RegisterSingleton<IDeviceActionService, DeviceActionService>();
@@ -226,6 +227,8 @@ namespace Bit.Android
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
// Other
container.RegisterSingleton(CrossSettings.Current);

View File

@@ -3,20 +3,20 @@ using Android.Content;
using Bit.App.Abstractions;
using Bit.App.Utilities;
using Plugin.Settings.Abstractions;
using System.Diagnostics;
using System;
using XLabs.Ioc;
namespace Bit.Android
{
[BroadcastReceiver(Name = "com.x8bit.bitwarden.PackageReplacedReceiver", Exported = true)]
[BroadcastReceiver(Name = "com.x8bit.bitwarden.PackageReplacedReceiver", Exported = false)]
[IntentFilter(new[] { Intent.ActionMyPackageReplaced })]
public class PackageReplacedReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
Debug.WriteLine("App updated!");
Console.WriteLine("Bitwarden App Updated!!");
Helpers.PerformUpdateTasks(Resolver.Resolve<ISettings>(),
Resolver.Resolve<IAppInfoService>(),Resolver.Resolve<IDatabaseService>(),
Resolver.Resolve<IAppInfoService>(), Resolver.Resolve<IDatabaseService>(),
Resolver.Resolve<ISyncService>());
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x8bit.bitwarden" android:versionName="1.12.2" android:installLocation="auto" android:versionCode="502" xmlns:tools="http://schemas.android.com/tools">
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="23" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.x8bit.bitwarden" android:versionName="1.13.0" android:installLocation="auto" android:versionCode="502" xmlns:tools="http://schemas.android.com/tools">
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="26" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:background="@color/lightgray"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="10dp"
android:maxWidth="20dp"
android:maxHeight="20dp"
android:adjustViewBounds="true"
android:src="@drawable/login" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/black"
android:text="Name" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/gray"
android:text="Username" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="AutoFillServiceSummary">
Assist with filling username and password fields in other apps and on the web.
</string>
<string name="AutoFillServiceDescription">
To allow bitwarden to auto-fill into other Android apps and on the web through your browser, enable the bitwarden
accessibility service by tapping the toggle switch above, then press OK on the confirmation pop-up. You can then press
the back button twice to return to the main bitwarden app.
It can be difficult for users (especially those with disabilities) to switch between apps and copy/paste
username and password information from their bitwarden vault.\n\nUsing this accessibility service allows bitwarden
to detect and read input fields on your device\'s screen. Whenever bitwarden detects a password field on the screen
a notification will appear that allows you to quickly access your bitwarden vault and automatically fill (auto-fill)
the correct login information into the necessary fields.
</string>
<string name="MyVault">
My Vault

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:summary="@string/AutoFillServiceSummary"
android:description="@string/AutoFillServiceDescription"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewFocused"
android:accessibilityFeedbackType="feedbackGeneric"

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8" ?>
<autofill-service
xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -33,14 +33,12 @@ namespace Bit.Android.Services
private readonly bool _oldAndroid;
private readonly ISettings _settings;
private readonly KeyStore _keyStore;
private readonly ISecureStorageService _oldKeyStorageService;
public AndroidKeyStoreStorageService(ISettings settings)
{
_oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M;
_rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
_oldKeyStorageService = new KeyStoreStorageService(new char[] { });
_settings = settings;
_keyStore = KeyStore.GetInstance(AndroidKeyStore);
@@ -53,8 +51,7 @@ namespace Bit.Android.Services
public bool Contains(string key)
{
return _settings.Contains(string.Format(SettingsFormat, key)) ||
_settings.Contains(string.Format(SettingsFormatV1, key)) ||
_oldKeyStorageService.Contains(key);
_settings.Contains(string.Format(SettingsFormatV1, key));
}
public void Delete(string key)
@@ -297,13 +294,6 @@ namespace Bit.Android.Services
private byte[] TryGetAndMigrate(string key)
{
if(_oldKeyStorageService.Contains(key))
{
var value = _oldKeyStorageService.Retrieve(key);
Store(key, value);
return value;
}
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
if(_settings.Contains(formattedKeyV1))
{
@@ -331,11 +321,6 @@ namespace Bit.Android.Services
private void CleanupOld(string key)
{
if(_oldKeyStorageService.Contains(key))
{
_oldKeyStorageService.Delete(key);
}
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
if(_settings.Contains(formattedKeyV1))
{

View File

@@ -1,4 +1,5 @@
using Android.App;
using Android.Views.Autofill;
using Bit.App.Abstractions;
using System.Linq;
using AndroidApp = Android.App.Application;
@@ -13,14 +14,27 @@ namespace Bit.Android.Services
public string Build => AndroidApp.Context.ApplicationContext.PackageManager
.GetPackageInfo(AndroidApp.Context.PackageName, 0).VersionCode.ToString();
public bool AutofillServiceEnabled => AutofillRunning();
public bool AutofillAccessibilityServiceEnabled => AutofillAccessibilityRunning();
public bool AutofillServiceEnabled => AutofillEnabled();
private bool AutofillRunning()
private bool AutofillAccessibilityRunning()
{
var manager = ((ActivityManager)Xamarin.Forms.Forms.Context.GetSystemService("activity"));
var services = manager.GetRunningServices(int.MaxValue);
return services.Any(s => s.Process.ToLowerInvariant().Contains("bitwarden") &&
s.Service.ClassName.ToLowerInvariant().Contains("autofill"));
}
private bool AutofillEnabled()
{
if(global::Android.OS.Build.VERSION.SdkInt < global::Android.OS.BuildVersionCodes.O)
{
return false;
}
var activity = (MainActivity)Xamarin.Forms.Forms.Context;
var afm = (AutofillManager)activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)));
return afm.IsEnabled;
}
}
}

View File

@@ -15,18 +15,32 @@ using System.Collections.Generic;
using Android;
using Android.Content.PM;
using Android.Support.V4.App;
using Bit.App.Models.Page;
using XLabs.Ioc;
using Android.App;
using Android.Views.Autofill;
using Android.App.Assist;
using Bit.Android.Autofill;
using System.Linq;
using Plugin.Settings.Abstractions;
using Acr.UserDialogs;
using Android.Views.InputMethods;
namespace Bit.Android.Services
{
public class DeviceActionService : IDeviceActionService
{
private readonly IAppSettingsService _appSettingsService;
private readonly IUserDialogs _userDialogs;
private bool _cameraPermissionsDenied;
private DateTime? _lastAction;
public DeviceActionService(
IAppSettingsService appSettingsService)
IAppSettingsService appSettingsService,
IUserDialogs userDialogs)
{
_appSettingsService = appSettingsService;
_userDialogs = userDialogs;
}
public void CopyToClipboard(string text)
@@ -109,34 +123,10 @@ namespace Bit.Android.Services
catch(Exception) { }
}
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;
}
}
public Task SelectFileAsync()
{
MessagingCenter.Unsubscribe<Application>(Application.Current, "SelectFileCameraPermissionDenied");
MessagingCenter.Unsubscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current,
"SelectFileCameraPermissionDenied");
var hasStorageWritePermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.WriteExternalStorage);
@@ -189,6 +179,195 @@ namespace Bit.Android.Services
return Task.FromResult(0);
}
public void Autofill(VaultListPageModel.Cipher cipher)
{
var activity = (MainActivity)Forms.Context;
if(activity.Intent.GetBooleanExtra("autofillFramework", 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);
parser.Parse();
if(!parser.FieldCollection.Fields.Any() || string.IsNullOrWhiteSpace(parser.Uri))
{
activity.SetResult(Result.Canceled);
activity.Finish();
return;
}
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection,
new FilledItem(cipher.CipherModel));
var replyIntent = new Intent();
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
activity.SetResult(Result.Ok, replyIntent);
activity.Finish();
}
else
{
var data = new Intent();
if(cipher == null)
{
data.PutExtra("canceled", "true");
}
else
{
var isPremium = Resolver.Resolve<ITokenService>()?.TokenPremium ?? false;
var settings = Resolver.Resolve<ISettings>();
var autoCopyEnabled = !settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false);
if(isPremium && autoCopyEnabled && cipher.LoginTotp?.Value != null)
{
CopyToClipboard(App.Utilities.Crypto.Totp(cipher.LoginTotp.Value));
}
data.PutExtra("uri", cipher.LoginUri);
data.PutExtra("username", cipher.LoginUsername);
data.PutExtra("password", cipher.LoginPassword?.Value ?? null);
}
if(activity.Parent == null)
{
activity.SetResult(Result.Ok, data);
}
else
{
activity.Parent.SetResult(Result.Ok, data);
}
activity.Finish();
MessagingCenter.Send(Xamarin.Forms.Application.Current, "FinishMainActivity");
}
}
public void CloseAutofill()
{
Autofill(null);
}
public void Background()
{
var activity = (MainActivity)Forms.Context;
if(activity.Intent.GetBooleanExtra("autofillFramework", false))
{
activity.SetResult(Result.Canceled);
activity.Finish();
}
else
{
activity.MoveTaskToBack(true);
}
}
public void RateApp()
{
var activity = (MainActivity)Forms.Context;
try
{
var rateIntent = RateIntentForUrl("market://details", activity);
activity.StartActivity(rateIntent);
}
catch(ActivityNotFoundException)
{
var rateIntent = RateIntentForUrl("https://play.google.com/store/apps/details", activity);
activity.StartActivity(rateIntent);
}
}
public void DismissKeyboard()
{
var activity = (MainActivity)Forms.Context;
try
{
var imm = (InputMethodManager)activity.GetSystemService(Context.InputMethodService);
imm.HideSoftInputFromWindow(activity.CurrentFocus.WindowToken, 0);
}
catch { }
}
public void OpenAccessibilitySettings()
{
var activity = (MainActivity)Forms.Context;
var intent = new Intent(Settings.ActionAccessibilitySettings);
activity.StartActivity(intent);
}
public void LaunchApp(string appName)
{
var activity = (MainActivity)Forms.Context;
if(_lastAction.LastActionWasRecent())
{
return;
}
_lastAction = DateTime.UtcNow;
appName = appName.Replace("androidapp://", string.Empty);
var launchIntent = activity.PackageManager.GetLaunchIntentForPackage(appName);
if(launchIntent == null)
{
_userDialogs.Alert(string.Format(AppResources.CannotOpenApp, appName));
}
else
{
activity.StartActivity(launchIntent);
}
}
private Intent RateIntentForUrl(string url, Activity activity)
{
var intent = new Intent(Intent.ActionView, global::Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
var flags = ActivityFlags.NoHistory | ActivityFlags.MultipleTask;
if((int)Build.VERSION.SdkInt >= 21)
{
flags |= ActivityFlags.NewDocument;
}
else
{
// noinspection deprecation
flags |= ActivityFlags.ClearWhenTaskReset;
}
intent.AddFlags(flags);
return intent;
}
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 List<IParcelable> GetCameraIntents(global::Android.Net.Uri outputUri)
{
var intents = new List<IParcelable>();
@@ -214,10 +393,11 @@ namespace Bit.Android.Services
private void AskCameraPermission(string permission)
{
MessagingCenter.Subscribe<Application>(Application.Current, "SelectFileCameraPermissionDenied", (sender) =>
{
_cameraPermissionsDenied = true;
});
MessagingCenter.Subscribe<Xamarin.Forms.Application>(Xamarin.Forms.Application.Current,
"SelectFileCameraPermissionDenied", (sender) =>
{
_cameraPermissionsDenied = true;
});
AskPermission(permission);
}
@@ -227,5 +407,13 @@ namespace Bit.Android.Services
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
Constants.SelectFilePermissionRequestCode);
}
public void OpenAutofillSettings()
{
var activity = (MainActivity)Forms.Context;
var intent = new Intent(Settings.ActionRequestSetAutofillService);
intent.SetData(global::Android.Net.Uri.Parse("package:com.x8bit.bitwarden"));
activity.StartActivity(intent);
}
}
}

View File

@@ -1,6 +1,7 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Views.Autofill;
using Bit.App.Abstractions;
namespace Bit.Android.Services
@@ -44,5 +45,18 @@ namespace Bit.Android.Services
}
public bool NfcEnabled => Utilities.NfcEnabled();
public bool HasCamera => Xamarin.Forms.Forms.Context.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
public bool AutofillServiceSupported => AutofillSupported();
public bool HasFaceIdSupport => false;
private bool AutofillSupported()
{
if(Build.VERSION.SdkInt < BuildVersionCodes.O)
{
return false;
}
var activity = (MainActivity)Xamarin.Forms.Forms.Context;
var afm = (AutofillManager)activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)));
return afm.IsAutofillSupported;
}
}
}

View File

@@ -10,17 +10,13 @@ namespace Bit.Android.Services
public class GoogleAnalyticsService : IGoogleAnalyticsService
{
private readonly GoogleAnalytics _instance;
private readonly IAuthService _authService;
private readonly Tracker _tracker;
public GoogleAnalyticsService(
Context appContext,
IAppIdService appIdService,
IAuthService authService,
ISettings settings)
{
_authService = authService;
_instance = GoogleAnalytics.GetInstance(appContext.ApplicationContext);
_instance.SetLocalDispatchPeriod(10);

View File

@@ -1,113 +0,0 @@
using System.IO;
using System.IO.IsolatedStorage;
using Java.Lang;
using Java.Security;
using Javax.Crypto;
using Android.OS;
using Bit.App.Abstractions;
namespace Bit.Android.Services
{
[System.Obsolete]
public class KeyStoreStorageService : ISecureStorageService
{
private const string StorageFile = "Bit.Android.KeyStoreStorageService";
private static readonly object SaveLock = new object();
private readonly KeyStore _keyStore;
private readonly KeyStore.PasswordProtection _protection;
public KeyStoreStorageService()
: this(Build.Serial.ToCharArray()) { }
public KeyStoreStorageService(char[] password)
{
_keyStore = KeyStore.GetInstance(KeyStore.DefaultType);
_protection = new KeyStore.PasswordProtection(password);
if(File.FileExists(StorageFile))
{
using(var stream = new IsolatedStorageFileStream(StorageFile, FileMode.Open, FileAccess.Read, File))
{
_keyStore.Load(stream, password);
}
}
else
{
_keyStore.Load(null, password);
}
}
private static IsolatedStorageFile File
{
get { return IsolatedStorageFile.GetUserStoreForApplication(); }
}
public void Store(string key, byte[] dataBytes)
{
_keyStore.SetEntry(key, new KeyStore.SecretKeyEntry(new SecureData(dataBytes)), _protection);
Save();
}
public byte[] Retrieve(string key)
{
var entry = _keyStore.GetEntry(key, _protection) as KeyStore.SecretKeyEntry;
if(entry == null)
{
return null;
}
return entry.SecretKey.GetEncoded();
}
public void Delete(string key)
{
_keyStore.DeleteEntry(key);
Save();
}
public bool Contains(string key)
{
return _keyStore.ContainsAlias(key);
}
private void Save()
{
lock(SaveLock)
{
using(var stream = new IsolatedStorageFileStream(StorageFile, FileMode.OpenOrCreate, FileAccess.Write, File))
{
_keyStore.Store(stream, _protection.GetPassword());
}
}
}
private class SecureData : Object, ISecretKey
{
private const string Raw = "RAW";
private readonly byte[] _data;
public SecureData(byte[] dataBytes)
{
_data = dataBytes;
}
public string Algorithm
{
get { return Raw; }
}
public string Format
{
get { return Raw; }
}
public byte[] GetEncoded()
{
return _data;
}
}
}
}

View File

@@ -16,7 +16,7 @@
<package id="PInvoke.NCrypt" version="0.5.97" targetFramework="monoandroid71" />
<package id="PInvoke.Windows.Core" version="0.5.97" targetFramework="monoandroid71" />
<package id="Plugin.CurrentActivity" version="1.0.1" targetFramework="monoandroid60" />
<package id="Plugin.Fingerprint" version="1.4.5" targetFramework="monoandroid71" />
<package id="Plugin.Fingerprint" version="1.4.6-beta4" targetFramework="monoandroid80" />
<package id="SimpleInjector" version="4.0.8" targetFramework="monoandroid71" />
<package id="Splat" version="1.6.2" targetFramework="monoandroid60" />
<package id="sqlite-net-pcl" version="1.5.166-beta" targetFramework="monoandroid71" />

View File

@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Bit.App.Models.Data;
using System.Collections.Generic;
namespace Bit.App.Abstractions
{
public interface ICipherCollectionRepository
{
Task<IEnumerable<CipherCollectionData>> GetAllByUserIdAsync(string userId);
Task<IEnumerable<CipherCollectionData>> GetAllByUserIdCollectionAsync(string userId, string collectionId);
Task InsertAsync(CipherCollectionData obj);
Task DeleteAsync(CipherCollectionData obj);
Task DeleteByUserIdAsync(string userId);
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Data;
namespace Bit.App.Abstractions
{
public interface ICollectionRepository : IRepository<CollectionData, string>
{
Task<IEnumerable<CollectionData>> GetAllByUserIdAsync(string userId);
}
}

View File

@@ -4,6 +4,7 @@
{
string Build { get; }
string Version { get; }
bool AutofillAccessibilityServiceEnabled { get; }
bool AutofillServiceEnabled { get; }
}
}

View File

@@ -4,6 +4,7 @@ namespace Bit.App.Abstractions
{
public interface IAppSettingsService
{
bool DefaultPageVault { get; set; }
bool Locked { get; set; }
DateTime LastActivity { get; set; }
DateTime LastCacheClear { get; set; }

View File

@@ -12,9 +12,8 @@ namespace Bit.App.Abstractions
bool UserIdChanged { get; }
string Email { get; set; }
string PIN { get; set; }
bool BelongsToOrganization(string orgId);
void LogOut();
void LogOut(string logoutMessage = null);
Task<FullLoginResult> TokenPostAsync(string email, string masterPassword);
Task<LoginResult> TokenPostTwoFactorAsync(TwoFactorProviderType type, string token, bool remember, string email,
string masterPasswordHash, SymmetricCryptoKey key);

View File

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Models.Api;
using System;
using Bit.App.Models.Data;
namespace Bit.App.Abstractions
{
@@ -11,11 +12,17 @@ namespace Bit.App.Abstractions
Task<Cipher> GetByIdAsync(string id);
Task<IEnumerable<Cipher>> GetAllAsync();
Task<IEnumerable<Cipher>> GetAllAsync(bool favorites);
Task<Tuple<IEnumerable<Cipher>, IEnumerable<Cipher>>> GetAllAsync(string uriString);
Task<IEnumerable<Cipher>> GetAllByFolderAsync(string folderId);
Task<IEnumerable<Cipher>> GetAllByCollectionAsync(string collectionId);
Task<Tuple<IEnumerable<Cipher>, IEnumerable<Cipher>, IEnumerable<Cipher>>> GetAllAsync(string uriString);
Task<ApiResult<CipherResponse>> SaveAsync(Cipher cipher);
Task UpsertDataAsync(CipherData cipher);
Task<ApiResult> DeleteAsync(string id);
Task DeleteDataAsync(string id);
Task<byte[]> DownloadAndDecryptAttachmentAsync(string url, string orgId = null);
Task<ApiResult<CipherResponse>> EncryptAndSaveAttachmentAsync(Cipher cipher, byte[] data, string fileName);
Task UpsertAttachmentDataAsync(IEnumerable<AttachmentData> attachments);
Task<ApiResult> DeleteAttachmentAsync(Cipher cipher, string attachmentId);
Task DeleteAttachmentDataAsync(string attachmentId);
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using System;
namespace Bit.App.Abstractions
{
public interface ICollectionService
{
Task<Collection> GetByIdAsync(string id);
Task<IEnumerable<Collection>> GetAllAsync();
Task<IEnumerable<Tuple<string, string>>> GetAllCipherAssociationsAsync();
}
}

View File

@@ -10,5 +10,13 @@ namespace Bit.App.Abstractions
bool CanOpenFile(string fileName);
Task SelectFileAsync();
void ClearCache();
void Autofill(Models.Page.VaultListPageModel.Cipher cipher);
void CloseAutofill();
void Background();
void RateApp();
void DismissKeyboard();
void OpenAccessibilitySettings();
void OpenAutofillSettings();
void LaunchApp(string appName);
}
}

View File

@@ -7,5 +7,7 @@
float Scale { get; }
bool NfcEnabled { get; }
bool HasCamera { get; }
bool AutofillServiceSupported { get; }
bool HasFaceIdSupport { get; }
}
}

View File

@@ -8,5 +8,7 @@ namespace Bit.App.Abstractions
{
void UpdateLastActivity(DateTime? activityDate = null);
Task<LockType> GetLockTypeAsync(bool forceLock);
Task CheckLockAsync(bool forceLock);
bool TopPageIsLock();
}
}

View File

@@ -14,12 +14,13 @@ using XLabs.Ioc;
using System.Reflection;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.App.Models;
namespace Bit.App
{
public class App : Application
{
private string _uri;
private AppOptions _options;
private readonly IDatabaseService _databaseService;
private readonly IConnectivity _connectivity;
private readonly IUserDialogs _userDialogs;
@@ -34,8 +35,7 @@ namespace Bit.App
private readonly IDeviceActionService _deviceActionService;
public App(
string uri,
bool myVault,
AppOptions options,
IAuthService authService,
IConnectivity connectivity,
IUserDialogs userDialogs,
@@ -49,7 +49,7 @@ namespace Bit.App
IAppSettingsService appSettingsService,
IDeviceActionService deviceActionService)
{
_uri = uri;
_options = options ?? new AppOptions();
_databaseService = databaseService;
_connectivity = connectivity;
_userDialogs = userDialogs;
@@ -66,42 +66,42 @@ namespace Bit.App
SetCulture();
SetStyles();
if(authService.IsAuthenticated && _uri != null)
if(authService.IsAuthenticated)
{
MainPage = new ExtendedNavigationPage(new VaultAutofillListCiphersPage(_uri));
}
else if(authService.IsAuthenticated)
{
MainPage = new MainPage(myVault: myVault);
if(_options.FromAutofillFramework && _options.SaveType.HasValue)
{
MainPage = new ExtendedNavigationPage(new VaultAddCipherPage(_options));
}
else if(_options.Uri != null)
{
MainPage = new ExtendedNavigationPage(new VaultAutofillListCiphersPage(_options));
}
else
{
MainPage = new MainPage(myVault: _options.MyVault);
}
}
else
{
MainPage = new ExtendedNavigationPage(new HomePage());
}
MessagingCenter.Subscribe<Application, bool>(Current, "Resumed", async (sender, args) =>
if(Device.RuntimePlatform == Device.iOS)
{
Device.BeginInvokeOnMainThread(async () => await CheckLockAsync(args));
await Task.Run(() => FullSyncAsync()).ConfigureAwait(false);
});
MessagingCenter.Subscribe<Application, bool>(Current, "Lock", (sender, args) =>
{
Device.BeginInvokeOnMainThread(async () => await CheckLockAsync(args));
});
MessagingCenter.Subscribe<Application, string>(Current, "Logout", (sender, args) =>
{
Logout(args);
});
MessagingCenter.Subscribe<Application, bool>(Current, "Resumed", async (sender, args) =>
{
Device.BeginInvokeOnMainThread(async () => await _lockService.CheckLockAsync(args));
await Task.Run(() => FullSyncAsync()).ConfigureAwait(false);
});
}
}
protected async override void OnStart()
{
// Handle when your app starts
await CheckLockAsync(false);
await _lockService.CheckLockAsync(false);
if(string.IsNullOrWhiteSpace(_uri))
if(string.IsNullOrWhiteSpace(_options.Uri))
{
var updated = Helpers.PerformUpdateTasks(_settings, _appInfoService, _databaseService, _syncService);
if(!updated)
@@ -125,7 +125,7 @@ namespace Bit.App
SetMainPageFromAutofill();
if(Device.RuntimePlatform == Device.Android && !TopPageIsLock())
if(Device.RuntimePlatform == Device.Android && !_lockService.TopPageIsLock())
{
_lockService.UpdateLastActivity();
}
@@ -144,7 +144,7 @@ namespace Bit.App
if(Device.RuntimePlatform == Device.Android)
{
await CheckLockAsync(false);
await _lockService.CheckLockAsync(false);
}
var lockPinPage = Current.MainPage.Navigation.ModalStack.LastOrDefault() as LockPinPage;
@@ -168,14 +168,15 @@ namespace Bit.App
private void SetMainPageFromAutofill()
{
if(Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(_uri))
if(Device.RuntimePlatform == Device.Android && !string.IsNullOrWhiteSpace(_options.Uri) &&
!_options.FromAutofillFramework)
{
Task.Run(() =>
{
Device.BeginInvokeOnMainThread(() =>
{
Current.MainPage = new MainPage();
_uri = null;
_options.Uri = null;
});
});
}
@@ -219,73 +220,6 @@ namespace Bit.App
}
}
private void Logout(string logoutMessage)
{
_authService.LogOut();
var deviceApiRepository = Resolver.Resolve<IDeviceApiRepository>();
var appIdService = Resolver.Resolve<IAppIdService>();
Task.Run(async () => await deviceApiRepository.PutClearTokenAsync(appIdService.AppId));
_googleAnalyticsService.TrackAppEvent("LoggedOut");
Device.BeginInvokeOnMainThread(() => Current.MainPage = new ExtendedNavigationPage(new HomePage()));
if(!string.IsNullOrWhiteSpace(logoutMessage))
{
_userDialogs.Toast(logoutMessage);
}
}
private async Task CheckLockAsync(bool forceLock)
{
if(TopPageIsLock())
{
// already locked
return;
}
var lockType = await _lockService.GetLockTypeAsync(forceLock);
if(lockType == Enums.LockType.None)
{
return;
}
_appSettingsService.Locked = true;
switch(lockType)
{
case Enums.LockType.Fingerprint:
await Current.MainPage.Navigation.PushModalAsync(new ExtendedNavigationPage(new LockFingerprintPage(!forceLock)), false);
break;
case Enums.LockType.PIN:
await Current.MainPage.Navigation.PushModalAsync(new ExtendedNavigationPage(new LockPinPage()), false);
break;
case Enums.LockType.Password:
await Current.MainPage.Navigation.PushModalAsync(new ExtendedNavigationPage(new LockPasswordPage()), false);
break;
default:
break;
}
}
private bool TopPageIsLock()
{
var currentPage = Current.MainPage.Navigation.ModalStack.LastOrDefault() as ExtendedNavigationPage;
if((currentPage?.CurrentPage as LockFingerprintPage) != null)
{
return true;
}
if((currentPage?.CurrentPage as LockPinPage) != null)
{
return true;
}
if((currentPage?.CurrentPage as LockPasswordPage) != null)
{
return true;
}
return false;
}
private void SetStyles()
{
var gray = Color.FromHex("333333");

View File

@@ -36,6 +36,8 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Abstractions\Repositories\IAttachmentRepository.cs" />
<Compile Include="Abstractions\Repositories\ICipherCollectionRepository.cs" />
<Compile Include="Abstractions\Repositories\ICollectionRepository.cs" />
<Compile Include="Abstractions\Repositories\ISyncApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" />
@@ -43,6 +45,7 @@
<Compile Include="Abstractions\Repositories\IDeviceApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ISettingsRepository.cs" />
<Compile Include="Abstractions\Services\IAppSettingsService.cs" />
<Compile Include="Abstractions\Services\ICollectionService.cs" />
<Compile Include="Abstractions\Services\IMemoryService.cs" />
<Compile Include="Abstractions\Services\IPushNotificationListener.cs" />
<Compile Include="Abstractions\Services\IPushNotification.cs" />
@@ -64,6 +67,7 @@
<Compile Include="Abstractions\Services\ISecureStorageService.cs" />
<Compile Include="Abstractions\Services\ISqlService.cs" />
<Compile Include="Constants.cs" />
<Compile Include="Controls\AddCipherToolBarItem.cs" />
<Compile Include="Controls\HybridWebView.cs" />
<Compile Include="Controls\ExtendedToolbarItem.cs" />
<Compile Include="Controls\DismissModalToolBarItem.cs" />
@@ -73,6 +77,7 @@
<Compile Include="Controls\ExtendedContentPage.cs" />
<Compile Include="Controls\LabeledRightDetailCell.cs" />
<Compile Include="Controls\MemoryContentView.cs" />
<Compile Include="Controls\SectionHeaderViewCell.cs" />
<Compile Include="Controls\StepperCell.cs" />
<Compile Include="Controls\ExtendedTableView.cs" />
<Compile Include="Controls\ExtendedPicker.cs" />
@@ -88,6 +93,7 @@
<Compile Include="Controls\FormEntryCell.cs" />
<Compile Include="Controls\PinControl.cs" />
<Compile Include="Controls\VaultAttachmentsViewCell.cs" />
<Compile Include="Controls\VaultGroupingViewCell.cs" />
<Compile Include="Controls\VaultListViewCell.cs" />
<Compile Include="Enums\DeviceType.cs" />
<Compile Include="Enums\FieldType.cs" />
@@ -107,6 +113,7 @@
<Compile Include="Models\Api\FieldDataModel.cs" />
<Compile Include="Models\Api\CardDataModel.cs" />
<Compile Include="Models\Api\IdentityDataModel.cs" />
<Compile Include="Models\Api\Response\CollectionResponse.cs" />
<Compile Include="Models\Api\SecureNoteDataModel.cs" />
<Compile Include="Models\Api\Request\DeviceTokenRequest.cs" />
<Compile Include="Models\Api\Request\FolderRequest.cs" />
@@ -129,11 +136,15 @@
<Compile Include="Models\Api\Response\TokenResponse.cs" />
<Compile Include="Models\Api\Response\ProfileResponse.cs" />
<Compile Include="Models\Api\LoginDataModel.cs" />
<Compile Include="Models\AppOptions.cs" />
<Compile Include="Models\Card.cs" />
<Compile Include="Models\CipherString.cs" />
<Compile Include="Models\Data\AttachmentData.cs" />
<Compile Include="Models\Attachment.cs" />
<Compile Include="Models\Data\CipherCollectionData.cs" />
<Compile Include="Models\Data\CollectionData.cs" />
<Compile Include="Models\Field.cs" />
<Compile Include="Models\Collection.cs" />
<Compile Include="Models\Identity.cs" />
<Compile Include="Models\Login.cs" />
<Compile Include="Models\Page\VaultAttachmentsPageModel.cs" />
@@ -164,7 +175,7 @@
<Compile Include="Pages\ScanPage.cs" />
<Compile Include="Pages\Settings\SettingsCreditsPage.cs" />
<Compile Include="Pages\Settings\SettingsHelpPage.cs" />
<Compile Include="Pages\Settings\SettingsFeaturesPage.cs" />
<Compile Include="Pages\Settings\SettingsOptionsPage.cs" />
<Compile Include="Pages\Settings\SettingsPinPage.cs" />
<Compile Include="Pages\Lock\LockPinPage.cs" />
<Compile Include="Pages\MainPage.cs" />
@@ -172,6 +183,7 @@
<Compile Include="Pages\Lock\LockFingerprintPage.cs" />
<Compile Include="Pages\Settings\SettingsAboutPage.cs" />
<Compile Include="Pages\Tools\ToolsAutofillServicePage.cs" />
<Compile Include="Pages\Tools\ToolsAccessibilityServicePage.cs" />
<Compile Include="Pages\Tools\ToolsExtensionPage.cs" />
<Compile Include="Pages\Tools\ToolsPasswordGeneratorSettingsPage.cs" />
<Compile Include="Pages\Tools\ToolsPasswordGeneratorPage.cs" />
@@ -182,9 +194,14 @@
<Compile Include="Pages\Vault\VaultCustomFieldsPage.cs" />
<Compile Include="Pages\Vault\VaultAutofillListCiphersPage.cs" />
<Compile Include="Pages\Vault\VaultAttachmentsPage.cs" />
<Compile Include="Pages\Vault\VaultListCiphersPage.cs" />
<Compile Include="Pages\Vault\VaultListGroupingsPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ICipherRepository.cs" />
<Compile Include="Repositories\AttachmentRepository.cs" />
<Compile Include="Repositories\BaseRepository.cs" />
<Compile Include="Repositories\CipherCollectionRepository.cs" />
<Compile Include="Repositories\CollectionRepository.cs" />
<Compile Include="Repositories\SyncApiRepository.cs" />
<Compile Include="Repositories\TwoFactorApiRepository.cs" />
<Compile Include="Repositories\SettingsApiRepository.cs" />
@@ -340,6 +357,7 @@
<DependentUpon>AppResources.zh-Hant.resx</DependentUpon>
</Compile>
<Compile Include="Services\AppSettingsService.cs" />
<Compile Include="Services\CollectionService.cs" />
<Compile Include="Services\SettingsService.cs" />
<Compile Include="Services\TokenService.cs" />
<Compile Include="Services\AppIdService.cs" />
@@ -362,7 +380,6 @@
<Compile Include="Pages\Vault\VaultAddCipherPage.cs" />
<Compile Include="Pages\Vault\VaultViewCipherPage.cs" />
<Compile Include="Pages\Vault\VaultEditCipherPage.cs" />
<Compile Include="Pages\Vault\VaultListCiphersPage.cs" />
<Compile Include="Services\PasswordGenerationService.cs" />
<Compile Include="Utilities\Base32.cs" />
<Compile Include="Utilities\Crypto.cs" />
@@ -537,11 +554,11 @@
<Reference Include="Plugin.Connectivity.Abstractions, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Xam.Plugin.Connectivity.3.0.2\lib\netstandard1.0\Plugin.Connectivity.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Plugin.Fingerprint, Version=1.4.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.5\lib\portable-net45+win8+wpa81+wp8\Plugin.Fingerprint.dll</HintPath>
<Reference Include="Plugin.Fingerprint, Version=1.4.6.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.6-beta4\lib\portable-net45+win8+wpa81+wp8\Plugin.Fingerprint.dll</HintPath>
</Reference>
<Reference Include="Plugin.Fingerprint.Abstractions, Version=1.4.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.5\lib\portable-net45+win8+wpa81+wp8\Plugin.Fingerprint.Abstractions.dll</HintPath>
<Reference Include="Plugin.Fingerprint.Abstractions, Version=1.4.6.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Plugin.Fingerprint.1.4.6-beta4\lib\portable-net45+win8+wpa81+wp8\Plugin.Fingerprint.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Plugin.Settings, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Xam.Plugins.Settings.3.0.1\lib\netstandard1.0\Plugin.Settings.dll</HintPath>

View File

@@ -13,6 +13,7 @@
public const string SettingDisableTotpCopy = "setting:disableAutoCopyTotp";
public const string AutofillPersistNotification = "setting:persistNotification";
public const string AutofillPasswordField = "setting:autofillPasswordField";
public const string SettingDefaultPageVault = "setting:defaultPageVault";
public const string PasswordGeneratorLength = "pwGenerator:length";
public const string PasswordGeneratorUppercase = "pwGenerator:uppercase";

View File

@@ -0,0 +1,16 @@
using Bit.App.Resources;
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class AddCipherToolBarItem : ExtendedToolbarItem
{
public AddCipherToolBarItem(Page page, string folderId)
: base(() => Helpers.AddCipher(page, folderId))
{
Text = AppResources.Add;
Icon = "plus.png";
}
}
}

View File

@@ -1,6 +1,4 @@
using Bit.App.Abstractions;
using Plugin.Settings.Abstractions;
using System;
using Xamarin.Forms;
using XLabs.Ioc;
@@ -11,6 +9,7 @@ namespace Bit.App.Controls
private ISyncService _syncService;
private IGoogleAnalyticsService _googleAnalyticsService;
private ILockService _lockService;
private IDeviceActionService _deviceActionService;
private bool _syncIndicator;
private bool _updateActivity;
@@ -21,25 +20,21 @@ namespace Bit.App.Controls
_syncService = Resolver.Resolve<ISyncService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_lockService = Resolver.Resolve<ILockService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
BackgroundColor = Color.FromHex("efeff4");
if(_syncIndicator)
{
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted", (sender, success) =>
{
Device.BeginInvokeOnMainThread(() => IsBusy = _syncService.SyncInProgress);
});
MessagingCenter.Subscribe<Application>(Application.Current, "SyncStarted", (sender) =>
{
Device.BeginInvokeOnMainThread(() => IsBusy = _syncService.SyncInProgress);
});
}
}
protected override void OnAppearing()
{
if(_syncIndicator)
{
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted",
(sender, success) => Device.BeginInvokeOnMainThread(() => IsBusy = _syncService.SyncInProgress));
MessagingCenter.Subscribe<ISyncService>(Application.Current, "SyncStarted",
(sender) => Device.BeginInvokeOnMainThread(() => IsBusy = _syncService.SyncInProgress));
}
if(_syncIndicator)
{
IsBusy = _syncService.SyncInProgress;
@@ -51,6 +46,12 @@ namespace Bit.App.Controls
protected override void OnDisappearing()
{
if(_syncIndicator)
{
MessagingCenter.Unsubscribe<Application, bool>(Application.Current, "SyncCompleted");
MessagingCenter.Unsubscribe<Application>(Application.Current, "SyncStarted");
}
if(_syncIndicator)
{
IsBusy = false;
@@ -62,7 +63,7 @@ namespace Bit.App.Controls
}
base.OnDisappearing();
MessagingCenter.Send(Application.Current, "DismissKeyboard");
_deviceActionService.DismissKeyboard();
}
}
}

View File

@@ -0,0 +1,45 @@
using Bit.App.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class SectionHeaderViewCell : ExtendedViewCell
{
public SectionHeaderViewCell(string bindingName, string countBindingName = null, Thickness? padding = null)
{
var label = new Label
{
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"],
VerticalTextAlignment = TextAlignment.Center,
HorizontalOptions = LayoutOptions.StartAndExpand
};
label.SetBinding(Label.TextProperty, bindingName);
var stackLayout = new StackLayout
{
Padding = padding ?? new Thickness(16, 8),
Children = { label },
Orientation = StackOrientation.Horizontal
};
if(!string.IsNullOrWhiteSpace(countBindingName))
{
var countLabel = new Label
{
LineBreakMode = LineBreakMode.NoWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"],
HorizontalOptions = LayoutOptions.End,
VerticalTextAlignment = TextAlignment.Center
};
countLabel.SetBinding(Label.TextProperty, countBindingName);
stackLayout.Children.Add(countLabel);
}
View = stackLayout;
BackgroundColor = Color.FromHex("efeff4");
}
}
}

View File

@@ -0,0 +1,79 @@
using Bit.App.Models.Page;
using FFImageLoading.Forms;
using System;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class VaultGroupingViewCell : ExtendedViewCell
{
public static readonly BindableProperty GroupingParameterProeprty = BindableProperty.Create(nameof(GroupingParameter),
typeof(VaultListPageModel.Grouping), typeof(VaultGroupingViewCell), null);
public VaultGroupingViewCell()
{
Icon = new CachedImage
{
WidthRequest = 20,
HeightRequest = 20,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
Source = "folder.png",
Margin = new Thickness(0, 0, 10, 0)
};
Label = new Label
{
LineBreakMode = LineBreakMode.TailTruncation,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
HorizontalOptions = LayoutOptions.StartAndExpand
};
Label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Grouping.Name));
CountLabel = new Label
{
LineBreakMode = LineBreakMode.NoWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"],
HorizontalOptions = LayoutOptions.End
};
CountLabel.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Grouping.Count));
var stackLayout = new StackLayout
{
Spacing = 0,
Padding = new Thickness(16, 8),
Children = { Icon, Label, CountLabel },
Orientation = StackOrientation.Horizontal
};
if(Device.RuntimePlatform == Device.Android)
{
Label.TextColor = Color.Black;
}
View = stackLayout;
BackgroundColor = Color.White;
SetBinding(GroupingParameterProeprty, new Binding("."));
}
public VaultListPageModel.Grouping GroupingParameter
{
get => GetValue(GroupingParameterProeprty) as VaultListPageModel.Grouping;
set { SetValue(GroupingParameterProeprty, value); }
}
public CachedImage Icon { get; private set; }
public Label Label { get; private set; }
public Label CountLabel { get; private set; }
protected override void OnBindingContextChanged()
{
if(BindingContext is VaultListPageModel.Grouping grouping)
{
Icon.Source = grouping.Folder ? $"folder{(grouping.Id == null ? "_o" : string.Empty)}.png" : "cube.png";
}
base.OnBindingContextChanged();
}
}
}

View File

@@ -1,5 +1,4 @@
using Bit.App.Models.Page;
using FFImageLoading.Forms;
using System;
using Xamarin.Forms;

View File

@@ -17,6 +17,7 @@ namespace Bit.App.Models.Api
public bool OrganizationUseTotp { get; set; }
public JObject Data { get; set; }
public IEnumerable<AttachmentResponse> Attachments { get; set; }
public IEnumerable<string> CollectionIds { get; set; }
public DateTime RevisionDate { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace Bit.App.Models.Api
{
public class CollectionResponse
{
public string Id { get; set; }
public string Name { get; set; }
public string OrganizationId { get; set; }
}
}

View File

@@ -6,6 +6,7 @@ namespace Bit.App.Models.Api
{
public ProfileResponse Profile { get; set; }
public IEnumerable<FolderResponse> Folders { get; set; }
public IEnumerable<CollectionResponse> Collections { get; set; }
public IEnumerable<CipherResponse> Ciphers { get; set; }
public DomainsResponse Domains { get; set; }
}

View File

@@ -0,0 +1,21 @@
using Bit.App.Enums;
namespace Bit.App.Models
{
public class AppOptions
{
public bool MyVault { get; set; }
public bool FromAutofillFramework { get; set; }
public CipherType? FillType { get; set; }
public string Uri { get; set; }
public CipherType? SaveType { get; set; }
public string SaveName { get; set; }
public string SaveUsername { get; set; }
public string SavePassword { get; set; }
public string SaveCardName { get; set; }
public string SaveCardNumber { get; set; }
public string SaveCardExpMonth { get; set; }
public string SaveCardExpYear { get; set; }
public string SaveCardCode { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
using Bit.App.Models.Data;
using Bit.App.Models.Api;
namespace Bit.App.Models
{
public class Collection
{
public Collection()
{ }
public Collection(CollectionData data)
{
Id = data.Id;
OrganizationId = data.OrganizationId;
Name = data.Name != null ? new CipherString(data.Name) : null;
}
public Collection(CollectionResponse response)
{
Id = response.Id;
OrganizationId = response.OrganizationId;
Name = response.Name != null ? new CipherString(response.Name) : null;
}
public string Id { get; set; }
public string OrganizationId { get; set; }
public CipherString Name { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using SQLite;
namespace Bit.App.Models.Data
{
[Table("CipherCollection")]
public class CipherCollectionData
{
[PrimaryKey]
[AutoIncrement]
public int Id { get; set; }
[Indexed]
public string UserId { get; set; }
[Indexed]
public string CipherId { get; set; }
[Indexed]
public string CollectionId { get; set; }
}
}

View File

@@ -0,0 +1,36 @@
using SQLite;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
namespace Bit.App.Models.Data
{
[Table("Collection")]
public class CollectionData : IDataObject<string>
{
public CollectionData()
{ }
public CollectionData(Collection collection, string userId)
{
Id = collection.Id;
UserId = userId;
Name = collection.Name?.EncryptedString;
OrganizationId = collection.OrganizationId;
}
public CollectionData(CollectionResponse collection, string userId)
{
Id = collection.Id;
UserId = userId;
Name = collection.Name;
OrganizationId = collection.OrganizationId;
}
[PrimaryKey]
public string Id { get; set; }
[Indexed]
public string UserId { get; set; }
public string Name { get; set; }
public string OrganizationId { get; set; }
}
}

View File

@@ -32,10 +32,5 @@ namespace Bit.App.Models.Data
public string UserId { get; set; }
public string Name { get; set; }
public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow;
public Folder ToFolder()
{
return new Folder(this);
}
}
}

View File

@@ -10,8 +10,8 @@ namespace Bit.App.Models
public Field(FieldDataModel model)
{
Type = model.Type;
Name = new CipherString(model.Name);
Value = new CipherString(model.Value);
Name = model.Name != null ? new CipherString(model.Name) : null;
Value = model.Value != null ? new CipherString(model.Value) : null;
}
public FieldType Type { get; set; }

View File

@@ -13,6 +13,7 @@ namespace Bit.App.Models.Page
{
public Cipher(Models.Cipher cipher, IAppSettingsService appSettings)
{
CipherModel = cipher;
Id = cipher.Id;
Shared = !string.IsNullOrWhiteSpace(cipher.OrganizationId);
HasAttachments = cipher.Attachments?.Any() ?? false;
@@ -20,6 +21,23 @@ namespace Bit.App.Models.Page
Name = cipher.Name?.Decrypt(cipher.OrganizationId);
Type = cipher.Type;
if(string.IsNullOrWhiteSpace(Name) || Name.Length == 0)
{
NameGroup = AppResources.Other;
}
else if(Char.IsLetter(Name[0]))
{
NameGroup = Name[0].ToString();
}
else if(Char.IsDigit(Name[0]))
{
NameGroup = "#";
}
else
{
NameGroup = AppResources.Other;
}
switch(cipher.Type)
{
case CipherType.Login:
@@ -114,10 +132,12 @@ namespace Bit.App.Models.Page
}
}
public Models.Cipher CipherModel { get; set; }
public string Id { get; set; }
public bool Shared { get; set; }
public bool HasAttachments { get; set; }
public string FolderId { get; set; }
public string NameGroup { get; set; }
public string Name { get; set; }
public string Subtitle { get; set; }
public CipherType Type { get; set; }
@@ -146,32 +166,56 @@ namespace Bit.App.Models.Page
public bool Fuzzy { get; set; }
}
public class Folder : List<Cipher>
public class Section<T> : List<T>
{
public Folder(Models.Folder folder)
public Section(List<T> groupItems, string name, bool doUpper = true)
{
Id = folder.Id;
Name = folder.Name?.Decrypt();
}
AddRange(groupItems);
public Folder(List<Cipher> ciphers)
{
AddRange(ciphers);
}
public string Id { get; set; }
public string Name { get; set; } = AppResources.FolderNone;
}
public class AutofillGrouping : List<AutofillCipher>
{
public AutofillGrouping(List<AutofillCipher> logins, string name)
{
AddRange(logins);
Name = name;
if(doUpper)
{
Name = name.ToUpperInvariant();
}
else
{
Name = name;
}
}
public string Name { get; set; }
}
public class Grouping
{
public Grouping(string name, int count)
{
Id = null;
Name = name;
Folder = true;
Count = count;
}
public Grouping(Folder folder, int count)
{
Id = folder.Id;
Name = folder.Name?.Decrypt();
Folder = true;
Count = count;
}
public Grouping(Collection collection, int count)
{
Id = collection.Id;
Name = collection.Name?.Decrypt(collection.OrganizationId);
Collection = true;
Count = count;
}
public string Id { get; set; }
public string Name { get; set; } = AppResources.FolderNone;
public int Count { get; set; }
public bool Folder { get; set; }
public bool Collection { get; set; }
}
}
}

View File

@@ -4,27 +4,28 @@ using Bit.App.Controls;
using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Abstractions;
namespace Bit.App.Pages
{
public class BaseLockPage : ExtendedContentPage
{
private readonly IDeviceActionService _deviceActionService;
public BaseLockPage()
: base(false, false)
{
UserDialogs = Resolver.Resolve<IUserDialogs>();
AuthService = Resolver.Resolve<IAuthService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
}
protected IUserDialogs UserDialogs { get; set; }
protected IAuthService AuthService { get; set; }
protected override bool OnBackButtonPressed()
{
if(Device.RuntimePlatform == Device.Android)
{
MessagingCenter.Send(Application.Current, "BackgroundApp");
}
_deviceActionService.Background();
return true;
}
@@ -34,8 +35,7 @@ namespace Bit.App.Pages
{
return;
}
MessagingCenter.Send(Application.Current, "Logout", (string)null);
AuthService.LogOut();
}
}
}

View File

@@ -15,6 +15,7 @@ namespace Bit.App.Pages
private readonly IFingerprint _fingerprint;
private readonly ISettings _settings;
private readonly IAppSettingsService _appSettings;
private readonly IDeviceInfoService _deviceInfoService;
private readonly bool _checkFingerprintImmediately;
private DateTime? _lastAction;
@@ -24,6 +25,7 @@ namespace Bit.App.Pages
_fingerprint = Resolver.Resolve<IFingerprint>();
_settings = Resolver.Resolve<ISettings>();
_appSettings = Resolver.Resolve<IAppSettingsService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
Init();
}
@@ -32,7 +34,7 @@ namespace Bit.App.Pages
{
var fingerprintIcon = new ExtendedButton
{
Image = "fingerprint.png",
Image = _deviceInfoService.HasFaceIdSupport ? "smile.png" : "fingerprint.png",
BackgroundColor = Color.Transparent,
Command = new Command(async () => await CheckFingerprintAsync()),
VerticalOptions = LayoutOptions.CenterAndExpand,
@@ -41,7 +43,8 @@ namespace Bit.App.Pages
var fingerprintButton = new ExtendedButton
{
Text = AppResources.UseFingerprintToUnlock,
Text = _deviceInfoService.HasFaceIdSupport ? AppResources.UseFaceIDToUnlock :
AppResources.UseFingerprintToUnlock,
Command = new Command(async () => await CheckFingerprintAsync()),
VerticalOptions = LayoutOptions.EndAndExpand,
Style = (Style)Application.Current.Resources["btn-primary"]
@@ -64,7 +67,7 @@ namespace Bit.App.Pages
Children = { fingerprintIcon, fingerprintButton, logoutButton }
};
Title = AppResources.VerifyFingerprint;
Title = _deviceInfoService.HasFaceIdSupport ? AppResources.VerifyFaceID : AppResources.VerifyFingerprint;
Content = stackLayout;
}
@@ -86,7 +89,8 @@ namespace Bit.App.Pages
}
_lastAction = DateTime.UtcNow;
var fingerprintRequest = new AuthenticationRequestConfiguration(AppResources.FingerprintDirection)
var fingerprintRequest = new AuthenticationRequestConfiguration(
_deviceInfoService.HasFaceIdSupport ? AppResources.FaceIDDirection : AppResources.FingerprintDirection)
{
AllowAlternativeAuthentication = true,
CancelTitle = AppResources.Cancel,
@@ -100,7 +104,7 @@ namespace Bit.App.Pages
}
else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
{
MessagingCenter.Send(Application.Current, "Logout", (string)null);
AuthService.LogOut();
}
}
}

View File

@@ -50,7 +50,7 @@ namespace Bit.App.Pages
NoFooter = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
PasswordCell
}

View File

@@ -76,7 +76,7 @@ namespace Bit.App.Pages
VerticalOptions = LayoutOptions.Start,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
EmailCell,
PasswordCell

View File

@@ -22,6 +22,7 @@ namespace Bit.App.Pages
private IUserDialogs _userDialogs;
private ISyncService _syncService;
private IDeviceInfoService _deviceInfoService;
private IDeviceActionService _deviceActionService;
private IGoogleAnalyticsService _googleAnalyticsService;
private ITwoFactorApiRepository _twoFactorApiRepository;
private IPushNotificationService _pushNotification;
@@ -45,6 +46,7 @@ namespace Bit.App.Pages
_providers = result.TwoFactorProviders;
_providerType = type ?? GetDefaultProvider();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_authService = Resolver.Resolve<IAuthService>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
_syncService = Resolver.Resolve<ISyncService>();
@@ -130,7 +132,7 @@ namespace Bit.App.Pages
TokenCell.Entry.ReturnType = ReturnType.Go;
var table = new TwoFactorTable(
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
TokenCell,
RememberCell
@@ -209,7 +211,7 @@ namespace Bit.App.Pages
});
var table = new TwoFactorTable(
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
RememberCell
});
@@ -240,7 +242,7 @@ namespace Bit.App.Pages
};
var table = new TwoFactorTable(
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
RememberCell
});
@@ -266,7 +268,7 @@ namespace Bit.App.Pages
InitEvents();
if(TokenCell == null && Device.RuntimePlatform == Device.Android)
{
MessagingCenter.Send(Application.Current, "DismissKeyboard");
_deviceActionService.DismissKeyboard();
}
}

View File

@@ -1,19 +1,20 @@
using System;
using Bit.App.Controls;
using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Abstractions;
namespace Bit.App.Pages
{
public class MainPage : ExtendedTabbedPage
{
public MainPage(string uri = null, bool myVault = false)
public MainPage(bool myVault = false)
{
TintColor = Color.FromHex("3c8dbc");
var settingsNavigation = new ExtendedNavigationPage(new SettingsPage());
var favoritesNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(true, uri));
var vaultNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(false, uri));
var favoritesNavigation = new ExtendedNavigationPage(new VaultListCiphersPage(favorites: true));
var vaultNavigation = new ExtendedNavigationPage(new VaultListGroupingsPage());
var toolsNavigation = new ExtendedNavigationPage(new ToolsPage());
favoritesNavigation.Icon = "star.png";
@@ -26,7 +27,7 @@ namespace Bit.App.Pages
Children.Add(toolsNavigation);
Children.Add(settingsNavigation);
if(myVault || uri != null)
if(myVault || Resolver.Resolve<IAppSettingsService>().DefaultPageVault)
{
SelectedItem = vaultNavigation;
}

View File

@@ -50,7 +50,7 @@ namespace Bit.App.Pages
VerticalOptions = LayoutOptions.Start,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
EmailCell
}

View File

@@ -67,7 +67,7 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
EmailCell,
PasswordCell
@@ -89,7 +89,7 @@ namespace Bit.App.Pages
NoHeader = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
ConfirmPasswordCell,
PasswordHintCell

View File

@@ -5,6 +5,7 @@ using Bit.App.Abstractions;
using XLabs.Ioc;
using Bit.App.Resources;
using FFImageLoading.Forms;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
@@ -59,7 +60,7 @@ namespace Bit.App.Pages
HasUnevenRows = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
CreditsCell
}

View File

@@ -8,6 +8,7 @@ using Plugin.Connectivity.Abstractions;
using Xamarin.Forms;
using XLabs.Ioc;
using System.Linq;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
@@ -42,7 +43,7 @@ namespace Bit.App.Pages
HasUnevenRows = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
NameCell
}

View File

@@ -7,6 +7,7 @@ using Plugin.Connectivity.Abstractions;
using Xamarin.Forms;
using XLabs.Ioc;
using System.Linq;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
@@ -55,11 +56,11 @@ namespace Bit.App.Pages
VerticalOptions = LayoutOptions.Start,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
NameCell
},
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
DeleteCell
}

View File

@@ -4,6 +4,7 @@ using Xamarin.Forms;
using Bit.App.Abstractions;
using XLabs.Ioc;
using Bit.App.Resources;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
@@ -38,7 +39,7 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
EmailCell
}
@@ -61,7 +62,7 @@ namespace Bit.App.Pages
NoHeader = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
WebsiteCell
}
@@ -84,7 +85,7 @@ namespace Bit.App.Pages
NoHeader = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
BugCell
}

View File

@@ -5,16 +5,17 @@ using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Controls;
using Plugin.Settings.Abstractions;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
public class SettingsFeaturesPage : ExtendedContentPage
public class SettingsOptionsPage : ExtendedContentPage
{
private readonly ISettings _settings;
private readonly IAppSettingsService _appSettings;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
public SettingsFeaturesPage()
public SettingsOptionsPage()
{
_settings = Resolver.Resolve<ISettings>();
_appSettings = Resolver.Resolve<IAppSettingsService>();
@@ -24,6 +25,8 @@ namespace Bit.App.Pages
}
private StackLayout StackLayout { get; set; }
private ExtendedSwitchCell DefaultPageVaultCell { get; set; }
private Label DefaultPageVaultLabel { get; set; }
private ExtendedSwitchCell CopyTotpCell { get; set; }
private Label CopyTotpLabel { get; set; }
private ExtendedSwitchCell AnalyticsCell { get; set; }
@@ -39,17 +42,34 @@ namespace Bit.App.Pages
private void Init()
{
DefaultPageVaultCell = new ExtendedSwitchCell
{
Text = AppResources.DefaultPageVault,
On = _appSettings.DefaultPageVault
};
var defaultPageVaultTable = new FormTableView(true)
{
Root = new TableRoot
{
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
DefaultPageVaultCell
}
}
};
WebsiteIconsCell = new ExtendedSwitchCell
{
Text = AppResources.DisableWebsiteIcons,
On = _appSettings.DisableWebsiteIcons
};
var websiteIconsTable = new FormTableView(true)
var websiteIconsTable = new FormTableView
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
WebsiteIconsCell
}
@@ -66,7 +86,7 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
CopyTotpCell
}
@@ -83,13 +103,18 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
AnalyticsCell
}
}
};
DefaultPageVaultLabel = new FormTableLabel(this)
{
Text = AppResources.DefaultPageVaultDescription
};
CopyTotpLabel = new FormTableLabel(this)
{
Text = AppResources.DisableAutoTotpCopyDescription
@@ -109,6 +134,7 @@ namespace Bit.App.Pages
{
Children =
{
defaultPageVaultTable, DefaultPageVaultLabel,
websiteIconsTable, WebsiteIconsLabel,
totpTable, CopyTotpLabel,
analyticsTable, AnalyticsLabel
@@ -128,7 +154,7 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(AppResources.AutofillService)
new TableSection(AppResources.AutofillAccessibilityService)
{
AutofillAlwaysCell
}
@@ -150,7 +176,7 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
AutofillPersistNotificationCell
}
@@ -172,7 +198,7 @@ namespace Bit.App.Pages
{
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
AutofillPasswordFieldCell
}
@@ -205,7 +231,7 @@ namespace Bit.App.Pages
ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close));
}
Title = AppResources.Features;
Title = AppResources.Options;
Content = scrollView;
}
@@ -213,6 +239,7 @@ namespace Bit.App.Pages
{
base.OnAppearing();
DefaultPageVaultCell.OnChanged += DefaultPageVaultCell_Changed;
AnalyticsCell.OnChanged += AnalyticsCell_Changed;
WebsiteIconsCell.OnChanged += WebsiteIconsCell_Changed;
CopyTotpCell.OnChanged += CopyTotpCell_OnChanged;
@@ -230,6 +257,7 @@ namespace Bit.App.Pages
{
base.OnDisappearing();
DefaultPageVaultCell.OnChanged -= DefaultPageVaultCell_Changed;
AnalyticsCell.OnChanged -= AnalyticsCell_Changed;
WebsiteIconsCell.OnChanged -= WebsiteIconsCell_Changed;
CopyTotpCell.OnChanged -= CopyTotpCell_OnChanged;
@@ -245,6 +273,7 @@ namespace Bit.App.Pages
private void Layout_LayoutChanged(object sender, EventArgs e)
{
DefaultPageVaultLabel.WidthRequest = StackLayout.Bounds.Width - DefaultPageVaultLabel.Bounds.Left * 2;
AnalyticsLabel.WidthRequest = StackLayout.Bounds.Width - AnalyticsLabel.Bounds.Left * 2;
WebsiteIconsLabel.WidthRequest = StackLayout.Bounds.Width - WebsiteIconsLabel.Bounds.Left * 2;
CopyTotpLabel.WidthRequest = StackLayout.Bounds.Width - CopyTotpLabel.Bounds.Left * 2;
@@ -266,6 +295,17 @@ namespace Bit.App.Pages
}
}
private void DefaultPageVaultCell_Changed(object sender, ToggledEventArgs e)
{
var cell = sender as ExtendedSwitchCell;
if(cell == null)
{
return;
}
_appSettings.DefaultPageVault = cell.On;
}
private void WebsiteIconsCell_Changed(object sender, ToggledEventArgs e)
{
var cell = sender as ExtendedSwitchCell;

View File

@@ -19,7 +19,9 @@ namespace Bit.App.Pages
private readonly IFingerprint _fingerprint;
private readonly IPushNotificationService _pushNotification;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly IDeviceActionService _deviceActionService;
private readonly IDeviceInfoService _deviceInfoService;
private readonly ILockService _lockService;
// TODO: Model binding context?
@@ -31,7 +33,9 @@ namespace Bit.App.Pages
_fingerprint = Resolver.Resolve<IFingerprint>();
_pushNotification = Resolver.Resolve<IPushNotificationService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_lockService = Resolver.Resolve<ILockService>();
Init();
}
@@ -49,7 +53,7 @@ namespace Bit.App.Pages
private ExtendedTextCell AboutCell { get; set; }
private ExtendedTextCell HelpCell { get; set; }
private ExtendedTextCell RateCell { get; set; }
private ExtendedTextCell FeaturesCell { get; set; }
private ExtendedTextCell OptionsCell { get; set; }
private LongDetailViewCell RateCellLong { get; set; }
private ExtendedTableView Table { get; set; }
@@ -89,8 +93,9 @@ namespace Bit.App.Pages
if((await _fingerprint.GetAvailabilityAsync()) == FingerprintAvailability.Available)
{
var fingerprintName = Helpers.OnPlatform(iOS: AppResources.TouchID, Android: AppResources.Fingerprint,
WinPhone: AppResources.Fingerprint);
var fingerprintName = Helpers.OnPlatform(
iOS: _deviceInfoService.HasFaceIdSupport ? AppResources.FaceID : AppResources.TouchID,
Android: AppResources.Fingerprint, Windows: AppResources.Fingerprint, WinPhone: AppResources.Fingerprint);
FingerprintCell = new ExtendedSwitchCell
{
Text = string.Format(AppResources.UnlockWith, fingerprintName),
@@ -141,15 +146,15 @@ namespace Bit.App.Pages
ShowDisclousure = true
};
FeaturesCell = new ExtendedTextCell
OptionsCell = new ExtendedTextCell
{
Text = AppResources.Features,
Text = AppResources.Options,
ShowDisclousure = true
};
var otherSection = new TableSection(AppResources.Other)
{
FeaturesCell,
OptionsCell,
AboutCell,
HelpCell
};
@@ -216,7 +221,7 @@ namespace Bit.App.Pages
LogOutCell.Tapped += LogOutCell_Tapped;
AboutCell.Tapped += AboutCell_Tapped;
HelpCell.Tapped += HelpCell_Tapped;
FeaturesCell.Tapped += FeaturesCell_Tapped;
OptionsCell.Tapped += OptionsCell_Tapped;
if(RateCellLong != null)
{
@@ -250,7 +255,7 @@ namespace Bit.App.Pages
LogOutCell.Tapped -= LogOutCell_Tapped;
AboutCell.Tapped -= AboutCell_Tapped;
HelpCell.Tapped -= HelpCell_Tapped;
FeaturesCell.Tapped -= FeaturesCell_Tapped;
OptionsCell.Tapped -= OptionsCell_Tapped;
if(RateCellLong != null)
{
@@ -327,22 +332,7 @@ namespace Bit.App.Pages
private void RateCell_Tapped(object sender, EventArgs e)
{
_googleAnalyticsService.TrackAppEvent("OpenedSetting", "RateApp");
if(Device.RuntimePlatform == Device.iOS)
{
if(_deviceInfoService.Version < 11)
{
Device.OpenUri(new Uri("itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews" +
"?id=1137397744&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software"));
}
else
{
Device.OpenUri(new Uri("itms-apps://itunes.apple.com/us/app/id1137397744?action=write-review"));
}
}
else if(Device.RuntimePlatform == Device.Android)
{
MessagingCenter.Send(Application.Current, "RateApp");
}
_deviceActionService.RateApp();
}
private void HelpCell_Tapped(object sender, EventArgs e)
@@ -353,7 +343,7 @@ namespace Bit.App.Pages
private void LockCell_Tapped(object sender, EventArgs e)
{
_googleAnalyticsService.TrackAppEvent("Locked");
MessagingCenter.Send(Application.Current, "Lock", true);
Device.BeginInvokeOnMainThread(async () => await _lockService.CheckLockAsync(true));
}
private async void LogOutCell_Tapped(object sender, EventArgs e)
@@ -363,7 +353,7 @@ namespace Bit.App.Pages
return;
}
MessagingCenter.Send(Application.Current, "Logout", (string)null);
_authService.LogOut();
}
private async void ChangeMasterPasswordCell_Tapped(object sender, EventArgs e)
@@ -444,9 +434,9 @@ namespace Bit.App.Pages
}
}
private void FeaturesCell_Tapped(object sender, EventArgs e)
private void OptionsCell_Tapped(object sender, EventArgs e)
{
Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsFeaturesPage()));
Navigation.PushModalAsync(new ExtendedNavigationPage(new SettingsOptionsPage()));
}
private void FoldersCell_Tapped(object sender, EventArgs e)

View File

@@ -0,0 +1,235 @@
using System;
using Bit.App.Controls;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Abstractions;
using Bit.App.Resources;
using FFImageLoading.Forms;
namespace Bit.App.Pages
{
public class ToolsAccessibilityServicePage : ExtendedContentPage
{
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly IAppInfoService _appInfoService;
private readonly IDeviceActionService _deviceActionService;
private bool _pageDisappeared = false;
public ToolsAccessibilityServicePage()
{
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_appInfoService = Resolver.Resolve<IAppInfoService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
Init();
}
public StackLayout EnabledStackLayout { get; set; }
public StackLayout DisabledStackLayout { get; set; }
public ScrollView ScrollView { get; set; }
public void Init()
{
var enabledFs = new FormattedString();
var statusSpan = new Span { Text = string.Concat(AppResources.Status, " ") };
enabledFs.Spans.Add(statusSpan);
enabledFs.Spans.Add(new Span
{
Text = AppResources.Enabled,
ForegroundColor = Color.Green,
FontAttributes = FontAttributes.Bold,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label))
});
var statusEnabledLabel = new Label
{
FormattedText = enabledFs,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
TextColor = Color.Black
};
var disabledFs = new FormattedString();
disabledFs.Spans.Add(statusSpan);
disabledFs.Spans.Add(new Span
{
Text = AppResources.Disabled,
ForegroundColor = Color.FromHex("c62929"),
FontAttributes = FontAttributes.Bold,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label))
});
var statusDisabledLabel = new Label
{
FormattedText = disabledFs,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
TextColor = Color.Black
};
var step1Label = new Label
{
Text = AppResources.BitwardenAutofillServiceStep1,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
TextColor = Color.Black
};
var step1Image = new CachedImage
{
Source = "accessibility_step1",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 98
};
var step2Label = new Label
{
Text = AppResources.BitwardenAutofillServiceStep2,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
TextColor = Color.Black
};
var step2Image = new CachedImage
{
Source = "accessibility_step2",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 67
};
var stepsStackLayout = new StackLayout
{
Children = { statusDisabledLabel, step1Image, step1Label, step2Image, step2Label },
Orientation = StackOrientation.Vertical,
Spacing = 10,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
var notificationsLabel = new Label
{
Text = AppResources.BitwardenAutofillServiceNotification,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
TextColor = Color.Black
};
var tapNotificationImage = new CachedImage
{
Source = "accessibility_notification.png",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 74
};
var tapNotificationIcon = new CachedImage
{
Source = "accessibility_notification_icon.png",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 54
};
var notificationsStackLayout = new StackLayout
{
Children = { statusEnabledLabel, tapNotificationIcon, tapNotificationImage, notificationsLabel },
Orientation = StackOrientation.Vertical,
Spacing = 10,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
DisabledStackLayout = new StackLayout
{
Children = { BuildServiceLabel(), stepsStackLayout, BuildGoButton() },
Orientation = StackOrientation.Vertical,
Spacing = 20,
Padding = new Thickness(20, 30),
VerticalOptions = LayoutOptions.FillAndExpand
};
EnabledStackLayout = new StackLayout
{
Children = { BuildServiceLabel(), notificationsStackLayout, BuildGoButton() },
Orientation = StackOrientation.Vertical,
Spacing = 20,
Padding = new Thickness(20, 30),
VerticalOptions = LayoutOptions.FillAndExpand
};
ScrollView = new ScrollView { Content = DisabledStackLayout };
Title = AppResources.AutofillAccessibilityService;
Content = ScrollView;
}
protected override void OnAppearing()
{
_pageDisappeared = false;
UpdateEnabled();
Device.StartTimer(new TimeSpan(0, 0, 3), () =>
{
System.Diagnostics.Debug.WriteLine("Check timer on accessibility");
if(_pageDisappeared)
{
return false;
}
UpdateEnabled();
return true;
});
base.OnAppearing();
}
protected override void OnDisappearing()
{
_pageDisappeared = true;
base.OnDisappearing();
}
private void UpdateEnabled()
{
ScrollView.Content = _appInfoService.AutofillAccessibilityServiceEnabled ?
EnabledStackLayout : DisabledStackLayout;
}
private Label BuildServiceLabel()
{
return new Label
{
Text = AppResources.AutofillAccessibilityDescription,
VerticalOptions = LayoutOptions.Start,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label))
};
}
private ExtendedButton BuildGoButton()
{
return new ExtendedButton
{
Text = AppResources.BitwardenAutofillServiceOpenAccessibilitySettings,
Command = new Command(() =>
{
_googleAnalyticsService.TrackAppEvent("OpenAccessibilitySettings");
_deviceActionService.OpenAccessibilitySettings();
}),
VerticalOptions = LayoutOptions.End,
HorizontalOptions = LayoutOptions.Fill,
Style = (Style)Application.Current.Resources["btn-primary"]
};
}
}
}

View File

@@ -12,12 +12,14 @@ namespace Bit.App.Pages
{
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly IAppInfoService _appInfoService;
private readonly IDeviceActionService _deviceActionService;
private bool _pageDisappeared = false;
public ToolsAutofillServicePage()
{
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_appInfoService = Resolver.Resolve<IAppInfoService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
Init();
}
@@ -45,7 +47,8 @@ namespace Bit.App.Pages
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
TextColor = Color.Black
TextColor = Color.Black,
VerticalOptions = LayoutOptions.CenterAndExpand
};
var disabledFs = new FormattedString();
@@ -67,90 +70,40 @@ namespace Bit.App.Pages
TextColor = Color.Black
};
var step1Label = new Label
var enableImage = new CachedImage
{
Text = AppResources.BitwardenAutofillServiceStep1,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
TextColor = Color.Black
};
var step1Image = new CachedImage
{
Source = "accessibility_step1",
Source = "autofill_enable.png",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 98
};
var step2Label = new Label
{
Text = AppResources.BitwardenAutofillServiceStep2,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
TextColor = Color.Black
};
var step2Image = new CachedImage
{
Source = "accessibility_step2",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 67
};
var stepsStackLayout = new StackLayout
{
Children = { statusDisabledLabel, step1Image, step1Label, step2Image, step2Label },
Orientation = StackOrientation.Vertical,
Spacing = 10,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
var notificationsLabel = new Label
{
Text = AppResources.BitwardenAutofillServiceNotification,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
TextColor = Color.Black
};
var tapNotificationImage = new CachedImage
{
Source = "accessibility_notification.png",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 74
HeightRequest = 118
};
var tapNotificationIcon = new CachedImage
var useImage = new CachedImage
{
Source = "accessibility_notification_icon.png",
Source = "autofill_use.png",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 20, 0, 0),
WidthRequest = 300,
HeightRequest = 54
};
var notificationsStackLayout = new StackLayout
{
Children = { statusEnabledLabel, tapNotificationIcon, tapNotificationImage, notificationsLabel },
Orientation = StackOrientation.Vertical,
Spacing = 10,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
WidthRequest = 300,
HeightRequest = 128
};
var goButton = new ExtendedButton
{
Text = AppResources.BitwardenAutofillServiceOpenAutofillSettings,
Command = new Command(() =>
{
_googleAnalyticsService.TrackAppEvent("OpenAutofillSettings");
_deviceActionService.OpenAutofillSettings();
}),
VerticalOptions = LayoutOptions.End,
HorizontalOptions = LayoutOptions.Fill,
Style = (Style)Application.Current.Resources["btn-primary"]
};
DisabledStackLayout = new StackLayout
{
Children = { BuildServiceLabel(), stepsStackLayout, BuildGoButton() },
Children = { BuildServiceLabel(), statusDisabledLabel, enableImage, goButton, BuildAccessibilityButton() },
Orientation = StackOrientation.Vertical,
Spacing = 20,
Padding = new Thickness(20, 30),
@@ -159,7 +112,7 @@ namespace Bit.App.Pages
EnabledStackLayout = new StackLayout
{
Children = { BuildServiceLabel(), notificationsStackLayout, BuildGoButton() },
Children = { BuildServiceLabel(), statusEnabledLabel, useImage, BuildAccessibilityButton() },
Orientation = StackOrientation.Vertical,
Spacing = 20,
Padding = new Thickness(20, 30),
@@ -167,19 +120,6 @@ namespace Bit.App.Pages
};
ScrollView = new ScrollView { Content = DisabledStackLayout };
UpdateEnabled();
Device.StartTimer(new TimeSpan(0, 0, 3), () =>
{
if(_pageDisappeared)
{
return false;
}
UpdateEnabled();
return true;
});
Title = AppResources.AutofillService;
Content = ScrollView;
}
@@ -187,6 +127,19 @@ namespace Bit.App.Pages
protected override void OnAppearing()
{
_pageDisappeared = false;
UpdateEnabled();
Device.StartTimer(new TimeSpan(0, 0, 2), () =>
{
System.Diagnostics.Debug.WriteLine("Check timer on autofill");
if(_pageDisappeared)
{
return false;
}
UpdateEnabled();
return true;
});
base.OnAppearing();
}
@@ -205,7 +158,7 @@ namespace Bit.App.Pages
{
return new Label
{
Text = AppResources.AutofillDescription,
Text = AppResources.AutofillServiceDescription,
VerticalOptions = LayoutOptions.Start,
HorizontalTextAlignment = TextAlignment.Center,
LineBreakMode = LineBreakMode.WordWrap,
@@ -213,20 +166,20 @@ namespace Bit.App.Pages
};
}
private ExtendedButton BuildGoButton()
private ExtendedButton BuildAccessibilityButton()
{
return new ExtendedButton
{
Text = AppResources.BitwardenAutofillServiceOpenSettings,
Command = new Command(() =>
Text = AppResources.AutofillAccessibilityService,
Command = new Command(async () =>
{
_googleAnalyticsService.TrackAppEvent("OpenAccessibilitySettings");
MessagingCenter.Send(Application.Current, "Accessibility");
await Navigation.PushAsync(new ToolsAccessibilityServicePage());
}),
VerticalOptions = LayoutOptions.End,
HorizontalOptions = LayoutOptions.Fill,
Style = (Style)Application.Current.Resources["btn-primary"],
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Button))
Style = (Style)Application.Current.Resources["btn-primaryAccent"],
Uppercase = false,
BackgroundColor = Color.Transparent
};
}
}

View File

@@ -7,6 +7,7 @@ using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using FFImageLoading.Forms;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
@@ -14,11 +15,13 @@ namespace Bit.App.Pages
{
private readonly IUserDialogs _userDialogs;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly IDeviceInfoService _deviceInfoService;
public ToolsPage()
{
_userDialogs = Resolver.Resolve<IUserDialogs>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
Init();
}
@@ -38,18 +41,20 @@ namespace Bit.App.Pages
ShareCell = new ToolsViewCell(AppResources.ShareVault, AppResources.ShareVaultDescription, "share_tools.png");
ImportCell = new ToolsViewCell(AppResources.ImportItems, AppResources.ImportItemsDescription, "cloudup.png");
var section = new TableSection(" ") { GeneratorCell };
var section = new TableSection(Helpers.GetEmptyTableSectionTitle()) { GeneratorCell };
if(Device.RuntimePlatform == Device.iOS)
{
ExtensionCell = new ToolsViewCell(AppResources.BitwardenAppExtension,
AppResources.BitwardenAppExtensionDescription, "upload");
AppResources.BitwardenAppExtensionDescription, "upload.png");
section.Add(ExtensionCell);
}
else
if(Device.RuntimePlatform == Device.Android)
{
AutofillCell = new ToolsViewCell(AppResources.BitwardenAutofillService,
AppResources.BitwardenAutofillServiceDescription, "upload.png");
var desc = _deviceInfoService.AutofillServiceSupported ?
AppResources.BitwardenAutofillServiceDescription :
AppResources.BitwardenAutofillAccessibilityServiceDescription;
AutofillCell = new ToolsViewCell(AppResources.BitwardenAutofillService, desc, "upload.png");
section.Add(AutofillCell);
}
@@ -115,7 +120,14 @@ namespace Bit.App.Pages
private void AutofillCell_Tapped(object sender, EventArgs e)
{
Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsAutofillServicePage()));
if(_deviceInfoService.AutofillServiceSupported)
{
Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsAutofillServicePage()));
}
else
{
Navigation.PushModalAsync(new ExtendedNavigationPage(new ToolsAccessibilityServicePage()));
}
}
private void ExtensionCell_Tapped(object sender, EventArgs e)

View File

@@ -74,7 +74,7 @@ namespace Bit.App.Pages
NoHeader = true,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
RegenerateCell,
CopyCell

View File

@@ -8,6 +8,7 @@ using Plugin.Connectivity.Abstractions;
using Plugin.Settings.Abstractions;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Utilities;
namespace Bit.App.Pages
{
@@ -77,19 +78,19 @@ namespace Bit.App.Pages
EnableSelection = false,
Root = new TableRoot
{
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
UppercaseCell,
LowercaseCell,
NumbersCell,
SpecialCell
},
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
NumbersMinCell,
SpecialMinCell
},
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
AvoidAmbiguousCell
}

View File

@@ -12,6 +12,7 @@ using XLabs.Ioc;
using Plugin.Settings.Abstractions;
using Bit.App.Utilities;
using Bit.App.Enums;
using Bit.App.Models.Page;
namespace Bit.App.Pages
{
@@ -28,14 +29,42 @@ namespace Bit.App.Pages
private readonly ISettings _settings;
private readonly IAppInfoService _appInfoService;
private readonly IDeviceInfoService _deviceInfo;
private readonly IDeviceActionService _deviceActionService;
private readonly string _defaultFolderId;
private readonly string _defaultUri;
private readonly string _defaultName;
private readonly string _defaultUsername;
private readonly string _defaultPassword;
private readonly string _defaultCardName;
private readonly string _defaultCardNumber;
private readonly int? _defaultCardExpMonth;
private readonly string _defaultCardExpYear;
private readonly string _defaultCardCode;
private readonly bool _fromAutofill;
private readonly bool _fromAutofillFramework;
private DateTime? _lastAction;
public VaultAddCipherPage(CipherType type, string defaultUri = null,
string defaultName = null, bool fromAutofill = false)
public VaultAddCipherPage(AppOptions options)
: this(options.SaveType.Value, options.Uri, options.SaveName, options.FromAutofillFramework, false)
{
_fromAutofillFramework = options.FromAutofillFramework;
_defaultUsername = options.SaveUsername;
_defaultPassword = options.SavePassword;
_defaultCardCode = options.SaveCardCode;
if(int.TryParse(options.SaveCardExpMonth, out int month) && month <= 12 && month >= 1)
{
_defaultCardExpMonth = month;
}
_defaultCardExpYear = options.SaveCardExpYear;
_defaultCardName = options.SaveCardName;
_defaultCardNumber = options.SaveCardNumber;
Init();
}
public VaultAddCipherPage(CipherType type, string defaultUri = null, string defaultName = null,
bool fromAutofill = false, bool doInit = true, string defaultFolderId = null)
{
_defaultFolderId = defaultFolderId;
_type = type;
_defaultUri = defaultUri;
_defaultName = defaultName;
@@ -49,8 +78,12 @@ namespace Bit.App.Pages
_settings = Resolver.Resolve<ISettings>();
_appInfoService = Resolver.Resolve<IAppInfoService>();
_deviceInfo = Resolver.Resolve<IDeviceInfoService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
Init();
if(doInit)
{
Init();
}
}
public List<Folder> Folders { get; set; }
@@ -115,11 +148,19 @@ namespace Bit.App.Pages
var folderOptions = new List<string> { AppResources.FolderNone };
Folders = _folderService.GetAllAsync().GetAwaiter().GetResult()
.OrderBy(f => f.Name?.Decrypt()).ToList();
var selectedIndex = 0;
var i = 1;
foreach(var folder in Folders)
{
if(folder.Id == _defaultFolderId)
{
selectedIndex = i;
}
folderOptions.Add(folder.Name.Decrypt());
i++;
}
FolderCell = new FormPickerCell(AppResources.Folder, folderOptions.ToArray());
FolderCell.Picker.SelectedIndex = selectedIndex;
// Favorite
FavoriteCell = new ExtendedSwitchCell { Text = AppResources.Favorite };
@@ -201,7 +242,7 @@ namespace Bit.App.Pages
DisplayAlert(AppResources.BitwardenAppExtension, AppResources.BitwardenAppExtensionAlert,
AppResources.Ok);
}
else if(Device.RuntimePlatform == Device.Android && !_appInfoService.AutofillServiceEnabled)
else if(Device.RuntimePlatform == Device.Android && !_appInfoService.AutofillAccessibilityServiceEnabled)
{
DisplayAlert(AppResources.BitwardenAutofillService, AppResources.BitwardenAutofillServiceAlert,
AppResources.Ok);
@@ -266,10 +307,21 @@ namespace Bit.App.Pages
}
}
protected override bool OnBackButtonPressed()
{
if(_fromAutofillFramework)
{
Application.Current.MainPage = new MainPage(true);
return true;
}
return base.OnBackButtonPressed();
}
private void PasswordButton_Clicked(object sender, EventArgs e)
{
LoginPasswordCell.Entry.InvokeToggleIsPassword();
LoginPasswordCell.Button.Image =
LoginPasswordCell.Button.Image =
"eye" + (!LoginPasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty) + ".png";
}
@@ -319,7 +371,7 @@ namespace Bit.App.Pages
NameCell
};
MiddleSection = new TableSection(" ")
MiddleSection = new TableSection(Helpers.GetEmptyTableSectionTitle())
{
FolderCell,
FavoriteCell
@@ -335,7 +387,7 @@ namespace Bit.App.Pages
}
LoginTotpCell.Entry.DisableAutocapitalize = true;
LoginTotpCell.Entry.Autocorrect = false;
LoginTotpCell.Entry.FontFamily =
LoginTotpCell.Entry.FontFamily =
Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", WinPhone: "Courier");
LoginPasswordCell = new FormEntryCell(AppResources.Password, isPassword: true, nextElement: LoginTotpCell.Entry,
@@ -343,8 +395,12 @@ namespace Bit.App.Pages
LoginPasswordCell.Button.Image = "eye.png";
LoginPasswordCell.Entry.DisableAutocapitalize = true;
LoginPasswordCell.Entry.Autocorrect = false;
LoginPasswordCell.Entry.FontFamily =
LoginPasswordCell.Entry.FontFamily =
Helpers.OnPlatform(iOS: "Menlo-Regular", Android: "monospace", WinPhone: "Courier");
if(!string.IsNullOrWhiteSpace(_defaultPassword))
{
LoginPasswordCell.Entry.Text = _defaultPassword;
}
LoginGenerateCell = new ExtendedTextCell
{
@@ -355,6 +411,10 @@ namespace Bit.App.Pages
LoginUsernameCell = new FormEntryCell(AppResources.Username, nextElement: LoginPasswordCell.Entry);
LoginUsernameCell.Entry.DisableAutocapitalize = true;
LoginUsernameCell.Entry.Autocorrect = false;
if(!string.IsNullOrWhiteSpace(_defaultUsername))
{
LoginUsernameCell.Entry.Text = _defaultUsername;
}
LoginUriCell = new FormEntryCell(AppResources.URI, Keyboard.Url, nextElement: LoginUsernameCell.Entry);
if(!string.IsNullOrWhiteSpace(_defaultUri))
@@ -375,19 +435,39 @@ namespace Bit.App.Pages
{
CardCodeCell = new FormEntryCell(AppResources.SecurityCode, Keyboard.Numeric,
nextElement: NotesCell.Editor);
if(!string.IsNullOrWhiteSpace(_defaultCardCode))
{
CardCodeCell.Entry.Text = _defaultCardCode;
}
CardExpYearCell = new FormEntryCell(AppResources.ExpirationYear, Keyboard.Numeric,
nextElement: CardCodeCell.Entry);
if(!string.IsNullOrWhiteSpace(_defaultCardExpYear))
{
CardExpYearCell.Entry.Text = _defaultCardExpYear;
}
CardExpMonthCell = new FormPickerCell(AppResources.ExpirationMonth, new string[] {
"--", AppResources.January, AppResources.February, AppResources.March, AppResources.April,
AppResources.May, AppResources.June, AppResources.July, AppResources.August, AppResources.September,
AppResources.October, AppResources.November, AppResources.December
});
if(_defaultCardExpMonth.HasValue)
{
CardExpMonthCell.Picker.SelectedIndex = _defaultCardExpMonth.Value;
}
CardBrandCell = new FormPickerCell(AppResources.Brand, new string[] {
"--", "Visa", "Mastercard", "American Express", "Discover", "Diners Club",
"JCB", "Maestro", "UnionPay", AppResources.Other
});
CardNumberCell = new FormEntryCell(AppResources.Number, Keyboard.Numeric);
if(!string.IsNullOrWhiteSpace(_defaultCardNumber))
{
CardNumberCell.Entry.Text = _defaultCardNumber;
}
CardNameCell = new FormEntryCell(AppResources.CardholderName, nextElement: CardNumberCell.Entry);
if(!string.IsNullOrWhiteSpace(_defaultCardName))
{
CardNameCell.Entry.Text = _defaultCardName;
}
NameCell.NextElement = CardNameCell.Entry;
// Build sections
@@ -679,7 +759,16 @@ namespace Bit.App.Pages
{
_googleAnalyticsService.TrackAppEvent("CreatedCipher");
}
await Navigation.PopForDeviceAsync();
if(_fromAutofillFramework)
{
// close and go back to app
_deviceActionService.CloseAutofill();
}
else
{
await Navigation.PopForDeviceAsync();
}
}
else if(saveTask.Errors.Count() > 0)
{

View File

@@ -13,6 +13,7 @@ using System.Threading;
using Bit.App.Models;
using System.Collections.Generic;
using Bit.App.Enums;
using static Bit.App.Models.Page.VaultListPageModel;
namespace Bit.App.Pages
{
@@ -20,23 +21,23 @@ namespace Bit.App.Pages
{
private readonly ICipherService _cipherService;
private readonly IDeviceInfoService _deviceInfoService;
private readonly IDeviceActionService _clipboardService;
private readonly IDeviceActionService _deviceActionService;
private readonly ISettingsService _settingsService;
private readonly IAppSettingsService _appSettingsService;
private CancellationTokenSource _filterResultsCancellationTokenSource;
private readonly string _name;
private readonly AppOptions _appOptions;
public VaultAutofillListCiphersPage(string uriString)
public VaultAutofillListCiphersPage(AppOptions appOptions)
: base(true)
{
Uri = uriString;
Uri uri;
if(uriString?.StartsWith(Constants.AndroidAppProtocol) ?? false)
_appOptions = appOptions;
Uri = appOptions.Uri;
if(Uri.StartsWith(Constants.AndroidAppProtocol))
{
_name = uriString.Substring(Constants.AndroidAppProtocol.Length);
_name = Uri.Substring(Constants.AndroidAppProtocol.Length);
}
else if(!System.Uri.TryCreate(uriString, UriKind.Absolute, out uri) ||
else if(!System.Uri.TryCreate(Uri, UriKind.Absolute, out Uri uri) ||
!DomainName.TryParseBaseDomain(uri.Host, out _name))
{
_name = "--";
@@ -44,7 +45,7 @@ namespace Bit.App.Pages
_cipherService = Resolver.Resolve<ICipherService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_clipboardService = Resolver.Resolve<IDeviceActionService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_settingsService = Resolver.Resolve<ISettingsService>();
UserDialogs = Resolver.Resolve<IUserDialogs>();
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
@@ -53,8 +54,8 @@ namespace Bit.App.Pages
Init();
}
public ExtendedObservableCollection<VaultListPageModel.AutofillGrouping> PresentationCiphersGroup { get; private set; }
= new ExtendedObservableCollection<VaultListPageModel.AutofillGrouping>();
public ExtendedObservableCollection<Section<AutofillCipher>> PresentationCiphersGroup { get; private set; }
= new ExtendedObservableCollection<Section<AutofillCipher>>();
public StackLayout NoDataStackLayout { get; set; }
public ListView ListView { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
@@ -99,9 +100,10 @@ namespace Bit.App.Pages
IsGroupingEnabled = true,
ItemsSource = PresentationCiphersGroup,
HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new HeaderViewCell()),
GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(
nameof(Section<AutofillCipher>.Name))),
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Cipher l) => MoreClickedAsync(l)))
(VaultListPageModel.Cipher c) => Helpers.CipherMoreClickedAsync(this, c, true)))
};
if(Device.RuntimePlatform == Device.iOS)
@@ -141,7 +143,7 @@ namespace Bit.App.Pages
protected override bool OnBackButtonPressed()
{
GoogleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Cipher)null);
_deviceActionService.CloseAutofill();
return true;
}
@@ -164,28 +166,44 @@ namespace Bit.App.Pages
Task.Run(async () =>
{
var autofillGroupings = new List<VaultListPageModel.AutofillGrouping>();
var autofillGroupings = new List<Section<AutofillCipher>>();
var ciphers = await _cipherService.GetAllAsync(Uri);
var normalLogins = ciphers?.Item1.Select(l => new VaultListPageModel.AutofillCipher(
l, _appSettingsService, false))
.OrderBy(s => s.Name)
.ThenBy(s => s.Subtitle)
.ToList();
if(normalLogins?.Any() ?? false)
if(_appOptions.FillType.HasValue && _appOptions.FillType.Value != CipherType.Login)
{
autofillGroupings.Add(new VaultListPageModel.AutofillGrouping(normalLogins, AppResources.MatchingItems));
var others = ciphers?.Item3.Where(c => c.Type == _appOptions.FillType.Value)
.Select(c => new AutofillCipher(c, _appSettingsService, false))
.OrderBy(s => s.Name)
.ThenBy(s => s.Subtitle)
.ToList();
if(others?.Any() ?? false)
{
autofillGroupings.Add(new Section<AutofillCipher>(others, AppResources.Items, false));
}
}
var fuzzyLogins = ciphers?.Item2.Select(l => new VaultListPageModel.AutofillCipher(
l, _appSettingsService, true))
.OrderBy(s => s.Name)
.ThenBy(s => s.LoginUsername)
.ToList();
if(fuzzyLogins?.Any() ?? false)
else
{
autofillGroupings.Add(new VaultListPageModel.AutofillGrouping(fuzzyLogins,
AppResources.PossibleMatchingItems));
var normalLogins = ciphers?.Item1
.Select(l => new AutofillCipher(l, _appSettingsService, false))
.OrderBy(s => s.Name)
.ThenBy(s => s.Subtitle)
.ToList();
if(normalLogins?.Any() ?? false)
{
autofillGroupings.Add(new Section<AutofillCipher>(normalLogins,
AppResources.MatchingItems, false));
}
var fuzzyLogins = ciphers?.Item2
.Select(l => new AutofillCipher(l, _appSettingsService, true))
.OrderBy(s => s.Name)
.ThenBy(s => s.Subtitle)
.ToList();
if(fuzzyLogins?.Any() ?? false)
{
autofillGroupings.Add(new Section<AutofillCipher>(fuzzyLogins,
AppResources.PossibleMatchingItems, false));
}
}
Device.BeginInvokeOnMainThread(() =>
@@ -204,7 +222,7 @@ namespace Bit.App.Pages
private async void CipherSelected(object sender, SelectedItemChangedEventArgs e)
{
var cipher = e.SelectedItem as VaultListPageModel.AutofillCipher;
var cipher = e.SelectedItem as AutofillCipher;
if(cipher == null)
{
return;
@@ -212,7 +230,7 @@ namespace Bit.App.Pages
if(_deviceInfoService.Version < 21)
{
MoreClickedAsync(cipher);
Helpers.CipherMoreClickedAsync(this, cipher, true);
}
else
{
@@ -227,7 +245,7 @@ namespace Bit.App.Pages
if(doAutofill)
{
GoogleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", cipher as VaultListPageModel.Cipher);
_deviceActionService.Autofill(cipher);
}
}
@@ -236,71 +254,15 @@ namespace Bit.App.Pages
private async void AddCipherAsync()
{
var page = new VaultAddCipherPage(CipherType.Login, Uri, _name, true);
await Navigation.PushForDeviceAsync(page);
}
private async void MoreClickedAsync(VaultListPageModel.Cipher cipher)
{
var buttons = new List<string> { AppResources.View, AppResources.Edit };
if(cipher.Type == CipherType.Login)
if(_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
{
if(!string.IsNullOrWhiteSpace(cipher.LoginPassword.Value))
{
buttons.Add(AppResources.CopyPassword);
}
if(!string.IsNullOrWhiteSpace(cipher.LoginUsername))
{
buttons.Add(AppResources.CopyUsername);
}
}
else if(cipher.Type == CipherType.Card)
{
if(!string.IsNullOrWhiteSpace(cipher.CardNumber))
{
buttons.Add(AppResources.CopyNumber);
}
if(!string.IsNullOrWhiteSpace(cipher.CardCode.Value))
{
buttons.Add(AppResources.CopySecurityCode);
}
var pageForOther = new VaultAddCipherPage(_appOptions.FillType.Value, null, null, true);
await Navigation.PushForDeviceAsync(pageForOther);
return;
}
var selection = await DisplayActionSheet(cipher.Name, AppResources.Cancel, null, buttons.ToArray());
if(selection == AppResources.View)
{
var page = new VaultViewCipherPage(cipher.Type, cipher.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.Edit)
{
var page = new VaultEditCipherPage(cipher.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.CopyPassword)
{
Copy(cipher.LoginPassword.Value, AppResources.Password);
}
else if(selection == AppResources.CopyUsername)
{
Copy(cipher.LoginUsername, AppResources.Username);
}
else if(selection == AppResources.CopyNumber)
{
Copy(cipher.CardNumber, AppResources.Number);
}
else if(selection == AppResources.CopySecurityCode)
{
Copy(cipher.CardCode.Value, AppResources.SecurityCode);
}
}
private void Copy(string copyText, string alertLabel)
{
_clipboardService.CopyToClipboard(copyText);
UserDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
var pageForLogin = new VaultAddCipherPage(CipherType.Login, Uri, _name, true);
await Navigation.PushForDeviceAsync(pageForLogin);
}
private class AddCipherToolBarItem : ExtendedToolbarItem
@@ -331,34 +293,10 @@ namespace Bit.App.Pages
{
_page.GoogleAnalyticsService.TrackExtensionEvent("CloseToSearch",
_page.Uri.StartsWith("http") ? "Website" : "App");
Application.Current.MainPage = new MainPage(_page.Uri);
Application.Current.MainPage = new ExtendedNavigationPage(new VaultListCiphersPage(uri: _page.Uri));
_page.UserDialogs.Toast(string.Format(AppResources.BitwardenAutofillServiceSearch, _page._name),
TimeSpan.FromSeconds(10));
}
}
private class HeaderViewCell : ExtendedViewCell
{
public HeaderViewCell()
{
var label = new Label
{
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"],
VerticalTextAlignment = TextAlignment.Center
};
label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.AutofillGrouping.Name));
var grid = new ContentView
{
Padding = new Thickness(16, 8, 0, 8),
Content = label
};
View = grid;
BackgroundColor = Color.FromHex("efeff4");
}
}
}
}

View File

@@ -43,7 +43,7 @@ namespace Bit.App.Pages
private void Init()
{
FieldsSection = new TableSection(" ");
FieldsSection = new TableSection(Helpers.GetEmptyTableSectionTitle());
Table = new ExtendedTableView
{

View File

@@ -165,7 +165,7 @@ namespace Bit.App.Pages
NameCell
};
MiddleSection = new TableSection(" ")
MiddleSection = new TableSection(Helpers.GetEmptyTableSectionTitle())
{
FolderCell,
FavoriteCell,
@@ -264,12 +264,12 @@ namespace Bit.App.Pages
option = "Other";
}
i++;
if(option == brand)
{
CardBrandCell.Picker.SelectedIndex = i;
break;
}
i++;
}
}
@@ -415,7 +415,7 @@ namespace Bit.App.Pages
{
NotesCell
},
new TableSection(" ")
new TableSection(Helpers.GetEmptyTableSectionTitle())
{
DeleteCell
}

View File

@@ -1,98 +1,89 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models.Page;
using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Utilities;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Collections.Generic;
using System.Threading;
using FFImageLoading.Forms;
using Bit.App.Enums;
using static Bit.App.Models.Page.VaultListPageModel;
using System.Collections.Generic;
namespace Bit.App.Pages
{
public class VaultListCiphersPage : ExtendedContentPage
{
private readonly IFolderService _folderService;
private readonly ICipherService _cipherService;
private readonly IUserDialogs _userDialogs;
private readonly IConnectivity _connectivity;
private readonly IDeviceActionService _clipboardService;
private readonly ISyncService _syncService;
private readonly IPushNotificationService _pushNotification;
private readonly IDeviceInfoService _deviceInfoService;
private readonly ISettings _settings;
private readonly IAppSettingsService _appSettingsService;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly bool _favorites;
private readonly IDeviceActionService _deviceActionService;
private CancellationTokenSource _filterResultsCancellationTokenSource;
private readonly bool _favorites = false;
private readonly bool _folder = false;
private readonly string _folderId = null;
private readonly string _collectionId = null;
private readonly string _groupingName = null;
private readonly string _uri = null;
public VaultListCiphersPage(bool favorites, string uri = null)
public VaultListCiphersPage(bool folder = false, string folderId = null,
string collectionId = null, string groupingName = null, bool favorites = false, string uri = null)
: base(true)
{
_folder = folder;
_folderId = folderId;
_collectionId = collectionId;
_favorites = favorites;
_folderService = Resolver.Resolve<IFolderService>();
_groupingName = groupingName;
_uri = uri;
_cipherService = Resolver.Resolve<ICipherService>();
_connectivity = Resolver.Resolve<IConnectivity>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
_clipboardService = Resolver.Resolve<IDeviceActionService>();
_syncService = Resolver.Resolve<ISyncService>();
_pushNotification = Resolver.Resolve<IPushNotificationService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_settings = Resolver.Resolve<ISettings>();
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
var cryptoService = Resolver.Resolve<ICryptoService>();
Uri = uri;
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
Init();
}
public ExtendedObservableCollection<VaultListPageModel.Folder> PresentationFolders { get; private set; }
= new ExtendedObservableCollection<VaultListPageModel.Folder>();
public ExtendedObservableCollection<Section<Cipher>> PresentationSections { get; private set; }
= new ExtendedObservableCollection<Section<Cipher>>();
public Cipher[] Ciphers { get; set; } = new Cipher[] { };
public ListView ListView { get; set; }
public VaultListPageModel.Cipher[] Ciphers { get; set; } = new VaultListPageModel.Cipher[] { };
public VaultListPageModel.Folder[] Folders { get; set; } = new VaultListPageModel.Folder[] { };
public SearchBar Search { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
public StackLayout NoDataStackLayout { get; set; }
public StackLayout ResultsStackLayout { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
private AddCipherToolBarItem AddCipherItem { get; set; }
public string Uri { get; set; }
private void Init()
{
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted", (sender, success) =>
if(!string.IsNullOrWhiteSpace(_uri) || _folder || !string.IsNullOrWhiteSpace(_folderId))
{
if(success)
{
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
});
if(!_favorites)
{
AddCipherItem = new AddCipherToolBarItem(this);
AddCipherItem = new AddCipherToolBarItem(this, _folderId);
ToolbarItems.Add(AddCipherItem);
}
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{
IsGroupingEnabled = true,
ItemsSource = PresentationFolders,
ItemsSource = PresentationSections,
HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new VaultListHeaderViewCell(this)),
GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(nameof(Section<Cipher>.Name),
nameof(Section<Cipher>.Count))),
GroupShortNameBinding = new Binding(nameof(Section<Cipher>.Name)),
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Cipher c) => MoreClickedAsync(c)))
(Cipher c) => Helpers.CipherMoreClickedAsync(this, c, !string.IsNullOrWhiteSpace(_uri))))
};
if(Device.RuntimePlatform == Device.iOS)
@@ -102,24 +93,16 @@ namespace Bit.App.Pages
Search = new SearchBar
{
Placeholder = AppResources.SearchVault,
Placeholder = AppResources.Search,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Button)),
CancelButtonColor = Color.FromHex("3c8dbc")
};
// Bug with searchbar on android 7, ref https://bugzilla.xamarin.com/show_bug.cgi?id=43975
// Bug with search bar on android 7, ref https://bugzilla.xamarin.com/show_bug.cgi?id=43975
if(Device.RuntimePlatform == Device.Android && _deviceInfoService.Version >= 24)
{
Search.HeightRequest = 50;
}
Title = _favorites ? AppResources.Favorites : AppResources.MyVault;
ResultsStackLayout = new StackLayout
{
Children = { Search, ListView },
Spacing = 0
};
var noDataLabel = new Label
{
Text = _favorites ? AppResources.NoFavorites : AppResources.NoItems,
@@ -128,6 +111,15 @@ namespace Bit.App.Pages
Style = (Style)Application.Current.Resources["text-muted"]
};
if(_folder || !string.IsNullOrWhiteSpace(_folderId))
{
noDataLabel.Text = AppResources.NoItemsFolder;
}
else if(!string.IsNullOrWhiteSpace(_collectionId))
{
noDataLabel.Text = AppResources.NoItemsCollection;
}
NoDataStackLayout = new StackLayout
{
Children = { noDataLabel },
@@ -136,16 +128,38 @@ namespace Bit.App.Pages
Spacing = 20
};
if(!_favorites)
if(string.IsNullOrWhiteSpace(_collectionId) && !_favorites)
{
var addCipherButton = new ExtendedButton
NoDataStackLayout.Children.Add(new ExtendedButton
{
Text = AppResources.AddAnItem,
Command = new Command(() => AddCipher()),
Command = new Command(() => Helpers.AddCipher(this, _folderId)),
Style = (Style)Application.Current.Resources["btn-primaryAccent"]
};
});
}
NoDataStackLayout.Children.Add(addCipherButton);
ResultsStackLayout = new StackLayout
{
Children = { Search, ListView },
Spacing = 0
};
if(!string.IsNullOrWhiteSpace(_groupingName))
{
Title = _groupingName;
}
else if(_favorites)
{
Title = AppResources.Favorites;
}
else
{
Title = AppResources.SearchVault;
if(Device.RuntimePlatform == Device.iOS || Device.RuntimePlatform == Device.Windows)
{
ToolbarItems.Add(new DismissModalToolBarItem(this));
}
}
LoadingIndicator = new ActivityIndicator
@@ -177,7 +191,7 @@ namespace Bit.App.Pages
_filterResultsCancellationTokenSource);
}
private CancellationTokenSource FilterResultsBackground(string searchFilter,
private CancellationTokenSource FilterResultsBackground(string searchFilter,
CancellationTokenSource previousCts)
{
var cts = new CancellationTokenSource();
@@ -212,102 +226,67 @@ namespace Bit.App.Pages
if(string.IsNullOrWhiteSpace(searchFilter))
{
LoadFolders(Ciphers, ct);
LoadSections(Ciphers, ct);
}
else
{
searchFilter = searchFilter.ToLower();
var filteredCiphers = Ciphers
.Where(s =>
s.Name.ToLower().Contains(searchFilter) ||
.Where(s => s.Name.ToLower().Contains(searchFilter) ||
(s.Subtitle?.ToLower().Contains(searchFilter) ?? false))
.TakeWhile(s => !ct.IsCancellationRequested)
.ToArray();
ct.ThrowIfCancellationRequested();
LoadFolders(filteredCiphers, ct);
LoadSections(filteredCiphers, ct);
}
}
protected override bool OnBackButtonPressed()
{
if(string.IsNullOrWhiteSpace(_uri))
{
return false;
}
_googleAnalyticsService.TrackExtensionEvent("BackClosed", _uri.StartsWith("http") ? "Website" : "App");
_deviceActionService.CloseAutofill();
return true;
}
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted", (sender, success) =>
{
if(success)
{
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
});
AddCipherItem?.InitEvents();
ListView.ItemSelected += CipherSelected;
Search.TextChanged += SearchBar_TextChanged;
Search.SearchButtonPressed += SearchBar_SearchButtonPressed;
AddCipherItem?.InitEvents();
_filterResultsCancellationTokenSource = FetchAndLoadVault();
if(_connectivity.IsConnected && Device.RuntimePlatform == Device.iOS && !_favorites)
{
var pushPromptShow = _settings.GetValueOrDefault(Constants.PushInitialPromptShown, false);
Action registerAction = () =>
{
var lastPushRegistration =
_settings.GetValueOrDefault(Constants.PushLastRegistrationDate, DateTime.MinValue);
if(!pushPromptShow || DateTime.UtcNow - lastPushRegistration > TimeSpan.FromDays(1))
{
_pushNotification.Register();
}
};
if(!pushPromptShow)
{
_settings.AddOrUpdateValue(Constants.PushInitialPromptShown, true);
_userDialogs.Alert(new AlertConfig
{
Message = AppResources.PushNotificationAlert,
Title = AppResources.EnableAutomaticSyncing,
OnAction = registerAction,
OkText = AppResources.OkGotIt
});
}
else
{
// Check push registration once per day
registerAction();
}
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Unsubscribe<Application, bool>(Application.Current, "SyncCompleted");
AddCipherItem?.Dispose();
ListView.ItemSelected -= CipherSelected;
Search.TextChanged -= SearchBar_TextChanged;
Search.SearchButtonPressed -= SearchBar_SearchButtonPressed;
AddCipherItem?.Dispose();
}
protected override bool OnBackButtonPressed()
{
if(string.IsNullOrWhiteSpace(Uri))
{
return false;
}
_googleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Cipher)null);
return true;
}
private void AdjustContent()
{
if(PresentationFolders.Count > 0 || !string.IsNullOrWhiteSpace(Search.Text))
{
Content = ResultsStackLayout;
}
else
{
Content = NoDataStackLayout;
}
}
private CancellationTokenSource FetchAndLoadVault()
{
var cts = new CancellationTokenSource();
if(PresentationFolders.Count > 0 && _syncService.SyncInProgress)
if(PresentationSections.Count > 0 && _syncService.SyncInProgress)
{
return cts;
}
@@ -316,21 +295,33 @@ namespace Bit.App.Pages
Task.Run(async () =>
{
var foldersTask = _folderService.GetAllAsync();
var ciphersTask = _favorites ? _cipherService.GetAllAsync(true) : _cipherService.GetAllAsync();
await Task.WhenAll(foldersTask, ciphersTask);
var folders = await foldersTask;
var ciphers = await ciphersTask;
Folders = folders
.Select(f => new VaultListPageModel.Folder(f))
.OrderBy(s => s.Name)
.ToArray();
IEnumerable<Models.Cipher> ciphers;
if(_folder || !string.IsNullOrWhiteSpace(_folderId))
{
ciphers = await _cipherService.GetAllByFolderAsync(_folderId);
}
else if(!string.IsNullOrWhiteSpace(_collectionId))
{
ciphers = await _cipherService.GetAllByCollectionAsync(_collectionId);
}
else if(_favorites)
{
ciphers = await _cipherService.GetAllAsync(true);
}
else
{
ciphers = await _cipherService.GetAllAsync();
}
Ciphers = ciphers
.Select(s => new VaultListPageModel.Cipher(s, _appSettingsService))
.OrderBy(s => s.Name)
.Select(s => new Cipher(s, _appSettingsService))
.OrderBy(s =>
{
// Sort numbers and letters before special characters
return !string.IsNullOrWhiteSpace(s.Name) && s.Name.Length > 0 &&
Char.IsDigit(s.Name[0]) ? 0 : Char.IsLetter(s.Name[0]) ? 1 : 2;
})
.ThenBy(s => s.Name)
.ThenBy(s => s.Subtitle)
.ToArray();
@@ -344,65 +335,52 @@ namespace Bit.App.Pages
return cts;
}
private void LoadFolders(VaultListPageModel.Cipher[] ciphers, CancellationToken ct)
private void LoadSections(Cipher[] ciphers, CancellationToken ct)
{
var folders = new List<VaultListPageModel.Folder>(Folders);
foreach(var folder in folders)
{
if(folder.Any())
{
folder.Clear();
}
var ciphersToAdd = ciphers
.Where(s => s.FolderId == folder.Id)
.TakeWhile(s => !ct.IsCancellationRequested)
.ToList();
ct.ThrowIfCancellationRequested();
folder.AddRange(ciphersToAdd);
}
var noneToAdd = ciphers
.Where(s => s.FolderId == null)
.TakeWhile(s => !ct.IsCancellationRequested)
.ToList();
ct.ThrowIfCancellationRequested();
var noneFolder = new VaultListPageModel.Folder(noneToAdd);
folders.Add(noneFolder);
var foldersToAdd = folders
.Where(f => f.Any())
.TakeWhile(s => !ct.IsCancellationRequested)
.ToList();
var sections = ciphers.GroupBy(c => c.NameGroup.ToUpperInvariant())
.Select(g => new Section<Cipher>(g.ToList(), g.Key));
ct.ThrowIfCancellationRequested();
Device.BeginInvokeOnMainThread(() =>
{
PresentationFolders.ResetWithRange(foldersToAdd);
AdjustContent();
PresentationSections.ResetWithRange(sections);
if(PresentationSections.Count > 0 || !string.IsNullOrWhiteSpace(Search.Text))
{
Content = ResultsStackLayout;
if(string.IsNullOrWhiteSpace(_uri) && !_folder && string.IsNullOrWhiteSpace(_folderId) &&
string.IsNullOrWhiteSpace(_collectionId) && !_favorites)
{
Search.Focus();
}
}
else if(_syncService.SyncInProgress)
{
Content = LoadingIndicator;
}
else
{
Content = NoDataStackLayout;
}
});
}
private async void CipherSelected(object sender, SelectedItemChangedEventArgs e)
{
var cipher = e.SelectedItem as VaultListPageModel.Cipher;
var cipher = e.SelectedItem as Cipher;
if(cipher == null)
{
return;
}
string selection = null;
if(!string.IsNullOrWhiteSpace(Uri))
if(!string.IsNullOrWhiteSpace(_uri))
{
selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
AppResources.Autofill, AppResources.View);
}
if(selection == AppResources.View || string.IsNullOrWhiteSpace(Uri))
if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri))
{
var page = new VaultViewCipherPage(cipher.Type, cipher.Id);
await Navigation.PushForDeviceAsync(page);
@@ -411,168 +389,17 @@ namespace Bit.App.Pages
{
if(_deviceInfoService.Version < 21)
{
MoreClickedAsync(cipher);
Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri));
}
else
{
_googleAnalyticsService.TrackExtensionEvent("AutoFilled",
Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", cipher);
_googleAnalyticsService.TrackExtensionEvent("AutoFilled",
_uri.StartsWith("http") ? "Website" : "App");
_deviceActionService.Autofill(cipher);
}
}
((ListView)sender).SelectedItem = null;
}
private async void MoreClickedAsync(VaultListPageModel.Cipher cipher)
{
var buttons = new List<string> { AppResources.View, AppResources.Edit };
if(cipher.Type == CipherType.Login)
{
if(!string.IsNullOrWhiteSpace(cipher.LoginPassword.Value))
{
buttons.Add(AppResources.CopyPassword);
}
if(!string.IsNullOrWhiteSpace(cipher.LoginUsername))
{
buttons.Add(AppResources.CopyUsername);
}
if(!string.IsNullOrWhiteSpace(cipher.LoginUri) && (cipher.LoginUri.StartsWith("http://")
|| cipher.LoginUri.StartsWith("https://")))
{
buttons.Add(AppResources.GoToWebsite);
}
}
else if(cipher.Type == CipherType.Card)
{
if(!string.IsNullOrWhiteSpace(cipher.CardNumber))
{
buttons.Add(AppResources.CopyNumber);
}
if(!string.IsNullOrWhiteSpace(cipher.CardCode.Value))
{
buttons.Add(AppResources.CopySecurityCode);
}
}
var selection = await DisplayActionSheet(cipher.Name, AppResources.Cancel, null, buttons.ToArray());
if(selection == AppResources.View)
{
var page = new VaultViewCipherPage(cipher.Type, cipher.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.Edit)
{
var page = new VaultEditCipherPage(cipher.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.CopyPassword)
{
Copy(cipher.LoginPassword.Value, AppResources.Password);
}
else if(selection == AppResources.CopyUsername)
{
Copy(cipher.LoginUsername, AppResources.Username);
}
else if(selection == AppResources.GoToWebsite)
{
Device.OpenUri(new Uri(cipher.LoginUri));
}
else if(selection == AppResources.CopyNumber)
{
Copy(cipher.CardNumber, AppResources.Number);
}
else if(selection == AppResources.CopySecurityCode)
{
Copy(cipher.CardCode.Value, AppResources.SecurityCode);
}
}
private void Copy(string copyText, string alertLabel)
{
_clipboardService.CopyToClipboard(copyText);
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
}
private async void AddCipher()
{
var type = await _userDialogs.ActionSheetAsync(AppResources.SelectTypeAdd, AppResources.Cancel, null, null,
AppResources.TypeLogin, AppResources.TypeCard, AppResources.TypeIdentity, AppResources.TypeSecureNote);
var selectedType = CipherType.SecureNote;
if(type == AppResources.Cancel)
{
return;
}
else if(type == AppResources.TypeLogin)
{
selectedType = CipherType.Login;
}
else if(type == AppResources.TypeCard)
{
selectedType = CipherType.Card;
}
else if(type == AppResources.TypeIdentity)
{
selectedType = CipherType.Identity;
}
var page = new VaultAddCipherPage(selectedType, Uri);
await Navigation.PushForDeviceAsync(page);
}
private class AddCipherToolBarItem : ExtendedToolbarItem
{
private readonly VaultListCiphersPage _page;
public AddCipherToolBarItem(VaultListCiphersPage page)
: base(() => page.AddCipher())
{
_page = page;
Text = AppResources.Add;
Icon = "plus.png";
}
}
private class VaultListHeaderViewCell : ExtendedViewCell
{
public VaultListHeaderViewCell(VaultListCiphersPage page)
{
var image = new CachedImage
{
Source = "folder.png",
WidthRequest = 20,
HeightRequest = 20,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center
};
var label = new Label
{
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"],
VerticalTextAlignment = TextAlignment.Center
};
label.SetBinding(Label.TextProperty, nameof(VaultListPageModel.Folder.Name));
var grid = new Grid
{
ColumnSpacing = 0,
RowSpacing = 0,
Padding = new Thickness(3, 8, 0, 8)
};
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(40, GridUnitType.Absolute) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.Children.Add(image, 0, 0);
grid.Children.Add(label, 1, 0);
View = grid;
BackgroundColor = Color.FromHex("efeff4");
}
}
}
}

View File

@@ -0,0 +1,283 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Utilities;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Collections.Generic;
using System.Threading;
using static Bit.App.Models.Page.VaultListPageModel;
namespace Bit.App.Pages
{
public class VaultListGroupingsPage : ExtendedContentPage
{
private readonly IFolderService _folderService;
private readonly ICollectionService _collectionService;
private readonly ICipherService _cipherService;
private readonly IUserDialogs _userDialogs;
private readonly IConnectivity _connectivity;
private readonly IDeviceActionService _deviceActionService;
private readonly ISyncService _syncService;
private readonly IPushNotificationService _pushNotification;
private readonly IDeviceInfoService _deviceInfoService;
private readonly ISettings _settings;
private readonly IAppSettingsService _appSettingsService;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private CancellationTokenSource _filterResultsCancellationTokenSource;
public VaultListGroupingsPage()
: base(true)
{
_folderService = Resolver.Resolve<IFolderService>();
_collectionService = Resolver.Resolve<ICollectionService>();
_cipherService = Resolver.Resolve<ICipherService>();
_connectivity = Resolver.Resolve<IConnectivity>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_syncService = Resolver.Resolve<ISyncService>();
_pushNotification = Resolver.Resolve<IPushNotificationService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_settings = Resolver.Resolve<ISettings>();
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
Init();
}
public ExtendedObservableCollection<Section<Grouping>> PresentationSections { get; private set; }
= new ExtendedObservableCollection<Section<Grouping>>();
public ListView ListView { get; set; }
public StackLayout NoDataStackLayout { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
private AddCipherToolBarItem AddCipherItem { get; set; }
private SearchToolBarItem SearchItem { get; set; }
private void Init()
{
SearchItem = new SearchToolBarItem(this);
AddCipherItem = new AddCipherToolBarItem(this, null);
ToolbarItems.Add(SearchItem);
ToolbarItems.Add(AddCipherItem);
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{
IsGroupingEnabled = true,
ItemsSource = PresentationSections,
HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(
nameof(Section<Grouping>.Name), nameof(Section<Grouping>.Count), new Thickness(16, 12))),
ItemTemplate = new DataTemplate(() => new VaultGroupingViewCell())
};
if(Device.RuntimePlatform == Device.iOS)
{
ListView.RowHeight = -1;
}
var noDataLabel = new Label
{
Text = AppResources.NoItems,
HorizontalTextAlignment = TextAlignment.Center,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"]
};
var addCipherButton = new ExtendedButton
{
Text = AppResources.AddAnItem,
Command = new Command(() => Helpers.AddCipher(this, null)),
Style = (Style)Application.Current.Resources["btn-primaryAccent"]
};
NoDataStackLayout = new StackLayout
{
Children = { noDataLabel, addCipherButton },
VerticalOptions = LayoutOptions.CenterAndExpand,
Padding = new Thickness(20, 0),
Spacing = 20
};
LoadingIndicator = new ActivityIndicator
{
IsRunning = true,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
Content = LoadingIndicator;
Title = AppResources.MyVault;
}
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted", (sender, success) =>
{
if(success)
{
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
});
ListView.ItemSelected += GroupingSelected;
AddCipherItem?.InitEvents();
SearchItem?.InitEvents();
_filterResultsCancellationTokenSource = FetchAndLoadVault();
if(_connectivity.IsConnected && Device.RuntimePlatform == Device.iOS)
{
var pushPromptShow = _settings.GetValueOrDefault(Constants.PushInitialPromptShown, false);
Action registerAction = () =>
{
var lastPushRegistration =
_settings.GetValueOrDefault(Constants.PushLastRegistrationDate, DateTime.MinValue);
if(!pushPromptShow || DateTime.UtcNow - lastPushRegistration > TimeSpan.FromDays(1))
{
_pushNotification.Register();
}
};
if(!pushPromptShow)
{
_settings.AddOrUpdateValue(Constants.PushInitialPromptShown, true);
_userDialogs.Alert(new AlertConfig
{
Message = AppResources.PushNotificationAlert,
Title = AppResources.EnableAutomaticSyncing,
OnAction = registerAction,
OkText = AppResources.OkGotIt
});
}
else
{
// Check push registration once per day
registerAction();
}
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Unsubscribe<Application, bool>(Application.Current, "SyncCompleted");
ListView.ItemSelected -= GroupingSelected;
AddCipherItem?.Dispose();
SearchItem?.Dispose();
}
private CancellationTokenSource FetchAndLoadVault()
{
var cts = new CancellationTokenSource();
_filterResultsCancellationTokenSource?.Cancel();
Task.Run(async () =>
{
var sections = new List<Section<Grouping>>();
var ciphers = await _cipherService.GetAllAsync();
var collectionsDict = (await _collectionService.GetAllCipherAssociationsAsync())
.GroupBy(c => c.Item2).ToDictionary(g => g.Key, v => v.ToList());
var folderCounts = new Dictionary<string, int> { ["none"] = 0 };
foreach(var cipher in ciphers)
{
if(cipher.FolderId != null)
{
if(!folderCounts.ContainsKey(cipher.FolderId))
{
folderCounts.Add(cipher.FolderId, 0);
}
folderCounts[cipher.FolderId]++;
}
else
{
folderCounts["none"]++;
}
}
var folders = await _folderService.GetAllAsync();
var folderGroupings = folders?
.Select(f => new Grouping(f, folderCounts.ContainsKey(f.Id) ? folderCounts[f.Id] : 0))
.OrderBy(g => g.Name).ToList();
folderGroupings.Add(new Grouping(AppResources.FolderNone, folderCounts["none"]));
sections.Add(new Section<Grouping>(folderGroupings, AppResources.Folders));
var collections = await _collectionService.GetAllAsync();
var collectionGroupings = collections?
.Select(c => new Grouping(c,
collectionsDict.ContainsKey(c.Id) ? collectionsDict[c.Id].Count() : 0))
.OrderBy(g => g.Name).ToList();
if(collectionGroupings?.Any() ?? false)
{
sections.Add(new Section<Grouping>(collectionGroupings, AppResources.Collections));
}
Device.BeginInvokeOnMainThread(() =>
{
PresentationSections.ResetWithRange(sections);
if(ciphers.Any() || folders.Any())
{
Content = ListView;
}
else if(_syncService.SyncInProgress)
{
Content = LoadingIndicator;
}
else
{
Content = NoDataStackLayout;
}
});
}, cts.Token);
return cts;
}
private async void GroupingSelected(object sender, SelectedItemChangedEventArgs e)
{
var grouping = e.SelectedItem as Grouping;
if(grouping == null)
{
return;
}
Page page;
if(grouping.Folder)
{
page = new VaultListCiphersPage(folder: true, folderId: grouping.Id, groupingName: grouping.Name);
}
else
{
page = new VaultListCiphersPage(collectionId: grouping.Id, groupingName: grouping.Name);
}
await Navigation.PushAsync(page);
((ListView)sender).SelectedItem = null;
}
private async void Search()
{
var page = new ExtendedNavigationPage(new VaultListCiphersPage());
await Navigation.PushModalAsync(page);
}
private class SearchToolBarItem : ExtendedToolbarItem
{
public SearchToolBarItem(VaultListGroupingsPage page)
: base(() => page.Search())
{
Text = AppResources.Search;
Icon = "search.png";
}
}
}
}

View File

@@ -136,7 +136,7 @@ namespace Bit.App.Pages
{
if(Device.RuntimePlatform == Device.Android && Model.LoginUri.StartsWith("androidapp://"))
{
MessagingCenter.Send(Application.Current, "LaunchApp", Model.LoginUri);
_deviceActionService.LaunchApp(Model.LoginUri);
}
else if(Model.LoginUri.StartsWith("http://") || Model.LoginUri.StartsWith("https://"))
{

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