1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-27 12:26:31 +01:00

Replaced accessibility service notification with in-line overlay. (#695)

* Replaced accessibility service notification with in-line overlay.  Requires draw-over permission to be enabled (will prompt if not, though this will be enhanced in subsequent commits)

* Updated with requested changes

* Fix for FDroid build
This commit is contained in:
Matt Portune 2020-01-09 12:17:16 -05:00 committed by Kyle Spearrin
parent c33728d418
commit 21c7b486ff
6 changed files with 213 additions and 130 deletions

View File

@ -1,9 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Android.Content;
using Android.Content.Res;
using Android.Graphics;
using Android.OS; using Android.OS;
using Android.Provider;
using Android.Views;
using Android.Views.Accessibility; using Android.Views.Accessibility;
using Android.Widget;
using Bit.App.Resources;
using Bit.Core; using Bit.Core;
using Plugin.CurrentActivity;
namespace Bit.Droid.Accessibility namespace Bit.Droid.Accessibility
{ {
@ -260,5 +268,88 @@ namespace Bit.Droid.Accessibility
{ {
return allEditTexts.TakeWhile(n => !n.Password).LastOrDefault(); return allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
} }
public static Boolean OverlayPermitted(Context context)
{
if(Build.VERSION.SdkInt >= BuildVersionCodes.M)
{
return Settings.CanDrawOverlays(context.ApplicationContext);
}
else
{
// TODO do older android versions require a check?
return true;
}
}
public static Boolean OpenOverlaySettings(Context context, string packageName)
{
try
{
var intent = new Intent(Settings.ActionManageOverlayPermission);
intent.SetPackage(packageName);
intent.SetFlags(ActivityFlags.NewTask);
context.StartActivity(intent);
return true;
}
catch
{
return false;
}
}
public static LinearLayout GetOverlayView(Context context)
{
var inflater = (LayoutInflater)context.GetSystemService(Context.LayoutInflaterService);
var view = (LinearLayout)inflater.Inflate(Resource.Layout.autofill_listitem, null);
var text1 = (TextView)view.FindViewById(Resource.Id.text1);
var text2 = (TextView)view.FindViewById(Resource.Id.text2);
var icon = (ImageView)view.FindViewById(Resource.Id.icon);
text1.Text = AppResources.AutofillWithBitwarden;
text2.Text = AppResources.GoToMyVault;
icon.SetImageResource(Resource.Drawable.icon);
return view;
}
public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo root, AccessibilityEvent e)
{
var rootRect = new Rect();
root.GetBoundsInScreen(rootRect);
var rootRectHeight = rootRect.Height();
var eSrcRect = new Rect();
e.Source.GetBoundsInScreen(eSrcRect);
var eSrcRectLeft = eSrcRect.Left;
var eSrcRectTop = eSrcRect.Top;
var navBarHeight = GetNavigationBarHeight();
var calculatedTop = rootRectHeight - eSrcRectTop - navBarHeight;
return new Point(eSrcRectLeft, calculatedTop);
}
private static int GetStatusBarHeight()
{
return GetSystemResourceDimenPx("status_bar_height");
}
private static int GetNavigationBarHeight()
{
return GetSystemResourceDimenPx("navigation_bar_height");
}
private static int GetSystemResourceDimenPx(String resName)
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var barHeight = 0;
var resourceId = activity.Resources.GetIdentifier(resName, "dimen", "android");
if(resourceId > 0)
{
barHeight = activity.Resources.GetDimensionPixelSize(resourceId);
}
return barHeight;
}
} }
} }

View File

