Share-to-Send for Android (#1343)

* Android implementation

* remove iOS attempt for now
This commit is contained in:
Matt Portune 2021-03-31 10:19:05 -04:00 committed by GitHub
parent ce0b8bc62d
commit d926565358
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 203 additions and 38 deletions

View File

@ -30,6 +30,16 @@ namespace Bit.Droid
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation |
ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
ConfigChanges.Navigation)] ConfigChanges.Navigation)]
[IntentFilter(
new[] { Intent.ActionSend },
Categories = new[] { Intent.CategoryDefault },
DataMimeTypes = new[]
{
@"application/*",
@"image/*",
@"video/*",
@"text/*"
})]
[Register("com.x8bit.bitwarden.MainActivity")] [Register("com.x8bit.bitwarden.MainActivity")]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{ {
@ -169,7 +179,7 @@ namespace Bit.Droid
_appOptions.GeneratorTile = true; _appOptions.GeneratorTile = true;
} }
} }
if (intent.GetBooleanExtra("myVaultTile", false)) else if (intent.GetBooleanExtra("myVaultTile", false))
{ {
_messagingService.Send("popAllAndGoToTabMyVault"); _messagingService.Send("popAllAndGoToTabMyVault");
if (_appOptions != null) if (_appOptions != null)
@ -177,6 +187,14 @@ namespace Bit.Droid
_appOptions.MyVaultTile = true; _appOptions.MyVaultTile = true;
} }
} }
else if (intent.Action == Intent.ActionSend && intent.Type != null)
{
if (_appOptions != null)
{
_appOptions.CreateSend = GetCreateSendRequest(intent);
}
_messagingService.Send("popAllAndGoToTabSend");
}
else else
{ {
ParseYubiKey(intent.DataString); ParseYubiKey(intent.DataString);
@ -298,7 +316,8 @@ namespace Bit.Droid
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"), Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra("autofillFrameworkUri"),
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false), MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false), GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false) FromAutofillFramework = Intent.GetBooleanExtra("autofillFramework", false),
CreateSend = GetCreateSendRequest(Intent)
}; };
var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0); var fillType = Intent.GetIntExtra("autofillFrameworkFillType", 0);
if (fillType > 0) if (fillType > 0)
@ -320,6 +339,37 @@ namespace Bit.Droid
return options; return options;
} }
private Tuple<SendType, string, byte[], string> GetCreateSendRequest(Intent intent)
{
if (intent.Action == Intent.ActionSend && intent.Type != null)
{
var type = intent.Type;
if (type.Contains("text/"))
{
var subject = intent.GetStringExtra(Intent.ExtraSubject);
var text = intent.GetStringExtra(Intent.ExtraText);
return new Tuple<SendType, string, byte[], string>(SendType.Text, subject, null, text);
}
else
{
var data = intent.ClipData?.GetItemAt(0);
var uri = data?.Uri;
var filename = AndroidHelpers.GetFileName(ApplicationContext, uri);
try
{
using (var stream = ContentResolver.OpenInputStream(uri))
using (var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
return new Tuple<SendType, string, byte[], string>(SendType.File, filename, memoryStream.ToArray(), null);
}
}
catch (Java.IO.FileNotFoundException) { }
}
}
return null;
}
private void ParseYubiKey(string data) private void ParseYubiKey(string data)
{ {
if (data == null) if (data == null)

View File

@ -760,6 +760,17 @@ namespace Bit.Droid.Services
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime() // ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
return SystemClock.ElapsedRealtime(); return SystemClock.ElapsedRealtime();
} }
public void CloseMainApp()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
if (activity == null)
{
return;
}
activity.Finish();
_messagingService.Send("finishMainActivity");
}
private bool DeleteDir(Java.IO.File dir) private bool DeleteDir(Java.IO.File dir)
{ {

View File

@ -44,5 +44,6 @@ namespace Bit.App.Abstractions
void OpenAutofillSettings(); void OpenAutofillSettings();
bool UsingDarkTheme(); bool UsingDarkTheme();
long GetActiveTime(); long GetActiveTime();
void CloseMainApp();
} }
} }

