Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
056bce3dd9 | ||
|
|
5d6575e97b | ||
|
|
f092d4ffc3 | ||
|
|
5bae15831b | ||
|
|
cf19bd88f0 | ||
|
|
38ac6a1082 | ||
|
|
b88e2bd3ce | ||
|
|
fad24c4308 | ||
|
|
018fd83dba | ||
|
|
aa95da167f | ||
|
|
24e6a0be68 | ||
|
|
a2c962c2f6 | ||
|
|
aa61331181 | ||
|
|
00f0a7589c | ||
|
|
d39609351a | ||
|
|
6985ccf076 | ||
|
|
b448cad4de | ||
|
|
14540b4cc0 | ||
|
|
898b76a549 | ||
|
|
5cf6e382d8 | ||
|
|
e2ba56a227 | ||
|
|
ec9960e28e | ||
|
|
dc59283160 | ||
|
|
d255d44be5 | ||
|
|
b2f68a5a7e | ||
|
|
ec32679ab1 | ||
|
|
8b2471c128 | ||
|
|
022eba2c05 | ||
|
|
029c6fcfe3 | ||
|
|
faaa0b2488 | ||
|
|
daa2ca876b | ||
|
|
81700cfb44 | ||
|
|
6e58db95ed | ||
|
|
9b54862450 | ||
|
|
f79efadd82 | ||
|
|
615a7670bd | ||
|
|
155b8b472f | ||
|
|
b35e3454f0 | ||
|
|
51b4716d45 | ||
|
|
b62803a03a | ||
|
|
616893955f | ||
|
|
0f387a139b | ||
|
|
219c81aac5 | ||
|
|
083003d34f | ||
|
|
699f76c29e | ||
|
|
b670280688 | ||
|
|
37ea84ffe9 | ||
|
|
40b861acbe | ||
|
|
783c4d104c | ||
|
|
9bbddd6aeb | ||
|
|
e753acbc3f | ||
|
|
92b7b1d603 | ||
|
|
b07dc8443e | ||
|
|
3f99c513f3 | ||
|
|
793241523d | ||
|
|
7cff22fb9e | ||
|
|
214f308027 | ||
|
|
c1ce971adb | ||
|
|
f5896be699 | ||
|
|
186f839569 | ||
|
|
4879d906d9 | ||
|
|
09412f0b78 | ||
|
|
2f2d85576f | ||
|
|
362ddd0339 | ||
|
|
9499b7f562 | ||
|
|
d8bb12b5f1 | ||
|
|
5d464f4477 | ||
|
|
aaea0b2659 | ||
|
|
c9ceb09906 | ||
|
|
3b44ede67e | ||
|
|
b48e8eeb0e | ||
|
|
1fafc29ec3 | ||
|
|
1a9d0576c8 | ||
|
|
bc04211b79 | ||
|
|
cfe34355bd | ||
|
|
e3e833d8c0 | ||
|
|
5606a0a968 | ||
|
|
f0358f1da8 | ||
|
|
a3129e9e17 | ||
|
|
7435ede254 | ||
|
|
84e79e92b4 | ||
|
|
6268130998 | ||
|
|
7ad639599a | ||
|
|
caff67b77d | ||
|
|
c45a77d538 | ||
|
|
4b24fe1bf4 | ||
|
|
73e5fb6314 | ||
|
|
84ea28adfa | ||
|
|
955fc97cb2 | ||
|
|
e4012e4f87 | ||
|
|
2c662c428c | ||
|
|
da199deed1 | ||
|
|
abf75cffd9 | ||
|
|
184f13b148 | ||
|
|
d1c7309b29 | ||
|
|
62db6552d2 | ||
|
|
a019b9e1d3 | ||
|
|
cb22572f2b | ||
|
|
b52134e9ee | ||
|
|
44ef82219b | ||
|
|
8c89b0e587 | ||
|
|
322b251def | ||
|
|
0a6767209d | ||
|
|
1694b5d6fd | ||
|
|
0dd9ad43e8 | ||
|
|
c1ae3f1fb2 | ||
|
|
d84627aa2c | ||
|
|
0e020924ff | ||
|
|
4f5e238685 | ||
|
|
72ff680114 | ||
|
|
849ec6fa8f | ||
|
|
36ee3aaec6 | ||
|
|
497d4f50dd | ||
|
|
74a40b2274 | ||
|
|
75e85541a6 | ||
|
|
1d8fbac796 | ||
|
|
daf6d1936f | ||
|
|
1768e8cb62 | ||
|
|
d2d6bfc065 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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">
|
||||
|
||||
146
src/Android/Autofill/AutofillHelpers.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/Android/Autofill/AutofillService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
192
src/Android/Autofill/Field.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
310
src/Android/Autofill/FieldCollection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
273
src/Android/Autofill/FilledItem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Android/Autofill/Parser.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Android/Autofill/SavedItem.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
859
src/Android/Resources/Resource.Designer.cs
generated
BIN
src/Android/Resources/drawable-hdpi/autofill_enable.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
src/Android/Resources/drawable-hdpi/autofill_use.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/Android/Resources/drawable-hdpi/cube.png
Normal file
|
After Width: | Height: | Size: 738 B |
BIN
src/Android/Resources/drawable-hdpi/folder_o.png
Normal file
|
After Width: | Height: | Size: 482 B |
BIN
src/Android/Resources/drawable-xhdpi/autofill_enable.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/Android/Resources/drawable-xhdpi/autofill_use.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/Android/Resources/drawable-xhdpi/cube.png
Normal file
|
After Width: | Height: | Size: 941 B |
BIN
src/Android/Resources/drawable-xhdpi/folder_o.png
Normal file
|
After Width: | Height: | Size: 686 B |
BIN
src/Android/Resources/drawable-xxhdpi/autofill_enable.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/autofill_use.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/cube.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/folder_o.png
Normal file
|
After Width: | Height: | Size: 816 B |
BIN
src/Android/Resources/drawable-xxxhdpi/cube.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/Android/Resources/drawable-xxxhdpi/folder_o.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/Android/Resources/drawable/autofill_enable.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/Android/Resources/drawable/autofill_use.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/Android/Resources/drawable/cube.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
src/Android/Resources/drawable/folder_o.png
Normal file
|
After Width: | Height: | Size: 401 B |
40
src/Android/Resources/layout/autofill_listitem.axml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
3
src/Android/Resources/xml/autofillservice.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<autofill-service
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
11
src/App/Abstractions/Repositories/ICollectionRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
string Build { get; }
|
||||
string Version { get; }
|
||||
bool AutofillAccessibilityServiceEnabled { get; }
|
||||
bool AutofillServiceEnabled { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
14
src/App/Abstractions/Services/ICollectionService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,7 @@
|
||||
float Scale { get; }
|
||||
bool NfcEnabled { get; }
|
||||
bool HasCamera { get; }
|
||||
bool AutofillServiceSupported { get; }
|
||||
bool HasFaceIdSupport { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,7 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
void UpdateLastActivity(DateTime? activityDate = null);
|
||||
Task<LockType> GetLockTypeAsync(bool forceLock);
|
||||
Task CheckLockAsync(bool forceLock);
|
||||
bool TopPageIsLock();
|
||||
}
|
||||
}
|
||||
128
src/App/App.cs
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
16
src/App/Controls/AddCipherToolBarItem.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
src/App/Controls/SectionHeaderViewCell.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/App/Controls/VaultGroupingViewCell.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.App.Models.Page;
|
||||
using FFImageLoading.Forms;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
9
src/App/Models/Api/Response/CollectionResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
21
src/App/Models/AppOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
29
src/App/Models/Collection.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
18
src/App/Models/Data/CipherCollectionData.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
36
src/App/Models/Data/CollectionData.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Bit.App.Pages
|
||||
NoFooter = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
new TableSection(Helpers.GetEmptyTableSectionTitle())
|
||||
{
|
||||
PasswordCell
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Bit.App.Pages
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
new TableSection(Helpers.GetEmptyTableSectionTitle())
|
||||
{
|
||||
EmailCell,
|
||||
PasswordCell
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Bit.App.Pages
|
||||
VerticalOptions = LayoutOptions.Start,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
new TableSection(Helpers.GetEmptyTableSectionTitle())
|
||||
{
|
||||
EmailCell
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
235
src/App/Pages/Tools/ToolsAccessibilityServicePage.cs
Normal 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"]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Bit.App.Pages
|
||||
NoHeader = true,
|
||||
Root = new TableRoot
|
||||
{
|
||||
new TableSection(" ")
|
||||
new TableSection(Helpers.GetEmptyTableSectionTitle())
|
||||
{
|
||||
RegenerateCell,
|
||||
CopyCell
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void Init()
|
||||
{
|
||||
FieldsSection = new TableSection(" ");
|
||||
FieldsSection = new TableSection(Helpers.GetEmptyTableSectionTitle());
|
||||
|
||||
Table = new ExtendedTableView
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
283
src/App/Pages/Vault/VaultListGroupingsPage.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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://"))
|
||||
{
|
||||
|
||||