diff --git a/src/Android/Accessibility/AccessibilityActivity.cs b/src/Android/Accessibility/AccessibilityActivity.cs index 5d52e0ef8..f6ea4aedf 100644 --- a/src/Android/Accessibility/AccessibilityActivity.cs +++ b/src/Android/Accessibility/AccessibilityActivity.cs @@ -4,6 +4,8 @@ using Android.OS; using Android.Runtime; using Android.Views; using System; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; namespace Bit.Droid.Accessibility { @@ -16,13 +18,13 @@ namespace Bit.Droid.Accessibility protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); - LaunchMainActivity(Intent, 932473); + HandleIntent(Intent, 932473); } protected override void OnNewIntent(Intent intent) { base.OnNewIntent(intent); - LaunchMainActivity(intent, 489729); + HandleIntent(intent, 489729); } protected override void OnDestroy() @@ -78,6 +80,21 @@ namespace Bit.Droid.Accessibility Finish(); } + private void HandleIntent(Intent callingIntent, int requestCode) + { + if(callingIntent?.GetBooleanExtra("autofillTileClicked", false) ?? false) + { + Intent.RemoveExtra("autofillTileClicked"); + var messagingService = ServiceContainer.Resolve("messagingService"); + messagingService.Send("OnAutofillTileClick"); + Finish(); + } + else + { + LaunchMainActivity(callingIntent, requestCode); + } + } + private void LaunchMainActivity(Intent callingIntent, int requestCode) { _lastQueriedUri = callingIntent?.GetStringExtra("uri"); diff --git a/src/Android/Accessibility/AccessibilityHelpers.cs b/src/Android/Accessibility/AccessibilityHelpers.cs index 62f4f593a..c4639059e 100644 --- a/src/Android/Accessibility/AccessibilityHelpers.cs +++ b/src/Android/Accessibility/AccessibilityHelpers.cs @@ -20,6 +20,8 @@ namespace Bit.Droid.Accessibility public static Credentials LastCredentials = null; public static string SystemUiPackage = "com.android.systemui"; public static string BitwardenTag = "bw_access"; + public static bool IsAutofillTileAdded = false; + public static bool IsAccessibilityBroadcastReady = false; public static Dictionary SupportedBrowsers => new List { diff --git a/src/Android/Accessibility/AccessibilityService.cs b/src/Android/Accessibility/AccessibilityService.cs index 73ae308f4..498bf767c 100644 --- a/src/Android/Accessibility/AccessibilityService.cs +++ b/src/Android/Accessibility/AccessibilityService.cs @@ -29,6 +29,7 @@ namespace Bit.Droid.Accessibility private const string BitwardenWebsite = "vault.bitwarden.com"; private IStorageService _storageService; + private IBroadcasterService _broadcasterService; private DateTime? _lastSettingsReload = null; private TimeSpan _settingsReloadSpan = TimeSpan.FromMinutes(1); private HashSet _blacklistedUris; @@ -48,6 +49,28 @@ namespace Bit.Droid.Accessibility private DateTime? _lastLauncherSetBuilt = null; private TimeSpan _rebuildLauncherSpan = TimeSpan.FromHours(1); + public override void OnCreate() + { + base.OnCreate(); + LoadServices(); + var settingsTask = LoadSettingsAsync(); + _broadcasterService.Subscribe(nameof(AccessibilityService), (message) => + { + if(message.Command == "OnAutofillTileClick") + { + var runnable = new Java.Lang.Runnable(OnAutofillTileClick); + _handler.PostDelayed(runnable, 250); + } + }); + AccessibilityHelpers.IsAccessibilityBroadcastReady = true; + } + + public override void OnDestroy() + { + AccessibilityHelpers.IsAccessibilityBroadcastReady = false; + _broadcasterService.Unsubscribe(nameof(AccessibilityService)); + } + public override void OnAccessibilityEvent(AccessibilityEvent e) { try @@ -174,6 +197,29 @@ namespace Bit.Droid.Accessibility passwordNodes.Dispose(); return filled; } + + private void OnAutofillTileClick() + { + CancelOverlayPrompt(); + + var root = RootInActiveWindow; + if(root != null && root.PackageName != BitwardenPackage && + root.PackageName != AccessibilityHelpers.SystemUiPackage && + !SkipPackage(root.PackageName)) + { + var uri = AccessibilityHelpers.GetUri(root); + if(!string.IsNullOrWhiteSpace(uri)) + { + var intent = new Intent(this, typeof(AccessibilityActivity)); + intent.PutExtra("uri", uri); + intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); + StartActivity(intent); + return; + } + } + + Toast.MakeText(this, AppResources.AutofillTileUriNotFound, ToastLength.Long).Show(); + } private void CancelOverlayPrompt() { @@ -181,8 +227,12 @@ namespace Bit.Droid.Accessibility if(_windowManager != null && _overlayView != null) { - _windowManager.RemoveViewImmediate(_overlayView); - System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Removed"); + try + { + _windowManager.RemoveViewImmediate(_overlayView); + System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Removed"); + } + catch { } } _overlayView = null; @@ -201,8 +251,14 @@ namespace Bit.Droid.Accessibility { if(!AccessibilityHelpers.OverlayPermitted()) { - System.Diagnostics.Debug.WriteLine(">>> Overlay Permission not granted"); - Toast.MakeText(this, AppResources.AccessibilityOverlayPermissionAlert, ToastLength.Long).Show(); + if(!AccessibilityHelpers.IsAutofillTileAdded) + { + // The user has the option of only using the autofill tile and leaving the overlay permission + // disabled, so only show this toast if they're using accessibility without overlay permission and + // have _not_ added the autofill tile + System.Diagnostics.Debug.WriteLine(">>> Overlay Permission not granted"); + Toast.MakeText(this, AppResources.AccessibilityOverlayPermissionAlert, ToastLength.Long).Show(); + } return; } @@ -392,6 +448,10 @@ namespace Bit.Droid.Accessibility { _storageService = ServiceContainer.Resolve("storageService"); } + if(_broadcasterService == null) + { + _broadcasterService = ServiceContainer.Resolve("broadcasterService"); + } } private async Task LoadSettingsAsync() @@ -405,6 +465,8 @@ namespace Bit.Droid.Accessibility { _blacklistedUris = new HashSet(uris); } + var isAutoFillTileAdded = await _storageService.GetAsync(Constants.AutofillTileAdded); + AccessibilityHelpers.IsAutofillTileAdded = isAutoFillTileAdded.GetValueOrDefault(); } } } diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index dc18ec1ac..b2dec6937 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -136,6 +136,7 @@ + diff --git a/src/Android/Resources/values/strings.xml b/src/Android/Resources/values/strings.xml index ec96d918f..53c6195b6 100644 --- a/src/Android/Resources/values/strings.xml +++ b/src/Android/Resources/values/strings.xml @@ -16,6 +16,9 @@ Password Generator + + Scan & Fill + Self-hosted server URL diff --git a/src/Android/Tiles/AutofillTileService.cs b/src/Android/Tiles/AutofillTileService.cs new file mode 100644 index 000000000..39f75c07a --- /dev/null +++ b/src/Android/Tiles/AutofillTileService.cs @@ -0,0 +1,95 @@ +using Android; +using Android.App; +using Android.Content; +using Android.Runtime; +using Android.Service.QuickSettings; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Utilities; +using Bit.Droid.Accessibility; +using Java.Lang; + +namespace Bit.Droid.Tile +{ + [Service(Permission = Manifest.Permission.BindQuickSettingsTile, Label = "@string/ScanAndFill", + Icon = "@drawable/shield")] + [IntentFilter(new string[] { ActionQsTile })] + [Register("com.x8bit.bitwarden.AutofillTileService")] + public class AutofillTileService : TileService + { + private IStorageService _storageService; + + public override void OnTileAdded() + { + base.OnTileAdded(); + SetTileAdded(true); + } + + public override void OnStartListening() + { + base.OnStartListening(); + } + + public override void OnStopListening() + { + base.OnStopListening(); + } + + public override void OnTileRemoved() + { + base.OnTileRemoved(); + SetTileAdded(false); + } + + public override void OnClick() + { + base.OnClick(); + + if(IsLocked) + { + UnlockAndRun(new Runnable(ScanAndFill)); + } + else + { + ScanAndFill(); + } + } + + private void SetTileAdded(bool isAdded) + { + AccessibilityHelpers.IsAutofillTileAdded = isAdded; + if(_storageService == null) + { + _storageService = ServiceContainer.Resolve("storageService"); + } + _storageService.SaveAsync(Constants.AutofillTileAdded, isAdded); + } + + private void ScanAndFill() + { + if(!AccessibilityHelpers.IsAccessibilityBroadcastReady) + { + ShowConfigErrorDialog(); + return; + } + + var intent = new Intent(this, typeof(AccessibilityActivity)); + intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); + intent.PutExtra("autofillTileClicked", true); + StartActivityAndCollapse(intent); + } + + private void ShowConfigErrorDialog() + { + var alertBuilder = new AlertDialog.Builder(this); + alertBuilder.SetMessage(AppResources.AutofillTileAccessibilityRequired); + alertBuilder.SetCancelable(true); + alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) => + { + (sender as AlertDialog)?.Cancel(); + }); + ShowDialog(alertBuilder.Create()); + } + } +} diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 1cb7ef024..371fbc0f5 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2859,5 +2859,17 @@ namespace Bit.App.Resources { return ResourceManager.GetString("SaveAttachmentSuccess", resourceCulture); } } + + public static string AutofillTileAccessibilityRequired { + get { + return ResourceManager.GetString("AutofillTileAccessibilityRequired", resourceCulture); + } + } + + public static string AutofillTileUriNotFound { + get { + return ResourceManager.GetString("AutofillTileUriNotFound", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index c95d3e8cb..08f25c932 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1625,4 +1625,10 @@ Attachment saved successfully + + Please enable "Auto-fill Accessibility Service" from Bitwarden Settings to use the Scan & Fill tile. + + + No password fields detected + \ No newline at end of file diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 193c74cce..4d18f871d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -15,6 +15,7 @@ public static string LastFileCacheClearKey = "lastFileCacheClear"; public static string AutofillDisableSavePromptKey = "autofillDisableSavePrompt"; public static string AutofillBlacklistedUrisKey = "autofillBlacklistedUris"; + public static string AutofillTileAdded = "autofillTileAdded"; public static string DisableFaviconKey = "disableFavicon"; public static string PushRegisteredTokenKey = "pushRegisteredToken"; public static string PushCurrentTokenKey = "pushCurrentToken";