@ -1,12 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Graphics;
using Android.OS; using Android.OS;
using Android.Provider;
using Android.Runtime; using Android.Runtime;
using Android.Views;
using Android.Views.Accessibility; using Android.Views.Accessibility;
using Android.Widget;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
@ -20,23 +24,18 @@ namespace Bit.Droid.Accessibility
[Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")] [Register("com.x8bit.bitwarden.Accessibility.AccessibilityService")]
public class AccessibilityService : Android.AccessibilityServices.AccessibilityService public class AccessibilityService : Android.AccessibilityServices.AccessibilityService
{ {
private NotificationChannel _notificationChannel;
private const int AutoFillNotificationId = 34573;
private const string BitwardenPackage = "com.x8bit.bitwarden"; private const string BitwardenPackage = "com.x8bit.bitwarden";
private const string BitwardenWebsite = "vault.bitwarden.com"; private const string BitwardenWebsite = "vault.bitwarden.com";
private IStorageService _storageService;
private bool _settingAutofillPasswordField;
private bool _settingAutofillPersistNotification;
private DateTime? _lastSettingsReload = null;
private TimeSpan _settingsReloadSpan = TimeSpan.FromMinutes(1);
private long _lastNotificationTime = 0;
private string _lastNotificationUri = null; private string _lastNotificationUri = null;
private HashSet<string> _launcherPackageNames = null; private HashSet<string> _launcherPackageNames = null;
private DateTime? _lastLauncherSetBuilt = null; private DateTime? _lastLauncherSetBuilt = null;
private TimeSpan _rebuildLauncherSpan = TimeSpan.FromHours(1); private TimeSpan _rebuildLauncherSpan = TimeSpan.FromHours(1);
private IWindowManager _windowManager = null;
private LinearLayout _overlayView = null;
public override void OnAccessibilityEvent(AccessibilityEvent e) public override void OnAccessibilityEvent(AccessibilityEvent e)
{ {
try try
@ -53,6 +52,7 @@ namespace Bit.Droid.Accessibility
if(SkipPackage(e?.PackageName)) if(SkipPackage(e?.PackageName))
{ {
CancelOverlayPrompt();
return; return;
} }
@ -62,107 +62,85 @@ namespace Bit.Droid.Accessibility
return; return;
} }
// AccessibilityHelpers.PrintTestData(root, e); var isKnownBroswer = AccessibilityHelpers.SupportedBrowsers.ContainsKey(root.PackageName);
LoadServices();
var settingsTask = LoadSettingsAsync();
var notificationManager = GetSystemService(NotificationService) as NotificationManager; // AccessibilityHelpers.PrintTestData(root, e);
var cancelNotification = true;
switch(e.EventType) switch(e.EventType)
{ {
case EventTypes.ViewFocused: case EventTypes.ViewFocused:
if(e.Source == null || !e.Source.Password || !_settingAutofillPasswordField) case EventTypes.ViewClicked:
if (e.EventType == EventTypes.ViewClicked && isKnownBroswer)
{ {
break; break;
} }
if (e.Source == null || !e.Source.Password)
{
CancelOverlayPrompt();
break;
}
if(e.PackageName == BitwardenPackage) if(e.PackageName == BitwardenPackage)
{ {
CancelNotification(notificationManager); CancelOverlayPrompt();
break; break;
} }
if(ScanAndAutofill(root, e, notificationManager, cancelNotification)) if(ScanAndAutofill(root, e))
{ {
CancelNotification(notificationManager); CancelOverlayPrompt();
}
else
{
OverlayPromptToAutofill(root, e);
} }
break; break;
case EventTypes.WindowContentChanged: case EventTypes.WindowContentChanged:
case EventTypes.WindowStateChanged: case EventTypes.WindowStateChanged:
if(_settingAutofillPasswordField && e.Source.Password) if (e.Source == null || e.Source.Password)
{ {
break; break;
} }
else if(_settingAutofillPasswordField && AccessibilityHelpers.LastCredentials == null) else if(AccessibilityHelpers.LastCredentials == null)
{ {
if(string.IsNullOrWhiteSpace(_lastNotificationUri)) if(string.IsNullOrWhiteSpace(_lastNotificationUri))
{ {
CancelNotification(notificationManager); CancelOverlayPrompt();
break; break;
} }
var uri = AccessibilityHelpers.GetUri(root); var uri = AccessibilityHelpers.GetUri(root);
if(uri != null && uri != _lastNotificationUri) if(uri != null && uri != _lastNotificationUri)
{ {
CancelNotification(notificationManager); CancelOverlayPrompt();
} }
else if(uri != null && uri.StartsWith(Constants.AndroidAppProtocol)) else if(uri != null && uri.StartsWith(Constants.AndroidAppProtocol))
{ {
CancelNotification(notificationManager, 30000); CancelOverlayPrompt();
} }
break; break;
} }
if(e.PackageName == BitwardenPackage) if(e.PackageName == BitwardenPackage)
{ {
CancelNotification(notificationManager); CancelOverlayPrompt();
break; break;
} }
if(_settingAutofillPersistNotification) if(ScanAndAutofill(root, e))
{ {
var uri = AccessibilityHelpers.GetUri(root); CancelOverlayPrompt();
if(uri != null && !uri.Contains(BitwardenWebsite))
{
var needToFill = AccessibilityHelpers.NeedToAutofill(
AccessibilityHelpers.LastCredentials, uri);
if(needToFill)
{
var passwordNodes = AccessibilityHelpers.GetWindowNodes(root, e,
n => n.Password, false);
needToFill = passwordNodes.Any();
if(needToFill)
{
AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes);
}
passwordNodes.Dispose();
}
if(!needToFill)
{
NotifyToAutofill(uri, notificationManager);
cancelNotification = false;
}
}
AccessibilityHelpers.LastCredentials = null;
}
else
{
cancelNotification = ScanAndAutofill(root, e, notificationManager, cancelNotification);
}
if(cancelNotification)
{
CancelNotification(notificationManager);
} }
break; break;
default: default:
break; break;
} }
notificationManager?.Dispose();
root.Dispose(); root.Dispose();
e.Dispose(); e.Dispose();
} }
// Suppress exceptions so that service doesn't crash. // Suppress exceptions so that service doesn't crash.
catch { } catch(Exception ex)
{
System.Diagnostics.Debug.WriteLine(">>> Exception: " + ex.StackTrace);
}
} }
public override void OnInterrupt() public override void OnInterrupt()
@ -170,9 +148,9 @@ namespace Bit.Droid.Accessibility
// Do nothing. // Do nothing.
} }
public bool ScanAndAutofill(AccessibilityNodeInfo root, AccessibilityEvent e, public bool ScanAndAutofill(AccessibilityNodeInfo root, AccessibilityEvent e)
NotificationManager notificationManager, bool cancelNotification)
{ {
var filled = false;
var passwordNodes = AccessibilityHelpers.GetWindowNodes(root, e, n => n.Password, false); var passwordNodes = AccessibilityHelpers.GetWindowNodes(root, e, n => n.Password, false);
if(passwordNodes.Count > 0) if(passwordNodes.Count > 0)
{ {
@ -182,12 +160,9 @@ namespace Bit.Droid.Accessibility
if(AccessibilityHelpers.NeedToAutofill(AccessibilityHelpers.LastCredentials, uri)) if(AccessibilityHelpers.NeedToAutofill(AccessibilityHelpers.LastCredentials, uri))
{ {
AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes); AccessibilityHelpers.GetNodesAndFill(root, e, passwordNodes);
filled = true;
} }
else
{
NotifyToAutofill(uri, notificationManager);
cancelNotification = false;
}
} }
AccessibilityHelpers.LastCredentials = null; AccessibilityHelpers.LastCredentials = null;
} }
@ -200,69 +175,94 @@ namespace Bit.Droid.Accessibility
}); });
} }
passwordNodes.Dispose(); passwordNodes.Dispose();
return cancelNotification; return filled;
} }
public void CancelNotification(NotificationManager notificationManager, long limit = 250) private void CancelOverlayPrompt()
{ {
if(Java.Lang.JavaSystem.CurrentTimeMillis() - _lastNotificationTime < limit) if(_windowManager == null || _overlayView == null)
{ {
return; return;
} }
_windowManager.RemoveViewImmediate(_overlayView);
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Removed");
_overlayView = null;
_lastNotificationUri = null; _lastNotificationUri = null;
notificationManager?.Cancel(AutoFillNotificationId);
} }
private void NotifyToAutofill(string uri, NotificationManager notificationManager) private void OverlayPromptToAutofill(AccessibilityNodeInfo root, AccessibilityEvent e)
{ {
if(notificationManager == null || string.IsNullOrWhiteSpace(uri)) if(!AccessibilityHelpers.OverlayPermitted(this))
{
System.Diagnostics.Debug.WriteLine(">>> Overlay Permission not granted");
Toast.MakeText(this, AppResources.AccessibilityOverlayPermissionAlert, ToastLength.Long).Show();
return;
}
var uri = AccessibilityHelpers.GetUri(root);
if (string.IsNullOrWhiteSpace(uri))
{ {
return; return;
} }
var now = Java.Lang.JavaSystem.CurrentTimeMillis(); WindowManagerTypes windowManagerType;
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
windowManagerType = WindowManagerTypes.ApplicationOverlay;
}
else
{
windowManagerType = WindowManagerTypes.Phone;
}
var layoutParams = new WindowManagerLayoutParams(
ViewGroup.LayoutParams.WrapContent,
ViewGroup.LayoutParams.WrapContent,
windowManagerType,
WindowManagerFlags.NotFocusable | WindowManagerFlags.NotTouchModal,
Android.Graphics.Format.Transparent);
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(root, e);
layoutParams.Gravity = GravityFlags.Bottom | GravityFlags.Left;
layoutParams.X = anchorPosition.X;
layoutParams.Y = anchorPosition.Y;
var intent = new Intent(this, typeof(AccessibilityActivity)); var intent = new Intent(this, typeof(AccessibilityActivity));
intent.PutExtra("uri", uri); intent.PutExtra("uri", uri);
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.UpdateCurrent);
var notificationContent = Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch ? if (_windowManager == null)
AppResources.BitwardenAutofillServiceNotificationContent :
AppResources.BitwardenAutofillServiceNotificationContentOld;
var builder = new Notification.Builder(this);
builder.SetSmallIcon(Resource.Drawable.shield)
.SetContentTitle(AppResources.BitwardenAutofillService)
.SetContentText(notificationContent)
.SetTicker(notificationContent)
.SetWhen(now)
.SetContentIntent(pendingIntent);
if(Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch)
{ {
builder.SetVisibility(NotificationVisibility.Secret) _windowManager = GetSystemService(WindowService).JavaCast<IWindowManager>();
.SetColor(Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
Resource.Color.primary));
}
if(Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
if(_notificationChannel == null)
{
_notificationChannel = new NotificationChannel("bitwarden_autofill_service",
AppResources.AutofillService, NotificationImportance.Low);
notificationManager.CreateNotificationChannel(_notificationChannel);
}
builder.SetChannelId(_notificationChannel.Id);
}
if(/*Build.VERSION.SdkInt <= BuildVersionCodes.N && */_settingAutofillPersistNotification)
{
builder.SetPriority(-2);
} }
_lastNotificationTime = now; var updateView = false;
if (_overlayView != null)
{
updateView = true;
}
_overlayView = AccessibilityHelpers.GetOverlayView(this);
_overlayView.Click += (sender, eventArgs) => {
CancelOverlayPrompt();
StartActivity(intent);
};
_lastNotificationUri = uri; _lastNotificationUri = uri;
notificationManager.Notify(AutoFillNotificationId, builder.Build());
builder.Dispose(); if (updateView)
{
_windowManager.UpdateViewLayout(_overlayView, layoutParams);
}
else
{
_windowManager.AddView(_overlayView, layoutParams);
}
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View " + (updateView ? "Updated to" : "Added at") + " X:{0} Y:{1}", layoutParams.X, layoutParams.Y);
} }
private bool SkipPackage(string eventPackageName) private bool SkipPackage(string eventPackageName)
@ -285,26 +285,5 @@ namespace Bit.Droid.Accessibility
} }
return _launcherPackageNames.Contains(eventPackageName); return _launcherPackageNames.Contains(eventPackageName);
} }
private void LoadServices()
{
if(_storageService == null)
{
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
}
}
private async Task LoadSettingsAsync()
{
var now = DateTime.UtcNow;
if(_lastSettingsReload == null || (now - _lastSettingsReload.Value) > _settingsReloadSpan)
{
_lastSettingsReload = now;
_settingAutofillPasswordField = await _storageService.GetAsync<bool>(
Constants.AccessibilityAutofillPasswordFieldKey);
_settingAutofillPersistNotification = await _storageService.GetAsync<bool>(
Constants.AccessibilityAutofillPersistNotificationKey);
}
}
} }
} }

