[EC-325] Settings option to allow screen capture on Android (#1914)

* settings option to allow screen capture on Android

* Improved code on Screen Capture and added prompt to the user to allow screen capture

* EC-325 Removed async on OnCreate of MainActivity given that's not necessary anymore

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
Pedro da Rocha Pires 2022-07-15 16:13:10 +01:00 committed by GitHub
parent cb0c52fb26
commit cf222bd0c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 5991 additions and 3843 deletions

View file

@ -64,10 +64,11 @@ namespace Bit.Droid
Intent?.Validate(); Intent?.Validate();
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
if (!CoreHelpers.InDebugMode())
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget(_ =>
{ {
Window.AddFlags(Android.Views.WindowManagerFlags.Secure); Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
} });
ServiceContainer.Resolve<ILogger>("logger").InitAsync(); ServiceContainer.Resolve<ILogger>("logger").InitAsync();

View file

@ -948,5 +948,21 @@ namespace Bit.Droid.Services
{ {
// for any Android-specific cleanup required after switching accounts // for any Android-specific cleanup required after switching accounts
} }
public async Task SetScreenCaptureAllowedAsync()
{
if (CoreHelpers.ForceScreenCaptureEnabled())
{
return;
}
var activity = CrossCurrentActivity.Current?.Activity;
if (await _stateService.GetScreenCaptureAllowedAsync())
{
activity.RunOnUiThread(() => activity.Window.ClearFlags(WindowManagerFlags.Secure));
return;
}
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
}
} }
} }

View file

@ -48,5 +48,6 @@ namespace Bit.App.Abstractions
bool SupportsFido2(); bool SupportsFido2();
float GetSystemFontSizeScale(); float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync(); Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
} }
} }

View file

@ -8,7 +8,6 @@ using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms; using Xamarin.Forms;
@ -31,11 +30,13 @@ namespace Bit.App.Pages
private readonly IKeyConnectorService _keyConnectorService; private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService; private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService; private readonly ILogger _loggerService;
private const int CustomVaultTimeoutValue = -100; private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric; private bool _supportsBiometric;
private bool _pin; private bool _pin;
private bool _biometric; private bool _biometric;
private bool _screenCaptureAllowed;
private string _lastSyncDate; private string _lastSyncDate;
private string _vaultTimeoutDisplayValue; private string _vaultTimeoutDisplayValue;
private string _vaultTimeoutActionDisplayValue; private string _vaultTimeoutActionDisplayValue;
@ -122,6 +123,7 @@ namespace Bit.App.Pages
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync(); var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
_pin = pinSet.Item1 || pinSet.Item2; _pin = pinSet.Item1 || pinSet.Item2;
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync(); _biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
_screenCaptureAllowed = await _stateService.GetScreenCaptureAllowedAsync();
if (_vaultTimeoutDisplayValue == null) if (_vaultTimeoutDisplayValue == null)
{ {
@ -547,6 +549,15 @@ namespace Bit.App.Pages
UseFrame = true, UseFrame = true,
}); });
} }
if (Device.RuntimePlatform == Device.Android)
{
securityItems.Add(new SettingsPageListItem
{
Name = AppResources.AllowScreenCapture,
SubLabel = _screenCaptureAllowed ? AppResources.Enabled : AppResources.Disabled,
ExecuteAsync = () => SetScreenCaptureAllowedAsync()
});
}
var accountItems = new List<SettingsPageListItem> var accountItems = new List<SettingsPageListItem>
{ {
new SettingsPageListItem new SettingsPageListItem
@ -709,5 +720,33 @@ namespace Bit.App.Pages
private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option; private string CreateSelectableOption(string option, bool selected) => selected ? $"✓ {option}" : option;
private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}"; private bool CompareSelection(string selection, string compareTo) => selection == compareTo || selection == $"✓ {compareTo}";
public async Task SetScreenCaptureAllowedAsync()
{
if (CoreHelpers.ForceScreenCaptureEnabled())
{
return;
}
try
{
if (!_screenCaptureAllowed
&&
!await Page.DisplayAlert(AppResources.AllowScreenCapture, AppResources.AreYouSureYouWantToEnableScreenCapture, AppResources.Yes, AppResources.No))
{
return;
}
await _stateService.SetScreenCaptureAllowedAsync(!_screenCaptureAllowed);
_screenCaptureAllowed = !_screenCaptureAllowed;
await _deviceActionService.SetScreenCaptureAllowedAsync();
BuildList();
}
catch (Exception ex)
{
_loggerService.Exception(ex);
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok);
}
}
} }
} }

View file

@ -181,6 +181,8 @@ namespace Bit.App.Pages
return; return;
} }
_deviceActionService.SetScreenCaptureAllowedAsync().FireAndForget();
await InitVaultFilterAsync(MainPage); await InitVaultFilterAsync(MainPage);
if (MainPage) if (MainPage)
{ {
@ -296,7 +298,7 @@ namespace Bit.App.Pages
} }
}, AppResources.Trash, _deletedCount, uppercaseGroupNames, false)); }, AppResources.Trash, _deletedCount, uppercaseGroupNames, false));
} }
// TODO: refactor this // TODO: refactor this
if (Device.RuntimePlatform == Device.Android if (Device.RuntimePlatform == Device.Android
|| ||

File diff suppressed because it is too large Load diff

View file

@ -2263,4 +2263,10 @@
<data name="GenericErrorMessage" xml:space="preserve"> <data name="GenericErrorMessage" xml:space="preserve">
<value>We were unable to process your request. Please try again or contact us.</value> <value>We were unable to process your request. Please try again or contact us.</value>
</data> </data>
<data name="AllowScreenCapture" xml:space="preserve">
<value>Allow Screen Capture</value>
</data>
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
<value>Are you sure you want to enable Screen Capture?</value>
</data>
</root> </root>

View file

@ -148,6 +148,8 @@ namespace Bit.Core.Abstractions
Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null); Task SetRefreshTokenAsync(string value, bool skipTokenStorage, string userId = null);
Task<string> GetTwoFactorTokenAsync(string email = null); Task<string> GetTwoFactorTokenAsync(string email = null);
Task SetTwoFactorTokenAsync(string value, string email = null); Task SetTwoFactorTokenAsync(string value, string email = null);
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
Task SaveExtensionActiveUserIdToStorageAsync(string userId); Task SaveExtensionActiveUserIdToStorageAsync(string userId);
} }
} }

