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

View file

@ -18,7 +18,11 @@ namespace Bit.Android
private const string SystemUiPackage = "com.android.systemui";
private const string ChromePackage = "com.android.chrome";
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 BitwardenWebsite = "bitwarden.com";
public override void OnAccessibilityEvent(AccessibilityEvent e)
{
@ -36,31 +40,21 @@ namespace Bit.Android
case EventTypes.WindowStateChanged:
var cancelNotification = true;
var root = RootInActiveWindow;
var isChrome = root == null ? false : root.PackageName == ChromePackage;
var avialablePasswordNodes = GetWindowNodes(root, e, n => AvailablePasswordField(n, isChrome));
var passwordNodes = GetWindowNodes(root, e, n => n.Password);
if(avialablePasswordNodes.Any())
if(passwordNodes.Any())
{
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName);
if(isChrome)
var uri = GetUri(root);
if(uri.Contains(BitwardenWebsite))
{
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
}
else if(root.PackageName == BrowserPackage)
{
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
break;
}
if(NeedToAutofill(AutofillActivity.LastCredentials, uri))
{
var allEditTexts = GetWindowNodes(root, e, n => EditText(n));
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
FillCredentials(usernameEditText, avialablePasswordNodes);
FillCredentials(usernameEditText, passwordNodes);
}
else
{
@ -73,8 +67,7 @@ namespace Bit.Android
if(cancelNotification)
{
var notificationManager = ((NotificationManager)GetSystemService(NotificationService));
notificationManager.Cancel(AutoFillNotificationId);
CancelNotification();
}
break;
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)
{
@ -123,13 +150,6 @@ namespace Bit.Android
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)
{
return n.ClassName != null && n.ClassName.Contains("EditText");
@ -145,14 +165,18 @@ namespace Bit.Android
var builder = new Notification.Builder(this);
builder.SetSmallIcon(Resource.Drawable.notification_sm)
.SetContentTitle("bitwarden Autofill Service")
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
.SetTicker("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 vault.")
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetVisibility(NotificationVisibility.Secret)
.SetColor(global::Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
Resource.Color.primary))
.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);
notificationManager.Notify(AutoFillNotificationId, builder.Build());
}

View file

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

View file

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

View file

@ -19,7 +19,7 @@ namespace Bit.App
{
public class App : Application
{
private readonly string _uri;
private string _uri;
private readonly IDatabaseService _databaseService;
private readonly IConnectivity _connectivity;
private readonly IUserDialogs _userDialogs;
@ -31,7 +31,7 @@ namespace Bit.App
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly ILocalizeService _localizeService;
public static bool WasFromAutofillService { get; set; } = false;
public static bool FromAutofillService { get; set; } = false;
public App(
string uri,
@ -61,6 +61,7 @@ namespace Bit.App
SetCulture();
SetStyles();
FromAutofillService = !string.IsNullOrWhiteSpace(_uri);
if(authService.IsAuthenticated && _uri != null)
{
MainPage = new ExtendedNavigationPage(new VaultAutofillListLoginsPage(_uri));
@ -89,12 +90,16 @@ namespace Bit.App
{
Device.BeginInvokeOnMainThread(() => Logout(args));
});
MessagingCenter.Subscribe<Application>(Current, "SetMainPage", async (sender) =>
{
await SetMainPageFromAutofill();
});
}
protected async override void OnStart()
{
// Handle when your app starts
ResumeFromAutofill();
await CheckLockAsync(false);
_databaseService.CreateTables();
await Task.Run(() => FullSyncAsync()).ConfigureAwait(false);
@ -102,11 +107,12 @@ namespace Bit.App
Debug.WriteLine("OnStart");
}
protected override void OnSleep()
protected async override void OnSleep()
{
// Handle when your app sleeps
Debug.WriteLine("OnSleep");
await SetMainPageFromAutofill(true);
if(Device.OS == TargetPlatform.Android && !TopPageIsLock())
{
_settings.AddOrUpdateValue(Constants.LastActivityDate, DateTime.UtcNow);
@ -123,7 +129,6 @@ namespace Bit.App
// Handle when your app resumes
Debug.WriteLine("OnResume");
ResumeFromAutofill();
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;
MainPage = new MainPage();
return;
}
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 XLabs.Ioc;
using Bit.App.Utilities;
using PushNotification.Plugin.Abstractions;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Threading;
using Bit.App.Models;
using System.Collections.Generic;