mirror of
https://github.com/bitwarden/mobile
synced 2025-12-10 13:23:39 +00:00
Accessibility fixes (#709)
* Show/hide accessibility overlay on scroll based on several visibility factors * Improvements to accessibility overlay anchor view tracking * Increase recursion limit and check for null children when walking the node tree * Cleanup * Hide overlay when expanding status (notification) bar * use .Any() instead of .Count()
This commit is contained in:
committed by
Kyle Spearrin
parent
c2e34a8b0e
commit
34e32403b0
@@ -15,6 +15,7 @@ using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Java.Util;
|
||||
|
||||
namespace Bit.Droid.Accessibility
|
||||
{
|
||||
@@ -27,17 +28,19 @@ namespace Bit.Droid.Accessibility
|
||||
private const string BitwardenPackage = "com.x8bit.bitwarden";
|
||||
private const string BitwardenWebsite = "vault.bitwarden.com";
|
||||
|
||||
private string _lastNotificationUri = null;
|
||||
private AccessibilityNodeInfo _anchorNode = null;
|
||||
private int _lastAnchorX, _lastAnchorY = 0;
|
||||
private static bool _overlayAnchorObserverRunning = false;
|
||||
private IWindowManager _windowManager = null;
|
||||
private LinearLayout _overlayView = null;
|
||||
private long _lastAutoFillTime = 0;
|
||||
private Java.Lang.Runnable _overlayAnchorObserverRunnable = null;
|
||||
private Handler _handler = new Handler(Looper.MainLooper);
|
||||
|
||||
private HashSet<string> _launcherPackageNames = null;
|
||||
private DateTime? _lastLauncherSetBuilt = null;
|
||||
private TimeSpan _rebuildLauncherSpan = TimeSpan.FromHours(1);
|
||||
|
||||
private IWindowManager _windowManager = null;
|
||||
private LinearLayout _overlayView = null;
|
||||
private int _anchorViewHash = 0;
|
||||
private int _lastAnchorX, _lastAnchorY = 0;
|
||||
|
||||
public override void OnAccessibilityEvent(AccessibilityEvent e)
|
||||
{
|
||||
try
|
||||
@@ -54,106 +57,95 @@ namespace Bit.Droid.Accessibility
|
||||
|
||||
if(SkipPackage(e?.PackageName))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
var root = RootInActiveWindow;
|
||||
if(root == null || root.PackageName != e.PackageName)
|
||||
{
|
||||
if(e?.PackageName != "com.android.systemui")
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// AccessibilityHelpers.PrintTestData(root, e);
|
||||
|
||||
AccessibilityNodeInfo root = null;
|
||||
|
||||
switch(e.EventType)
|
||||
{
|
||||
case EventTypes.ViewFocused:
|
||||
case EventTypes.ViewClicked:
|
||||
case EventTypes.ViewScrolled:
|
||||
var isKnownBroswer = AccessibilityHelpers.SupportedBrowsers.ContainsKey(root.PackageName);
|
||||
if(e.EventType == EventTypes.ViewClicked && isKnownBroswer)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if(e.Source == null || e.PackageName == BitwardenPackage)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
if(e.EventType == EventTypes.ViewScrolled)
|
||||
|
||||
root = RootInActiveWindow;
|
||||
if(root == null || root.PackageName != e.PackageName)
|
||||
{
|
||||
AdjustOverlayForScroll(root, e);
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
var isKnownBroswer = AccessibilityHelpers.SupportedBrowsers.ContainsKey(root.PackageName);
|
||||
if(e.EventType == EventTypes.ViewClicked && isKnownBroswer)
|
||||
{
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
if(!(e.Source?.Password ?? false) && !AccessibilityHelpers.IsUsernameEditText(root, e))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
if(ScanAndAutofill(root, e))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
e.Recycle();
|
||||
}
|
||||
else
|
||||
{
|
||||
var isUsernameEditText1 = AccessibilityHelpers.IsUsernameEditText(root, e);
|
||||
var isPasswordEditText1 = e.Source?.Password ?? false;
|
||||
if(!isUsernameEditText1 && !isPasswordEditText1)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
break;
|
||||
}
|
||||
if(ScanAndAutofill(root, e))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
else
|
||||
{
|
||||
OverlayPromptToAutofill(root, e);
|
||||
}
|
||||
OverlayPromptToAutofill(root, e);
|
||||
}
|
||||
break;
|
||||
case EventTypes.WindowContentChanged:
|
||||
case EventTypes.WindowStateChanged:
|
||||
var isUsernameEditText2 = AccessibilityHelpers.IsUsernameEditText(root, e);
|
||||
var isPasswordEditText2 = e.Source?.Password ?? false;
|
||||
if(e.Source == null || isUsernameEditText2 || isPasswordEditText2)
|
||||
if(AccessibilityHelpers.LastCredentials == null)
|
||||
{
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
else if(AccessibilityHelpers.LastCredentials == null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(_lastNotificationUri))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
break;
|
||||
}
|
||||
var uri = AccessibilityHelpers.GetUri(root);
|
||||
if(uri != null && uri != _lastNotificationUri)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
else if(uri != null && uri.StartsWith(Constants.AndroidAppProtocol))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if(e.PackageName == BitwardenPackage)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
|
||||
root = RootInActiveWindow;
|
||||
if(root == null || root.PackageName != e.PackageName)
|
||||
{
|
||||
e.Recycle();
|
||||
break;
|
||||
}
|
||||
if(ScanAndAutofill(root, e))
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
e.Recycle();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
root.Dispose();
|
||||
e.Dispose();
|
||||
if(root != null)
|
||||
{
|
||||
root.Recycle();
|
||||
}
|
||||
}
|
||||
// Suppress exceptions so that service doesn't crash.
|
||||
catch(Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> Exception: " + ex.StackTrace);
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,8 +167,8 @@ namespace Bit.Droid.Accessibility
|
||||
{
|
||||
AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes);
|
||||
filled = true;
|
||||
_lastAutoFillTime = Java.Lang.JavaSystem.CurrentTimeMillis();
|
||||
}
|
||||
|
||||
}
|
||||
AccessibilityHelpers.LastCredentials = null;
|
||||
}
|
||||
@@ -194,19 +186,23 @@ namespace Bit.Droid.Accessibility
|
||||
|
||||
private void CancelOverlayPrompt()
|
||||
{
|
||||
if(_windowManager == null || _overlayView == null)
|
||||
_overlayAnchorObserverRunning = false;
|
||||
|
||||
if(_windowManager != null && _overlayView != null)
|
||||
{
|
||||
return;
|
||||
_windowManager.RemoveViewImmediate(_overlayView);
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Removed");
|
||||
}
|
||||
|
||||
_windowManager.RemoveViewImmediate(_overlayView);
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Removed");
|
||||
|
||||
_overlayView = null;
|
||||
_anchorViewHash = 0;
|
||||
_lastNotificationUri = null;
|
||||
_lastAnchorX = 0;
|
||||
_lastAnchorY = 0;
|
||||
|
||||
if(_anchorNode != null)
|
||||
{
|
||||
_anchorNode.Recycle();
|
||||
_anchorNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OverlayPromptToAutofill(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
@@ -215,12 +211,25 @@ namespace Bit.Droid.Accessibility
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> Overlay Permission not granted");
|
||||
Toast.MakeText(this, AppResources.AccessibilityOverlayPermissionAlert, ToastLength.Long).Show();
|
||||
e.Recycle();
|
||||
return;
|
||||
}
|
||||
|
||||
if(_overlayView != null || _anchorNode != null || _overlayAnchorObserverRunning)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
}
|
||||
|
||||
if(Java.Lang.JavaSystem.CurrentTimeMillis() - _lastAutoFillTime < 1000)
|
||||
{
|
||||
e.Recycle();
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = AccessibilityHelpers.GetUri(root);
|
||||
if(string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
e.Recycle();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,54 +243,85 @@ namespace Bit.Droid.Accessibility
|
||||
_windowManager = GetSystemService(WindowService).JavaCast<IWindowManager>();
|
||||
}
|
||||
|
||||
if(_overlayView == null)
|
||||
var intent = new Intent(this, typeof(AccessibilityActivity));
|
||||
intent.PutExtra("uri", uri);
|
||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
|
||||
_overlayView = AccessibilityHelpers.GetOverlayView(this);
|
||||
_overlayView.Click += (sender, eventArgs) =>
|
||||
{
|
||||
var intent = new Intent(this, typeof(AccessibilityActivity));
|
||||
intent.PutExtra("uri", uri);
|
||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
|
||||
CancelOverlayPrompt();
|
||||
StartActivity(intent);
|
||||
};
|
||||
|
||||
_overlayView = AccessibilityHelpers.GetOverlayView(this);
|
||||
_overlayView.Click += (sender, eventArgs) =>
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
StartActivity(intent);
|
||||
};
|
||||
|
||||
_lastNotificationUri = uri;
|
||||
|
||||
_windowManager.AddView(_overlayView, layoutParams);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Added at X:{0} Y:{1}",
|
||||
layoutParams.X, layoutParams.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
_windowManager.UpdateViewLayout(_overlayView, layoutParams);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Updated to X:{0} Y:{1}",
|
||||
layoutParams.X, layoutParams.Y);
|
||||
}
|
||||
|
||||
_anchorViewHash = e.Source.GetHashCode();
|
||||
_anchorNode = e.Source;
|
||||
_lastAnchorX = anchorPosition.X;
|
||||
_lastAnchorY = anchorPosition.Y;
|
||||
|
||||
_windowManager.AddView(_overlayView, layoutParams);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Added at X:{0} Y:{1}",
|
||||
layoutParams.X, layoutParams.Y);
|
||||
|
||||
StartOverlayAnchorObserver();
|
||||
}
|
||||
|
||||
private void AdjustOverlayForScroll(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
private void StartOverlayAnchorObserver()
|
||||
{
|
||||
if(_overlayView == null || _anchorViewHash <= 0)
|
||||
if(_overlayAnchorObserverRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_overlayAnchorObserverRunning = true;
|
||||
|
||||
_overlayAnchorObserverRunnable = new Java.Lang.Runnable(() =>
|
||||
{
|
||||
if(_overlayAnchorObserverRunning)
|
||||
{
|
||||
AdjustOverlayForScroll();
|
||||
_handler.PostDelayed(_overlayAnchorObserverRunnable, 250);
|
||||
}
|
||||
});
|
||||
|
||||
_handler.PostDelayed(_overlayAnchorObserverRunnable, 250);
|
||||
}
|
||||
|
||||
private void AdjustOverlayForScroll()
|
||||
{
|
||||
if(_overlayView == null || _anchorNode == null)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
var root = RootInActiveWindow;
|
||||
IEnumerable<AccessibilityWindowInfo> windows = null;
|
||||
if(Build.VERSION.SdkInt > BuildVersionCodes.Kitkat)
|
||||
{
|
||||
windows = Windows;
|
||||
}
|
||||
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(_anchorNode, root, windows);
|
||||
root.Recycle();
|
||||
|
||||
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(_anchorViewHash, root, e);
|
||||
if(anchorPosition == null)
|
||||
{
|
||||
CancelOverlayPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
if(anchorPosition.X == _lastAnchorX && anchorPosition.Y == _lastAnchorY)
|
||||
else if(anchorPosition.X == -1 && anchorPosition.Y == -1)
|
||||
{
|
||||
if(_overlayView.Visibility != ViewStates.Gone)
|
||||
{
|
||||
_overlayView.Visibility = ViewStates.Gone;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if(anchorPosition.X == _lastAnchorX && anchorPosition.Y == _lastAnchorY)
|
||||
{
|
||||
if(_overlayView.Visibility != ViewStates.Visible)
|
||||
{
|
||||
_overlayView.Visibility = ViewStates.Visible;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -289,11 +329,16 @@ namespace Bit.Droid.Accessibility
|
||||
layoutParams.X = anchorPosition.X;
|
||||
layoutParams.Y = anchorPosition.Y;
|
||||
|
||||
_windowManager.UpdateViewLayout(_overlayView, layoutParams);
|
||||
|
||||
_lastAnchorX = anchorPosition.X;
|
||||
_lastAnchorY = anchorPosition.Y;
|
||||
|
||||
_windowManager.UpdateViewLayout(_overlayView, layoutParams);
|
||||
|
||||
if(_overlayView.Visibility != ViewStates.Visible)
|
||||
{
|
||||
_overlayView.Visibility = ViewStates.Visible;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Updated to X:{0} Y:{1}",
|
||||
layoutParams.X, layoutParams.Y);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user