View file

@ -94,11 +94,13 @@ namespace Bit.Core.Models.Domain
EnvironmentUrls = copy.EnvironmentUrls; EnvironmentUrls = copy.EnvironmentUrls;
VaultTimeout = copy.VaultTimeout; VaultTimeout = copy.VaultTimeout;
VaultTimeoutAction = copy.VaultTimeoutAction; VaultTimeoutAction = copy.VaultTimeoutAction;
ScreenCaptureAllowed = copy.ScreenCaptureAllowed;
} }
public EnvironmentUrlData EnvironmentUrls; public EnvironmentUrlData EnvironmentUrls;
public int? VaultTimeout; public int? VaultTimeout;
public VaultTimeoutAction? VaultTimeoutAction; public VaultTimeoutAction? VaultTimeoutAction;
public bool ScreenCaptureAllowed;
} }
public class AccountVolatileData public class AccountVolatileData

View file

@ -191,7 +191,7 @@ namespace Bit.Core.Services
EnvironmentUrls = environmentUrls, EnvironmentUrls = environmentUrls,
VaultTimeout = vaultTimeout, VaultTimeout = vaultTimeout,
VaultTimeoutAction = VaultTimeoutAction =
vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock, vaultTimeoutAction == "logout" ? VaultTimeoutAction.Logout : VaultTimeoutAction.Lock
}; };
var state = new State { Accounts = new Dictionary<string, Account> { [userId] = account } }; var state = new State { Accounts = new Dictionary<string, Account> { [userId] = account } };
state.ActiveUserId = userId; state.ActiveUserId = userId;

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
@ -558,6 +559,27 @@ namespace Bit.Core.Services
await SaveAccountAsync(account, reconciledOptions); await SaveAccountAsync(account, reconciledOptions);
} }
public async Task<bool> GetScreenCaptureAllowedAsync(string userId = null)
{
if (CoreHelpers.ForceScreenCaptureEnabled())
{
return true;
}
return (await GetAccountAsync(
ReconcileOptions(new StorageOptions { UserId = userId }, await GetDefaultStorageOptionsAsync())
))?.Settings?.ScreenCaptureAllowed ?? false;
}
public async Task SetScreenCaptureAllowedAsync(bool value, string userId = null)
{
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
await GetDefaultStorageOptionsAsync());
var account = await GetAccountAsync(reconciledOptions);
account.Settings.ScreenCaptureAllowed = value;
await SaveAccountAsync(account, reconciledOptions);
}
public async Task<DateTime?> GetLastFileCacheClearAsync() public async Task<DateTime?> GetLastFileCacheClearAsync()
{ {
var options = await GetDefaultStorageOptionsAsync(); var options = await GetDefaultStorageOptionsAsync();
@ -1461,6 +1483,7 @@ namespace Bit.Core.Services
var existingAccount = state.Accounts[account.Profile.UserId]; var existingAccount = state.Accounts[account.Profile.UserId];
account.Settings.VaultTimeout = existingAccount.Settings.VaultTimeout; account.Settings.VaultTimeout = existingAccount.Settings.VaultTimeout;
account.Settings.VaultTimeoutAction = existingAccount.Settings.VaultTimeoutAction; account.Settings.VaultTimeoutAction = existingAccount.Settings.VaultTimeoutAction;
account.Settings.ScreenCaptureAllowed = existingAccount.Settings.ScreenCaptureAllowed;
} }
// New account defaults // New account defaults

View file

@ -34,6 +34,25 @@ namespace Bit.Core.Utilities
#endif #endif
} }
/// <summary>
/// Returns whether to force enabling the screen capture.
/// On Debug it will allow screen capture by default but this method
/// makes it easier to test the change on enabling/disabling the feature
/// on debug.
/// </summary>
/// <remarks>
/// To test enabling/disabling in DEBUG, just return <c>false</c> in the #if condition
/// and that's it.
/// </remarks>
public static bool ForceScreenCaptureEnabled()
{
#if DEBUG
return true;
#else
return false;
#endif
}
public static string GetHostname(string uriString) public static string GetHostname(string uriString)
{ {
var uri = GetUri(uriString); var uri = GetUri(uriString);

View file

@ -604,6 +604,12 @@ namespace Bit.iOS.Core.Services
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentities();
} }
public Task SetScreenCaptureAllowedAsync()
{
// only used by Android. Not possible in iOS
return Task.CompletedTask;
}
public class PickerDelegate : UIDocumentPickerDelegate public class PickerDelegate : UIDocumentPickerDelegate
{ {
private readonly DeviceActionService _deviceActionService; private readonly DeviceActionService _deviceActionService;