View File

@ -15,6 +15,7 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" /> <uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />

View File

@ -2,7 +2,7 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:summary="@string/AutoFillServiceSummary" android:summary="@string/AutoFillServiceSummary"
android:description="@string/AutoFillServiceDescription" android:description="@string/AutoFillServiceDescription"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewFocused" android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged|typeViewFocused|typeViewClicked"
android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds" android:accessibilityFlags="flagReportViewIds"
android:notificationTimeout="100" android:notificationTimeout="100"

View File

@ -69,6 +69,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Bitwarden requires additional accessibility configuration. Open the app for more info..
/// </summary>
public static string AccessibilityOverlayPermissionAlert {
get {
return ResourceManager.GetString("AccessibilityOverlayPermissionAlert", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Account. /// Looks up a localized string similar to Account.
/// </summary> /// </summary>

View File

@ -1587,4 +1587,7 @@
<data name="UseBiometricsToUnlock" xml:space="preserve"> <data name="UseBiometricsToUnlock" xml:space="preserve">
<value>Use Biometrics To Unlock</value> <value>Use Biometrics To Unlock</value>
</data> </data>
<data name="AccessibilityOverlayPermissionAlert" xml:space="preserve">
<value>Bitwarden requires additional accessibility configuration. Open the app for more info.</value>
</data>
</root> </root>