View File

@ -131,7 +131,8 @@ namespace Bit.App
await SetMainPageAsync(); await SetMainPageAsync();
} }
else if (message.Command == "popAllAndGoToTabGenerator" || else if (message.Command == "popAllAndGoToTabGenerator" ||
message.Command == "popAllAndGoToTabMyVault") message.Command == "popAllAndGoToTabMyVault" ||
message.Command == "popAllAndGoToTabSend")
{ {
Device.BeginInvokeOnMainThread(async () => Device.BeginInvokeOnMainThread(async () =>
{ {
@ -146,11 +147,15 @@ namespace Bit.App
Options.MyVaultTile = false; Options.MyVaultTile = false;
tabsPage.ResetToVaultPage(); tabsPage.ResetToVaultPage();
} }
else else if (message.Command == "popAllAndGoToTabGenerator")
{ {
Options.GeneratorTile = false; Options.GeneratorTile = false;
tabsPage.ResetToGeneratorPage(); tabsPage.ResetToGeneratorPage();
} }
else if (message.Command == "popAllAndGoToTabSend")
{
tabsPage.ResetToSendPage();
}
} }
}); });
} }
@ -274,6 +279,10 @@ namespace Bit.App
{ {
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
} }
else if (Options.CreateSend != null)
{
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
}
else else
{ {
Current.MainPage = new TabsPage(Options); Current.MainPage = new TabsPage(Options);

View File

@ -1,4 +1,5 @@
using Bit.Core.Enums; using System;
using Bit.Core.Enums;
namespace Bit.App.Models namespace Bit.App.Models
{ {
@ -19,6 +20,7 @@ namespace Bit.App.Models
public string SaveCardExpYear { get; set; } public string SaveCardExpYear { get; set; }
public string SaveCardCode { get; set; } public string SaveCardCode { get; set; }
public bool IosExtension { get; set; } public bool IosExtension { get; set; }
public Tuple<SendType, string, byte[], string> CreateSend { get; set; }
public void SetAllFrom(AppOptions o) public void SetAllFrom(AppOptions o)
{ {
@ -41,6 +43,7 @@ namespace Bit.App.Models
SaveCardExpYear = o.SaveCardExpYear; SaveCardExpYear = o.SaveCardExpYear;
SaveCardCode = o.SaveCardCode; SaveCardCode = o.SaveCardCode;
IosExtension = o.IosExtension; IosExtension = o.IosExtension;
CreateSend = o.CreateSend;
} }
} }
} }

View File

