bitwarden-android/src/Android/AutofillService.cs

213 lines
8.3 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using Android.AccessibilityServices;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Views.Accessibility;
namespace Bit.Android
{
2017-01-24 07:32:52 +03:00
[Service(Permission = "android.permission.BIND_ACCESSIBILITY_SERVICE", Label = "bitwarden")]
[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
public class AutofillService : AccessibilityService
{
2017-01-24 07:32:52 +03:00
private const int AutoFillNotificationId = 34573;
private const string AndroidAppProtocol = "androidapp://";
private const string SystemUiPackage = "com.android.systemui";
private const string ChromePackage = "com.android.chrome";
private const string BrowserPackage = "com.android.browser";
public override void OnAccessibilityEvent(AccessibilityEvent e)
{
var eventType = e.EventType;
2017-01-24 07:32:52 +03:00
var packageName = e.PackageName;
if(packageName == SystemUiPackage)
{
return;
}
switch(eventType)
{
case EventTypes.WindowContentChanged:
case EventTypes.WindowStateChanged:
var root = RootInActiveWindow;
2017-01-24 07:32:52 +03:00
var isChrome = root == null ? false : root.PackageName == ChromePackage;
var cancelNotification = true;
var avialablePasswordNodes = GetNodeOrChildren(root, n => AvailablePasswordField(n, isChrome));
2017-01-24 07:32:52 +03:00
if(avialablePasswordNodes.Any() && AnyNodeOrChildren(root, n => n.WindowId == e.WindowId &&
!(n.ViewIdResourceName != null && n.ViewIdResourceName.StartsWith(SystemUiPackage))))
{
var uri = string.Concat(AndroidAppProtocol, root.PackageName);
if(isChrome)
{
2017-01-24 07:32:52 +03:00
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
}
2017-01-24 07:32:52 +03:00
else if(root.PackageName == BrowserPackage)
{
2017-01-24 07:32:52 +03:00
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
}
2017-01-24 07:32:52 +03:00
var allEditTexts = GetNodeOrChildren(root, n => EditText(n));
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
2017-01-28 07:32:48 +03:00
if(AutofillActivity.LastCredentials != null && SameUri(AutofillActivity.LastCredentials.LastUri, uri))
2017-01-24 07:32:52 +03:00
{
FillCredentials(usernameEditText, avialablePasswordNodes);
}
2017-01-24 07:32:52 +03:00
else
{
2017-01-24 07:32:52 +03:00
AskFillPassword(uri, usernameEditText, avialablePasswordNodes);
cancelNotification = false;
}
}
2017-01-24 07:32:52 +03:00
if(cancelNotification)
{
((NotificationManager)GetSystemService(NotificationService)).Cancel(AutoFillNotificationId);
}
break;
default:
break;
}
}
public override void OnInterrupt()
{
}
2017-01-24 07:32:52 +03:00
private string ExtractUriFromAddressField(string uri, AccessibilityNodeInfo addressNode)
{
2017-01-24 07:32:52 +03:00
if(addressNode != null)
{
2017-01-24 07:32:52 +03:00
uri = addressNode.Text;
if(!uri.Contains("://"))
{
uri = string.Concat("http://", uri);
}
}
2017-01-24 07:32:52 +03:00
return uri;
}
2017-01-24 07:32:52 +03:00
private bool SameUri(string uriString1, string uriString2)
{
2017-01-24 07:32:52 +03:00
Uri uri1, uri2;
if(Uri.TryCreate(uriString1, UriKind.RelativeOrAbsolute, out uri1) &&
Uri.TryCreate(uriString2, UriKind.RelativeOrAbsolute, out uri2) && uri1.Host == uri2.Host)
{
return true;
}
return false;
}
2017-01-24 07:32:52 +03:00
private static bool AvailablePasswordField(AccessibilityNodeInfo n, bool isChrome)
{
2017-01-24 07:32:52 +03:00
// 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));
}
2017-01-24 07:32:52 +03:00
private static bool EditText(AccessibilityNodeInfo n)
{
2017-01-24 07:32:52 +03:00
return n.ClassName != null && n.ClassName.Contains("EditText");
}
2017-01-24 07:32:52 +03:00
private void AskFillPassword(string uri, AccessibilityNodeInfo usernameNode,
IEnumerable<AccessibilityNodeInfo> passwordNodes)
{
2017-01-24 07:32:52 +03:00
var intent = new Intent(this, typeof(AutofillActivity));
intent.PutExtra("uri", uri);
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
var pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.UpdateCurrent);
2017-01-24 07:32:52 +03:00
var targetName = uri;
if(uri.StartsWith(AndroidAppProtocol))
{
2017-01-24 07:32:52 +03:00
var packageName = uri.Substring(AndroidAppProtocol.Length);
try
{
var appInfo = PackageManager.GetApplicationInfo(packageName, 0);
2017-01-24 07:32:52 +03:00
targetName = appInfo != null ? PackageManager.GetApplicationLabel(appInfo) : packageName;
}
2017-01-24 07:32:52 +03:00
catch
{
targetName = packageName;
}
}
else
{
2017-01-24 07:32:52 +03:00
//targetName = KeePassLib.Utility.UrlUtil.GetHost(uri);
}
var builder = new Notification.Builder(this);
//TODO icon
//TODO plugin icon
builder.SetSmallIcon(Resource.Drawable.icon)
2017-01-24 07:32:52 +03:00
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
.SetContentTitle("bitwarden Autofill Service")
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
2017-01-24 07:32:52 +03:00
.SetTicker("Tap this notification to autofill a login from your bitwarden vault.")
.SetVisibility(NotificationVisibility.Secret)
2017-01-24 07:32:52 +03:00
.SetContentIntent(pendingIntent);
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
2017-01-24 07:32:52 +03:00
notificationManager.Notify(AutoFillNotificationId, builder.Build());
}
2017-01-24 07:32:52 +03:00
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
{
2017-01-28 07:32:48 +03:00
FillEditText(usernameNode, AutofillActivity.LastCredentials.Username);
2017-01-24 07:32:52 +03:00
foreach(var pNode in passwordNodes)
{
FillEditText(pNode, AutofillActivity.LastCredentials.Password);
}
2017-01-24 07:32:52 +03:00
AutofillActivity.LastCredentials = null;
}
2017-01-24 07:32:52 +03:00
private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
{
2017-01-24 07:32:52 +03:00
var bundle = new Bundle();
bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value);
editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle);
}
2017-01-24 07:32:52 +03:00
private bool AnyNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
{
return GetNodeOrChildren(n, p).Any();
}
2017-01-24 07:32:52 +03:00
private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n,
Func<AccessibilityNodeInfo, bool> p)
{
if(n != null)
{
if(p(n))
{
yield return n;
}
for(int i = 0; i < n.ChildCount; i++)
{
2017-01-24 07:32:52 +03:00
foreach(var node in GetNodeOrChildren(n.GetChild(i), p))
{
2017-01-24 07:32:52 +03:00
yield return node;
}
}
}
}
}
2017-01-28 07:32:48 +03:00
}