Skip to content

Commit

Permalink
Clipboard handling adjustments for Android 13 (bitwarden#1947)
Browse files Browse the repository at this point in the history
* Android 13 clipboard tweaks

* adjustments

* adjustments round 2
  • Loading branch information
mpbw2 authored Jun 10, 2022
1 parent dd6003b commit 48a8d9a
Show file tree
Hide file tree
Showing 13 changed files with 84 additions and 43 deletions.
10 changes: 5 additions & 5 deletions src/Android/MainApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Droid.Services;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
using Plugin.Fingerprint;
using Xamarin.Android.Net;
Expand Down Expand Up @@ -134,10 +133,11 @@ private void RegisterLocalServices()
var stateService = new StateService(mobileStorageService, secureStorageService);
var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var deviceActionService = new DeviceActionService(stateService, messagingService,
var clipboardService = new ClipboardService(stateService);
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
var biometricService = new BiometricService();
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
Expand All @@ -152,7 +152,7 @@ private void RegisterLocalServices()
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
ServiceContainer.Register<IStateService>("stateService", stateService);
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
ServiceContainer.Register<IClipboardService>("clipboardService", new ClipboardService(stateService));
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
Expand Down
34 changes: 31 additions & 3 deletions src/Android/Services/ClipboardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Bit.Core;
using Android.OS;
using Bit.Core.Abstractions;
using Bit.Droid.Receivers;
using Plugin.CurrentActivity;
Expand All @@ -26,13 +26,41 @@ public ClipboardService(IStateService stateService)
PendingIntentFlags.UpdateCurrent));
}

public async Task CopyTextAsync(string text, int expiresInMs = -1)
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
{
await Clipboard.SetTextAsync(text);
// Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+
if ((int)Build.VERSION.SdkInt < 33)
{
await Clipboard.SetTextAsync(text);
}
else
{
CopyToClipboard(text, isSensitive);
}

await ClearClipboardAlarmAsync(expiresInMs);
}

public bool IsCopyNotificationHandledByPlatform()
{
// Android 13+ provides built-in notification when text is copied to the clipboard
return (int)Build.VERSION.SdkInt >= 33;
}

private void CopyToClipboard(string text, bool isSensitive = true)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var clipboardManager = activity.GetSystemService(
Context.ClipboardService) as Android.Content.ClipboardManager;
var clipData = ClipData.NewPlainText("bitwarden", text);
if (isSensitive)
{
clipData.Description.Extras ??= new PersistableBundle();
clipData.Description.Extras.PutBoolean("android.content.extra.IS_SENSITIVE", true);
}
clipboardManager.PrimaryClip = clipData;
}

private async Task ClearClipboardAlarmAsync(int expiresInMs = -1)
{
var clearMs = expiresInMs;
Expand Down
13 changes: 4 additions & 9 deletions src/Android/Services/DeviceActionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace Bit.Droid.Services
{
public class DeviceActionService : IDeviceActionService
{
private readonly IClipboardService _clipboardService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
Expand All @@ -47,11 +48,13 @@ public class DeviceActionService : IDeviceActionService
private string _userAgent;

public DeviceActionService(
IClipboardService clipboardService,
IStateService stateService,
IMessagingService messagingService,
IBroadcasterService broadcasterService,
Func<IEventService> eventServiceFunc)
{
_clipboardService = clipboardService;
_stateService = stateService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
Expand Down Expand Up @@ -929,20 +932,12 @@ private async Task CopyTotpAsync(CipherView cipher)
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
if (totp != null)
{
CopyToClipboard(totp);
await _clipboardService.CopyTextAsync(totp);
}
}
}
}

private void CopyToClipboard(string text)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var clipboardManager = activity.GetSystemService(
Context.ClipboardService) as Android.Content.ClipboardManager;
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", text);
}

public float GetSystemFontSizeScale()
{
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
Expand Down
3 changes: 1 addition & 2 deletions src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ public async Task ClearAsync()
private async void CopyAsync(GeneratedPasswordHistory ph)
{
await _clipboardService.CopyTextAsync(ph.Password);
_platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}

public async Task UpdateOnThemeChanged()
Expand Down
4 changes: 1 addition & 3 deletions src/App/Pages/Generator/GeneratorPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.Forms;

namespace Bit.App.Pages
{
Expand Down Expand Up @@ -319,8 +318,7 @@ public async Task SliderInputAsync()
public async Task CopyAsync()
{
await _clipboardService.CopyTextAsync(Password);
_platformUtilsService.ShowToast("success", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}

private void LoadFromOptions()
Expand Down
3 changes: 1 addition & 2 deletions src/App/Pages/Vault/PasswordHistoryPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ public async Task InitAsync()
private async void CopyAsync(PasswordHistoryView ph)
{
await _clipboardService.CopyTextAsync(ph.Password);
_platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
_platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
}
}
}
2 changes: 1 addition & 1 deletion src/App/Pages/Vault/ViewPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ private async void CopyAsync(string id, string text = null)
await _clipboardService.CopyTextAsync(text);
if (!string.IsNullOrWhiteSpace(name))
{
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name));
_platformUtilsService.ShowToastForCopiedValue(name);
}
if (id == "LoginPassword")
{
Expand Down
12 changes: 12 additions & 0 deletions src/App/Services/MobilePlatformUtilsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class MobilePlatformUtilsService : IPlatformUtilsService
private const int DialogPromiseExpiration = 600000; // 10 minutes

private readonly IDeviceActionService _deviceActionService;
private readonly IClipboardService _clipboardService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;

Expand All @@ -28,10 +29,12 @@ public class MobilePlatformUtilsService : IPlatformUtilsService

public MobilePlatformUtilsService(
IDeviceActionService deviceActionService,
IClipboardService clipboardService,
IMessagingService messagingService,
IBroadcasterService broadcasterService)
{
_deviceActionService = deviceActionService;
_clipboardService = clipboardService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
}
Expand Down Expand Up @@ -129,6 +132,15 @@ public bool SupportsDuo()
return true;
}