@ -117,7 +117,7 @@
</StackLayout> </StackLayout>
<StackLayout <StackLayout
StyleClass="box-row" StyleClass="box-row"
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"> IsVisible="{Binding ShowTypeButtons}">
<Label <Label
Text="{u:I18n Type}" Text="{u:I18n Type}"
StyleClass="box-label" /> StyleClass="box-label" />
@ -222,6 +222,7 @@
HorizontalTextAlignment="Center" /> HorizontalTextAlignment="Center" />
<Button <Button
Text="{u:I18n ChooseFile}" Text="{u:I18n ChooseFile}"
IsVisible="{Binding IsAddFromShare, Converter={StaticResource inverseBool}}"
IsEnabled="{Binding SendEnabled}" IsEnabled="{Binding SendEnabled}"
StyleClass="box-button-row" StyleClass="box-button-row"
Clicked="ChooseFile_Clicked" /> Clicked="ChooseFile_Clicked" />
@ -234,7 +235,7 @@
</StackLayout> </StackLayout>
<Label <Label
Text="{u:I18n TypeFileInfo}" Text="{u:I18n TypeFileInfo}"
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}" IsVisible="{Binding ShowTypeButtons}"
StyleClass="box-footer-label" StyleClass="box-footer-label"
Margin="0,5,0,0" /> Margin="0,5,0,0" />
</StackLayout> </StackLayout>

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -15,18 +17,21 @@ namespace Bit.App.Pages
{ {
private readonly IBroadcasterService _broadcasterService; private readonly IBroadcasterService _broadcasterService;
private AppOptions _appOptions;
private SendAddEditPageViewModel _vm; private SendAddEditPageViewModel _vm;
public SendAddEditPage( public SendAddEditPage(
AppOptions appOptions = null,
string sendId = null, string sendId = null,
SendType? type = null) SendType? type = null)
{ {
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService"); _broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_appOptions = appOptions;
InitializeComponent(); InitializeComponent();
_vm = BindingContext as SendAddEditPageViewModel; _vm = BindingContext as SendAddEditPageViewModel;
_vm.Page = this; _vm.Page = this;
_vm.SendId = sendId; _vm.SendId = sendId;
_vm.Type = type; _vm.Type = appOptions?.CreateSend?.Item1 ?? type;
SetActivityIndicator(); SetActivityIndicator();
if (Device.RuntimePlatform == Device.Android) if (Device.RuntimePlatform == Device.Android)
{ {
@ -95,6 +100,7 @@ namespace Bit.App.Pages
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
return; return;
} }
await HandleCreateRequest();
if (!_vm.EditMode && string.IsNullOrWhiteSpace(_vm.Send?.Name)) if (!_vm.EditMode && string.IsNullOrWhiteSpace(_vm.Send?.Name))
{ {
RequestFocus(_nameEntry); RequestFocus(_nameEntry);
@ -271,5 +277,33 @@ namespace Bit.App.Pages
ToolbarItems.Remove(_shareLink); ToolbarItems.Remove(_shareLink);
} }
} }
private async Task HandleCreateRequest()
{
if (_appOptions?.CreateSend == null)
{
return;
}
_vm.IsAddFromShare = true;
var name = _appOptions.CreateSend.Item2;
_vm.Send.Name = name;
var type = _appOptions.CreateSend.Item1;
if (type == SendType.File)
{
_vm.FileData = _appOptions.CreateSend.Item3;
_vm.FileName = name;
FileType_Clicked(null, null);
}
else
{
var text = _appOptions.CreateSend.Item4;
_vm.Send.Text.Text = text;
TextType_Clicked(null, null);
}
_appOptions.CreateSend = null;
}
} }
} }

View File

