From 48a8d9ae35726a08c05adf99e8cc9ad63772d620 Mon Sep 17 00:00:00 2001 From: mp-bw <59324545+mp-bw@users.noreply.github.com> Date: Fri, 10 Jun 2022 12:02:17 -0400 Subject: [PATCH] Clipboard handling adjustments for Android 13 (#1947) * Android 13 clipboard tweaks * adjustments * adjustments round 2 --- src/Android/MainApplication.cs | 10 +++--- src/Android/Services/ClipboardService.cs | 34 +++++++++++++++++-- src/Android/Services/DeviceActionService.cs | 13 +++---- .../GeneratorHistoryPageViewModel.cs | 3 +- .../Pages/Generator/GeneratorPageViewModel.cs | 4 +-- .../Vault/PasswordHistoryPageViewModel.cs | 3 +- src/App/Pages/Vault/ViewPageViewModel.cs | 2 +- .../Services/MobilePlatformUtilsService.cs | 12 +++++++ src/App/Utilities/AppHelpers.cs | 21 ++++-------- src/Core/Abstractions/IClipboardService.cs | 10 +++++- .../Abstractions/IPlatformUtilsService.cs | 1 + src/iOS.Core/Services/ClipboardService.cs | 10 +++++- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 4 +-- 13 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 4b5e706e3..95ef08b3b 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -12,7 +12,6 @@ using Bit.Core.Abstractions; 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; @@ -134,10 +133,11 @@ namespace Bit.Droid 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("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); @@ -152,7 +152,7 @@ namespace Bit.Droid ServiceContainer.Register("secureStorageService", secureStorageService); ServiceContainer.Register("stateService", stateService); ServiceContainer.Register("stateMigrationService", stateMigrationService); - ServiceContainer.Register("clipboardService", new ClipboardService(stateService)); + ServiceContainer.Register("clipboardService", clipboardService); ServiceContainer.Register("deviceActionService", deviceActionService); ServiceContainer.Register("platformUtilsService", platformUtilsService); ServiceContainer.Register("biometricService", biometricService); diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs index 07e720c57..2abd8df45 100644 --- a/src/Android/Services/ClipboardService.cs +++ b/src/Android/Services/ClipboardService.cs @@ -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; @@ -26,13 +26,41 @@ namespace Bit.Droid.Services 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; diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 37d90d0d4..df873f79c 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -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; @@ -47,11 +48,13 @@ namespace Bit.Droid.Services private string _userAgent; public DeviceActionService( + IClipboardService clipboardService, IStateService stateService, IMessagingService messagingService, IBroadcasterService broadcasterService, Func eventServiceFunc) { + _clipboardService = clipboardService; _stateService = stateService; _messagingService = messagingService; _broadcasterService = broadcasterService; @@ -929,20 +932,12 @@ namespace Bit.Droid.Services 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; diff --git a/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs b/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs index 1d34812ec..687d227d3 100644 --- a/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs +++ b/src/App/Pages/Generator/GeneratorHistoryPageViewModel.cs @@ -58,8 +58,7 @@ namespace Bit.App.Pages 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() diff --git a/src/App/Pages/Generator/GeneratorPageViewModel.cs b/src/App/Pages/Generator/GeneratorPageViewModel.cs index 2332267ed..b0669484e 100644 --- a/src/App/Pages/Generator/GeneratorPageViewModel.cs +++ b/src/App/Pages/Generator/GeneratorPageViewModel.cs @@ -5,7 +5,6 @@ using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Models.Domain; using Bit.Core.Utilities; -using Xamarin.Forms; namespace Bit.App.Pages { @@ -319,8 +318,7 @@ namespace Bit.App.Pages 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() diff --git a/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs b/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs index bc415da2a..6aac41dd2 100644 --- a/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs +++ b/src/App/Pages/Vault/PasswordHistoryPageViewModel.cs @@ -51,8 +51,7 @@ namespace Bit.App.Pages 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); } } } diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index ffced6d18..505fdb00b 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -663,7 +663,7 @@ namespace Bit.App.Pages await _clipboardService.CopyTextAsync(text); if (!string.IsNullOrWhiteSpace(name)) { - _platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name)); + _platformUtilsService.ShowToastForCopiedValue(name); } if (id == "LoginPassword") { diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs index 6b424dc66..8c74d2e5f 100644 --- a/src/App/Services/MobilePlatformUtilsService.cs +++ b/src/App/Services/MobilePlatformUtilsService.cs @@ -20,6 +20,7 @@ namespace Bit.App.Services private const int DialogPromiseExpiration = 600000; // 10 minutes private readonly IDeviceActionService _deviceActionService; + private readonly IClipboardService _clipboardService; private readonly IMessagingService _messagingService; private readonly IBroadcasterService _broadcasterService; @@ -28,10 +29,12 @@ namespace Bit.App.Services public MobilePlatformUtilsService( IDeviceActionService deviceActionService, + IClipboardService clipboardService, IMessagingService messagingService, IBroadcasterService broadcasterService) { _deviceActionService = deviceActionService; + _clipboardService = clipboardService; _messagingService = messagingService; _broadcasterService = broadcasterService; } @@ -129,6 +132,15 @@ namespace Bit.App.Services return true; } + public void ShowToastForCopiedValue(string valueNameCopied) + { + if (!_clipboardService.IsCopyNotificationHandledByPlatform()) + { + ShowToast("info", null, + string.Format(AppResources.ValueHasBeenCopied, valueNameCopied)); + } + } + public bool SupportsFido2() { return _deviceActionService.SupportsFido2(); diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index 454d77021..fd61c4320 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -97,16 +97,14 @@ namespace Bit.App.Utilities 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); } } @@ -119,8 +117,7 @@ namespace Bit.App.Utilities if (!string.IsNullOrWhiteSpace(totp)) { await clipboardService.CopyTextAsync(totp); - platformUtilsService.ShowToast("info", null, - string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp)); + platformUtilsService.ShowToastForCopiedValue(AppResources.VerificationCodeTotp); } } } @@ -133,8 +130,7 @@ namespace Bit.App.Utilities 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) @@ -142,16 +138,14 @@ namespace Bit.App.Utilities 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; } @@ -262,8 +256,7 @@ namespace Bit.App.Utilities var platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); var clipboardService = ServiceContainer.Resolve("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) diff --git a/src/Core/Abstractions/IClipboardService.cs b/src/Core/Abstractions/IClipboardService.cs index 43b844433..f5a7e4699 100644 --- a/src/Core/Abstractions/IClipboardService.cs +++ b/src/Core/Abstractions/IClipboardService.cs @@ -8,9 +8,17 @@ namespace Bit.Core.Abstractions /// Copies the to the Clipboard. /// If 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 is true the sensitive flag is passed to the clipdata to obfuscate the + /// clipboard text in the popup (Android 13+ only) /// /// Text to be copied to the Clipboard /// Expiration time in milliseconds of the copied text - Task CopyTextAsync(string text, int expiresInMs = -1); + /// Flag to mark copied text as sensitive + Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true); + + /// + /// Returns true if the platform provides its own notification when text is copied to the clipboard + /// + bool IsCopyNotificationHandledByPlatform(); } } diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs index ec5b2894f..e3c73d5ca 100644 --- a/src/Core/Abstractions/IPlatformUtilsService.cs +++ b/src/Core/Abstractions/IPlatformUtilsService.cs @@ -23,6 +23,7 @@ namespace Bit.Core.Abstractions Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func> validator); void ShowToast(string type, string title, string text, Dictionary options = null); void ShowToast(string type, string title, string[] text, Dictionary options = null); + void ShowToastForCopiedValue(string valueNameCopied); bool SupportsFido2(); bool SupportsDuo(); Task SupportsBiometricAsync(); diff --git a/src/iOS.Core/Services/ClipboardService.cs b/src/iOS.Core/Services/ClipboardService.cs index 67f7743ac..1b68b7f1c 100644 --- a/src/iOS.Core/Services/ClipboardService.cs +++ b/src/iOS.Core/Services/ClipboardService.cs @@ -16,8 +16,10 @@ namespace Bit.iOS.Core.Services _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) { @@ -36,5 +38,11 @@ namespace Bit.iOS.Core.Services 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; + } } } diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 29cd91d67..c43a1e180 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -64,8 +64,8 @@ namespace Bit.iOS.Core.Utilities 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);