public void ShowToastForCopiedValue(string valueNameCopied)
{
if (!_clipboardService.IsCopyNotificationHandledByPlatform())
{
ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, valueNameCopied));
}
}

public bool SupportsFido2()
{
return _deviceActionService.SupportsFido2();
Expand Down
21 changes: 7 additions & 14 deletions src/App/Utilities/AppHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,14 @@ public static async Task<string> CipherListOptions(ContentPage page, CipherView
else if (selection == AppResources.CopyUsername)
{
await clipboardService.CopyTextAsync(cipher.Login.Username);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Username));
platformUtilsService.ShowToastForCopiedValue(AppResources.Username);
}
else if (selection == AppResources.CopyPassword)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Login.Password);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Password));
platformUtilsService.ShowToastForCopiedValue(AppResources.Password);
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, cipher.Id);
}
}
Expand All @@ -119,8 +117,7 @@ public static async Task<string> CipherListOptions(ContentPage page, CipherView
if (!string.IsNullOrWhiteSpace(totp))
{
await clipboardService.CopyTextAsync(totp);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp);
}
}
}
Expand All @@ -133,25 +130,22 @@ public static async Task<string> CipherListOptions(ContentPage page, CipherView
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Card.Number);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Number));
platformUtilsService.ShowToastForCopiedValue(AppResources.Number);
}
}
else if (selection == AppResources.CopySecurityCode)
{
if (cipher.Reprompt == CipherRepromptType.None || await passwordRepromptService.ShowPasswordPromptAsync())
{
await clipboardService.CopyTextAsync(cipher.Card.Code);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SecurityCode));
platformUtilsService.ShowToastForCopiedValue(AppResources.SecurityCode);
var task = eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, cipher.Id);
}
}
else if (selection == AppResources.CopyNotes)
{
await clipboardService.CopyTextAsync(cipher.Notes);
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.Notes));
platformUtilsService.ShowToastForCopiedValue(AppResources.Notes);
}
return selection;
}
Expand Down Expand Up @@ -262,8 +256,7 @@ public static async Task CopySendUrlAsync(SendView send)
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
await clipboardService.CopyTextAsync(GetSendUrl(send));
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.SendLink));
platformUtilsService.ShowToastForCopiedValue(AppResources.SendLink);
}

public static async Task ShareSendUrlAsync(SendView send)
Expand Down
10 changes: 9 additions & 1 deletion src/Core/Abstractions/IClipboardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ public interface IClipboardService
/// Copies the <paramref name="text"/> to the Clipboard.
/// If <paramref name="expiresInMs"/> is set > 0 then the Clipboard will be cleared after this time in milliseconds.
/// if less than 0 then it takes the configuration that the user set in Options.
/// If <paramref name="isSensitive"/> is true the sensitive flag is passed to the clipdata to obfuscate the
/// clipboard text in the popup (Android 13+ only)
/// </summary>
/// <param name="text">Text to be copied to the Clipboard</param>
/// <param name="expiresInMs">Expiration time in milliseconds of the copied text</param>
Task CopyTextAsync(string text, int expiresInMs = -1);
/// <param name="isSensitive">Flag to mark copied text as sensitive</param>
Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true);

/// <summary>
/// Returns true if the platform provides its own notification when text is copied to the clipboard
/// </summary>
bool IsCopyNotificationHandledByPlatform();
}
}
1 change: 1 addition & 0 deletions src/Core/Abstractions/IPlatformUtilsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Task<bool> ShowDialogAsync(string text, string title = null, string confirmText
Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func<string, Task<bool>> validator);
void ShowToast(string type, string title, string text, Dictionary<string, object> options = null);
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
void ShowToastForCopiedValue(string valueNameCopied);
bool SupportsFido2();
bool SupportsDuo();
Task<bool> SupportsBiometricAsync();
Expand Down
10 changes: 9 additions & 1 deletion src/iOS.Core/Services/ClipboardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ public ClipboardService(IStateService stateService)
_stateService = stateService;
}

public async Task CopyTextAsync(string text, int expiresInMs = -1)
public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true)
{
// isSensitive is only used by Android for now

int clearSeconds = -1;
if (expiresInMs < 0)
{
Expand All @@ -36,5 +38,11 @@ public async Task CopyTextAsync(string text, int expiresInMs = -1)
ExpirationDate = clearSeconds > 0 ? NSDate.FromTimeIntervalSinceNow(clearSeconds) : null
}));
}

public bool IsCopyNotificationHandledByPlatform()
{
// return true for any future versions of iOS that notify the user when text is copied to the clipboard
return false;
}
}
}
4 changes: 2 additions & 2 deletions src/iOS.Core/Utilities/iOSCoreHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ public static void RegisterLocalServices()
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var deviceActionService = new DeviceActionService(stateService, messagingService);
var clipboardService = new ClipboardService(stateService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
var biometricService = new BiometricService(mobileStorageService);
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService);
Expand Down

0 comments on commit 48a8d9a

Please sign in to comment.