@ -95,6 +95,7 @@ namespace Bit.App.Pages
public string NewPassword { get; set; } public string NewPassword { get; set; }
public bool ShareOnSave { get; set; } public bool ShareOnSave { get; set; }
public bool DisableHideEmailControl { get; set; } public bool DisableHideEmailControl { get; set; }
public bool IsAddFromShare { get; set; }
public List<KeyValuePair<string, SendType>> TypeOptions { get; } public List<KeyValuePair<string, SendType>> TypeOptions { get; }
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; } public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; } public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
@ -208,6 +209,7 @@ namespace Bit.App.Pages
get => _sendOptionsPolicyInEffect; get => _sendOptionsPolicyInEffect;
set => SetProperty(ref _sendOptionsPolicyInEffect, value); set => SetProperty(ref _sendOptionsPolicyInEffect, value);
} }
public bool ShowTypeButtons => !EditMode && !IsAddFromShare;
public bool EditMode => !string.IsNullOrWhiteSpace(SendId); public bool EditMode => !string.IsNullOrWhiteSpace(SendId);
public bool IsText => Send?.Type == SendType.Text; public bool IsText => Send?.Type == SendType.Text;
public bool IsFile => Send?.Type == SendType.File; public bool IsFile => Send?.Type == SendType.File;
@ -335,7 +337,7 @@ namespace Bit.App.Pages
{ {
if (!_canAccessPremium) if (!_canAccessPremium)
{ {
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired); await _platformUtilsService.ShowDialogAsync(AppResources.SendFilePremiumRequired);
return false; return false;
} }
if (!EditMode) if (!EditMode)
@ -374,10 +376,6 @@ namespace Bit.App.Pages
var sendId = await _sendService.SaveWithServerAsync(send, encryptedFileData); var sendId = await _sendService.SaveWithServerAsync(send, encryptedFileData);
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null,
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
await Page.Navigation.PopModalAsync();
if (Device.RuntimePlatform == Device.Android && IsFile) if (Device.RuntimePlatform == Device.Android && IsFile)
{ {
// Workaround for https://github.com/xamarin/Xamarin.Forms/issues/5418 // Workaround for https://github.com/xamarin/Xamarin.Forms/issues/5418
@ -395,7 +393,21 @@ namespace Bit.App.Pages
await AppHelpers.ShareSendUrlAsync(savedSendView); await AppHelpers.ShareSendUrlAsync(savedSendView);
} }
} }
else
{
_platformUtilsService.ShowToast("success", null,
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
}
if (IsAddFromShare && Device.RuntimePlatform == Device.Android)
{
_deviceActionService.CloseMainApp();
}
else
{
await Page.Navigation.PopModalAsync();
}
return true; return true;
} }
catch (ApiException e) catch (ApiException e)
@ -432,11 +444,29 @@ namespace Bit.App.Pages
public async Task TypeChangedAsync(SendType type) public async Task TypeChangedAsync(SendType type)
{ {
if (!SendEnabled)
{
await _platformUtilsService.ShowDialogAsync(AppResources.SendDisabledWarning);
if (IsAddFromShare && Device.RuntimePlatform == Device.Android)
{
_deviceActionService.CloseMainApp();
}
else
{
await Page.Navigation.PopModalAsync();
}
return;
}
if (Send != null) if (Send != null)
{ {
if (!EditMode && type == SendType.File && !_canAccessPremium) if (!EditMode && type == SendType.File && !_canAccessPremium)
{ {
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired); await _platformUtilsService.ShowDialogAsync(AppResources.SendFilePremiumRequired);
if (IsAddFromShare && Device.RuntimePlatform == Device.Android)
{
_deviceActionService.CloseMainApp();
return;
}
type = SendType.Text; type = SendType.Text;
} }
Send.Type = type; Send.Type = type;

View File

@ -19,10 +19,11 @@ namespace Bit.App.Pages
private readonly SendGroupingsPageViewModel _vm; private readonly SendGroupingsPageViewModel _vm;
private readonly string _pageName; private readonly string _pageName;
private AppOptions _appOptions;
private PreviousPageInfo _previousPage; private PreviousPageInfo _previousPage;
public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null, public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null,
PreviousPageInfo previousPage = null) AppOptions appOptions = null, PreviousPageInfo previousPage = null)
{ {
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks); _pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
InitializeComponent(); InitializeComponent();
@ -35,6 +36,7 @@ namespace Bit.App.Pages
_vm.Page = this; _vm.Page = this;
_vm.MainPage = mainPage; _vm.MainPage = mainPage;
_vm.Type = type; _vm.Type = type;
_appOptions = appOptions;
_previousPage = previousPage; _previousPage = previousPage;
if (pageTitle != null) if (pageTitle != null)
{ {
@ -109,8 +111,8 @@ namespace Bit.App.Pages
} }
} }
await ShowPreviousPageAsync();
AdjustToolbar(); AdjustToolbar();
await CheckAddRequest();
}, _mainContent); }, _mainContent);
} }
@ -122,6 +124,18 @@ namespace Bit.App.Pages
_vm.DisableRefreshing(); _vm.DisableRefreshing();
} }
private async Task CheckAddRequest()
{
if (_appOptions?.CreateSend != null)
{
if (DoOnce())
{
var page = new SendAddEditPage(_appOptions);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
}
private async void RowSelected(object sender, SelectedItemChangedEventArgs e) private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
{ {
((ListView)sender).SelectedItem = null; ((ListView)sender).SelectedItem = null;
@ -172,28 +186,11 @@ namespace Bit.App.Pages
{ {
if (DoOnce()) if (DoOnce())
{ {
var page = new SendAddEditPage(null, _vm.Type); var page = new SendAddEditPage(null, null, _vm.Type);
await Navigation.PushModalAsync(new NavigationPage(page)); await Navigation.PushModalAsync(new NavigationPage(page));
} }
} }
private async Task ShowPreviousPageAsync()
{
if (_previousPage == null)
{
return;
}
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
{
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.SendId)));
}
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
{
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.SendId)));
}
_previousPage = null;
}
private void AdjustToolbar() private void AdjustToolbar()
{ {
_addItem.IsEnabled = _vm.SendEnabled; _addItem.IsEnabled = _vm.SendEnabled;

View File

@ -208,7 +208,7 @@ namespace Bit.App.Pages
public async Task SelectSendAsync(SendView send) public async Task SelectSendAsync(SendView send)
{ {
var page = new SendAddEditPage(send.Id); var page = new SendAddEditPage(null, send.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page)); await Page.Navigation.PushModalAsync(new NavigationPage(page));
} }

View File

@ -113,7 +113,7 @@ namespace Bit.App.Pages
public async Task SelectSendAsync(SendView send) public async Task SelectSendAsync(SendView send)
{ {
var page = new SendAddEditPage(send.Id); var page = new SendAddEditPage(null, send.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page)); await Page.Navigation.PushModalAsync(new NavigationPage(page));
} }

