accessibility service WIP

This commit is contained in:
Kyle Spearrin 2017-01-31 20:45:51 -05:00
parent 47e427a851
commit 2c446f939e
6 changed files with 86 additions and 50 deletions

View File

@ -6,9 +6,9 @@ using Android.Views;
namespace Bit.Android namespace Bit.Android
{ {
[Activity(Label = "bitwarden", [Activity(Theme = "@style/BitwardenTheme.Splash",
Label = "bitwarden",
Icon = "@drawable/icon", Icon = "@drawable/icon",
LaunchMode = global::Android.Content.PM.LaunchMode.SingleTask,
WindowSoftInputMode = SoftInput.StateHidden)] WindowSoftInputMode = SoftInput.StateHidden)]
public class AutofillActivity : Activity public class AutofillActivity : Activity
{ {
@ -69,6 +69,7 @@ namespace Bit.Android
} }
finally finally
{ {
Xamarin.Forms.MessagingCenter.Send(Xamarin.Forms.Application.Current, "SetMainPage");
Finish(); Finish();
} }
} }

View File

@ -18,7 +18,11 @@ namespace Bit.Android
private const string SystemUiPackage = "com.android.systemui"; private const string SystemUiPackage = "com.android.systemui";
private const string ChromePackage = "com.android.chrome"; private const string ChromePackage = "com.android.chrome";
private const string BrowserPackage = "com.android.browser"; private const string BrowserPackage = "com.android.browser";
private const string BravePackage = "com.brave.browser";
private const string OperaPackage = "com.opera.browser";
private const string OperaMiniPackage = "com.opera.mini.native";
private const string BitwardenPackage = "com.x8bit.bitwarden"; private const string BitwardenPackage = "com.x8bit.bitwarden";
private const string BitwardenWebsite = "bitwarden.com";
public override void OnAccessibilityEvent(AccessibilityEvent e) public override void OnAccessibilityEvent(AccessibilityEvent e)
{ {
@ -36,31 +40,21 @@ namespace Bit.Android
case EventTypes.WindowStateChanged: case EventTypes.WindowStateChanged:
var cancelNotification = true; var cancelNotification = true;
var root = RootInActiveWindow; var root = RootInActiveWindow;
var isChrome = root == null ? false : root.PackageName == ChromePackage; var passwordNodes = GetWindowNodes(root, e, n => n.Password);
var avialablePasswordNodes = GetWindowNodes(root, e, n => AvailablePasswordField(n, isChrome));
if(avialablePasswordNodes.Any()) if(passwordNodes.Any())
{ {
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName); var uri = GetUri(root);
if(isChrome) if(uri.Contains(BitwardenWebsite))
{ {
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar") break;
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
}
else if(root.PackageName == BrowserPackage)
{
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
} }
if(NeedToAutofill(AutofillActivity.LastCredentials, uri)) if(NeedToAutofill(AutofillActivity.LastCredentials, uri))
{ {
var allEditTexts = GetWindowNodes(root, e, n => EditText(n)); var allEditTexts = GetWindowNodes(root, e, n => EditText(n));
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault(); var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
FillCredentials(usernameEditText, avialablePasswordNodes); FillCredentials(usernameEditText, passwordNodes);
} }
else else
{ {
@ -73,8 +67,7 @@ namespace Bit.Android
if(cancelNotification) if(cancelNotification)
{ {
var notificationManager = ((NotificationManager)GetSystemService(NotificationService)); CancelNotification();
notificationManager.Cancel(AutoFillNotificationId);
} }
break; break;
default: default:
@ -87,7 +80,41 @@ namespace Bit.Android
} }
private string ExtractUriFromAddressField(string uri, AccessibilityNodeInfo addressNode) private void CancelNotification()
{
var notificationManager = ((NotificationManager)GetSystemService(NotificationService));
notificationManager.Cancel(AutoFillNotificationId);
}
private string GetUri(AccessibilityNodeInfo root)
{
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName);
string addressViewId = null;
if(root.PackageName == ChromePackage || root.PackageName == BravePackage)
{
addressViewId = "url_bar";
}
else if(true || root.PackageName == BrowserPackage)
{
addressViewId = "url";
}
else if(root.PackageName == OperaPackage || root.PackageName == OperaMiniPackage)
{
addressViewId = "url_field";
}
if(!string.IsNullOrWhiteSpace(addressViewId))
{
var addressNode = root.FindAccessibilityNodeInfosByViewId(
$"{root.PackageName}:id/{addressViewId}").FirstOrDefault();
uri = ExtractUri(uri, addressNode);
}
return uri;
}
private string ExtractUri(string uri, AccessibilityNodeInfo addressNode)
{ {
if(addressNode != null) if(addressNode != null)
{ {
@ -123,13 +150,6 @@ namespace Bit.Android
return false; return false;
} }
private static bool AvailablePasswordField(AccessibilityNodeInfo n, bool isChrome)
{
// chrome sends password field values in many conditions when the field is still actually empty
// ex. placeholders, nearby label, etc
return n.Password && (isChrome || string.IsNullOrWhiteSpace(n.Text));
}
private static bool EditText(AccessibilityNodeInfo n) private static bool EditText(AccessibilityNodeInfo n)
{ {
return n.ClassName != null && n.ClassName.Contains("EditText"); return n.ClassName != null && n.ClassName.Contains("EditText");
@ -145,14 +165,18 @@ namespace Bit.Android
var builder = new Notification.Builder(this); var builder = new Notification.Builder(this);
builder.SetSmallIcon(Resource.Drawable.notification_sm) builder.SetSmallIcon(Resource.Drawable.notification_sm)
.SetContentTitle("bitwarden Autofill Service") .SetContentTitle("bitwarden Autofill Service")
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.") .SetContentText("Tap this notification to autofill a login from your vault.")
.SetTicker("Tap this notification to autofill a login from your bitwarden vault.") .SetTicker("Tap this notification to autofill a login from your vault.")
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis()) .SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetVisibility(NotificationVisibility.Secret)
.SetColor(global::Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
Resource.Color.primary))
.SetContentIntent(pendingIntent); .SetContentIntent(pendingIntent);
if(Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch)
{
builder.SetVisibility(NotificationVisibility.Secret)
.SetColor(global::Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
Resource.Color.primary));
}
var notificationManager = (NotificationManager)GetSystemService(NotificationService); var notificationManager = (NotificationManager)GetSystemService(NotificationService);
notificationManager.Notify(AutoFillNotificationId, builder.Build()); notificationManager.Notify(AutoFillNotificationId, builder.Build());
} }

View File

@ -21,7 +21,6 @@ namespace Bit.Android
[Activity(Label = "bitwarden", [Activity(Label = "bitwarden",
Icon = "@drawable/icon", Icon = "@drawable/icon",
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
LaunchMode = LaunchMode.SingleTask,
WindowSoftInputMode = SoftInput.StateHidden)] WindowSoftInputMode = SoftInput.StateHidden)]
public class MainActivity : FormsAppCompatActivity public class MainActivity : FormsAppCompatActivity
{ {
@ -89,7 +88,7 @@ namespace Bit.Android
private void ReturnCredentials(VaultListPageModel.Login login) private void ReturnCredentials(VaultListPageModel.Login login)
{ {
App.App.WasFromAutofillService = true; App.App.FromAutofillService = true;
Intent data = new Intent(); Intent data = new Intent();
if(login == null) if(login == null)
{ {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged" android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackSpoken" android:accessibilityFeedbackType="feedbackSpoken"
android:accessibilityFlags="flagDefault" android:accessibilityFlags="flagDefault"
android:notificationTimeout="100" android:notificationTimeout="100"

View File

@ -19,7 +19,7 @@ namespace Bit.App
{ {
public class App : Application public class App : Application
{ {
private readonly string _uri; private string _uri;
private readonly IDatabaseService _databaseService; private readonly IDatabaseService _databaseService;
private readonly IConnectivity _connectivity; private readonly IConnectivity _connectivity;
private readonly IUserDialogs _userDialogs; private readonly IUserDialogs _userDialogs;
@ -31,7 +31,7 @@ namespace Bit.App
private readonly IGoogleAnalyticsService _googleAnalyticsService; private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly ILocalizeService _localizeService; private readonly ILocalizeService _localizeService;
public static bool WasFromAutofillService { get; set; } = false; public static bool FromAutofillService { get; set; } = false;
public App( public App(
string uri, string uri,
@ -61,6 +61,7 @@ namespace Bit.App
SetCulture(); SetCulture();
SetStyles(); SetStyles();
FromAutofillService = !string.IsNullOrWhiteSpace(_uri);
if(authService.IsAuthenticated && _uri != null) if(authService.IsAuthenticated && _uri != null)
{ {
MainPage = new ExtendedNavigationPage(new VaultAutofillListLoginsPage(_uri)); MainPage = new ExtendedNavigationPage(new VaultAutofillListLoginsPage(_uri));
@ -89,12 +90,16 @@ namespace Bit.App
{ {
Device.BeginInvokeOnMainThread(() => Logout(args)); Device.BeginInvokeOnMainThread(() => Logout(args));
}); });
MessagingCenter.Subscribe<Application>(Current, "SetMainPage", async (sender) =>
{
await SetMainPageFromAutofill();
});
} }
protected async override void OnStart() protected async override void OnStart()
{ {
// Handle when your app starts // Handle when your app starts
ResumeFromAutofill();
await CheckLockAsync(false); await CheckLockAsync(false);
_databaseService.CreateTables(); _databaseService.CreateTables();
await Task.Run(() => FullSyncAsync()).ConfigureAwait(false); await Task.Run(() => FullSyncAsync()).ConfigureAwait(false);
@ -102,11 +107,12 @@ namespace Bit.App
Debug.WriteLine("OnStart"); Debug.WriteLine("OnStart");
} }
protected override void OnSleep() protected async override void OnSleep()
{ {
// Handle when your app sleeps // Handle when your app sleeps
Debug.WriteLine("OnSleep"); Debug.WriteLine("OnSleep");
await SetMainPageFromAutofill(true);
if(Device.OS == TargetPlatform.Android && !TopPageIsLock()) if(Device.OS == TargetPlatform.Android && !TopPageIsLock())
{ {
_settings.AddOrUpdateValue(Constants.LastActivityDate, DateTime.UtcNow); _settings.AddOrUpdateValue(Constants.LastActivityDate, DateTime.UtcNow);
@ -123,7 +129,6 @@ namespace Bit.App
// Handle when your app resumes // Handle when your app resumes
Debug.WriteLine("OnResume"); Debug.WriteLine("OnResume");
ResumeFromAutofill();
if(Device.OS == TargetPlatform.Android) if(Device.OS == TargetPlatform.Android)
{ {
@ -137,16 +142,26 @@ namespace Bit.App
} }
} }
private void ResumeFromAutofill() private async Task SetMainPageFromAutofill(bool skipAlreadyOnCheck = false)
{ {
if(Device.OS == TargetPlatform.Android && WasFromAutofillService) if(Device.OS != TargetPlatform.Android)
{ {
WasFromAutofillService = false; return;
MainPage = new MainPage();
} }
else
var alreadyOnMainPage = MainPage as MainPage;
if(!skipAlreadyOnCheck && alreadyOnMainPage != null)
{ {
WasFromAutofillService = !string.IsNullOrWhiteSpace(_uri); return;
}
if(FromAutofillService || !string.IsNullOrWhiteSpace(_uri))
{
// delay some so that we dont see the screen change as autofill closes
await Task.Delay(1000);
MainPage = new MainPage();
_uri = null;
FromAutofillService = false;
} }
} }

View File

@ -9,9 +9,6 @@ using Bit.App.Resources;
using Xamarin.Forms; using Xamarin.Forms;
using XLabs.Ioc; using XLabs.Ioc;
using Bit.App.Utilities; using Bit.App.Utilities;
using PushNotification.Plugin.Abstractions;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Threading; using System.Threading;
using Bit.App.Models; using Bit.App.Models;
using System.Collections.Generic; using System.Collections.Generic;