View File

@ -20,7 +20,7 @@ namespace Bit.App.Pages
}; };
Children.Add(_groupingsPage); Children.Add(_groupingsPage);
_sendGroupingsPage = new NavigationPage(new SendGroupingsPage(true)) _sendGroupingsPage = new NavigationPage(new SendGroupingsPage(true, null, null, appOptions))
{ {
Title = AppResources.Send, Title = AppResources.Send,
IconImageSource = "paper_plane.png", IconImageSource = "paper_plane.png",
@ -60,6 +60,10 @@ namespace Bit.App.Pages
{ {
appOptions.MyVaultTile = false; appOptions.MyVaultTile = false;
} }
else if (appOptions?.CreateSend != null)
{
ResetToSendPage();
}
} }
public void ResetToVaultPage() public void ResetToVaultPage()
@ -71,6 +75,11 @@ namespace Bit.App.Pages
{ {
CurrentPage = _generatorPage; CurrentPage = _generatorPage;
} }
public void ResetToSendPage()
{
CurrentPage = _sendGroupingsPage;
}
protected async override void OnCurrentPageChanged() protected async override void OnCurrentPageChanged()
{ {

View File

@ -3496,5 +3496,11 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("SendOptionsPolicyInEffect", resourceCulture); return ResourceManager.GetString("SendOptionsPolicyInEffect", resourceCulture);
} }
} }
public static string SendFilePremiumRequired {
get {
return ResourceManager.GetString("SendFilePremiumRequired", resourceCulture);
}
}
} }
} }

View File

@ -1979,4 +1979,8 @@
<value>One or more organization policies are affecting your Send options.</value> <value>One or more organization policies are affecting your Send options.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment> <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data> </data>
<data name="SendFilePremiumRequired" xml:space="preserve">
<value>Free accounts are restricted to sharing text only. A premium membership is required to use files with Send.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
</root> </root>

View File

@ -154,7 +154,7 @@ namespace Bit.App.Utilities
} }
else if (selection == AppResources.Edit) else if (selection == AppResources.Edit)
{ {
await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(send.Id))); await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(null, send.Id)));
} }
else if (selection == AppResources.CopyLink) else if (selection == AppResources.CopyLink)
{ {
@ -409,6 +409,11 @@ namespace Bit.App.Utilities
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions)); Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions));
return true; return true;
} }
if (appOptions.CreateSend != null)
{
Application.Current.MainPage = new NavigationPage(new SendAddEditPage(appOptions));
return true;
}
} }
return false; return false;
} }

View File

@ -437,6 +437,11 @@ namespace Bit.iOS.Core.Services
return iOSHelpers.GetSystemUpTimeMilliseconds() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); return iOSHelpers.GetSystemUpTimeMilliseconds() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
} }
public void CloseMainApp()
{
throw new NotImplementedException();
}
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e) private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{ {
if (sender is UIImagePickerController picker) if (sender is UIImagePickerController picker)