2016-08-27 01:56:09 +03:00
|
|
|
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")]
|
2016-08-27 01:56:09 +03:00
|
|
|
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";
|
2016-12-23 06:37:16 +03:00
|
|
|
|
2016-08-27 01:56:09 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-08-27 01:56:09 +03:00
|
|
|
switch(eventType)
|
|
|
|
{
|
2016-12-23 06:37:16 +03:00
|
|
|
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));
|
2016-12-23 06:37:16 +03:00
|
|
|
|
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)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
|
|
|
|
.FirstOrDefault();
|
|
|
|
uri = ExtractUriFromAddressField(uri, addressNode);
|
2016-12-23 06:37:16 +03:00
|
|
|
|
|
|
|
}
|
2017-01-24 07:32:52 +03:00
|
|
|
else if(root.PackageName == BrowserPackage)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url")
|
|
|
|
.FirstOrDefault();
|
|
|
|
uri = ExtractUriFromAddressField(uri, addressNode);
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
var allEditTexts = GetNodeOrChildren(root, n => EditText(n));
|
|
|
|
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
|
2016-12-23 06:37:16 +03:00
|
|
|
|
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);
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
2017-01-24 07:32:52 +03:00
|
|
|
else
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
AskFillPassword(uri, usernameEditText, avialablePasswordNodes);
|
|
|
|
cancelNotification = false;
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
2016-08-27 01:56:09 +03:00
|
|
|
}
|
2017-01-24 07:32:52 +03:00
|
|
|
|
|
|
|
if(cancelNotification)
|
|
|
|
{
|
|
|
|
((NotificationManager)GetSystemService(NotificationService)).Cancel(AutoFillNotificationId);
|
|
|
|
}
|
2016-08-27 01:56:09 +03:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void OnInterrupt()
|
|
|
|
{
|
|
|
|
|
|
|
|
}
|
2016-12-23 06:37:16 +03:00
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private string ExtractUriFromAddressField(string uri, AccessibilityNodeInfo addressNode)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
if(addressNode != null)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
uri = addressNode.Text;
|
|
|
|
if(!uri.Contains("://"))
|
|
|
|
{
|
|
|
|
uri = string.Concat("http://", uri);
|
|
|
|
}
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
return uri;
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private bool SameUri(string uriString1, string uriString2)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
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;
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private static bool AvailablePasswordField(AccessibilityNodeInfo n, bool isChrome)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
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));
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private static bool EditText(AccessibilityNodeInfo n)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
return n.ClassName != null && n.ClassName.Contains("EditText");
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private void AskFillPassword(string uri, AccessibilityNodeInfo usernameNode,
|
|
|
|
IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
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);
|
2016-12-23 06:37:16 +03:00
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
var targetName = uri;
|
|
|
|
if(uri.StartsWith(AndroidAppProtocol))
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
var packageName = uri.Substring(AndroidAppProtocol.Length);
|
2016-12-23 06:37:16 +03:00
|
|
|
try
|
|
|
|
{
|
|
|
|
var appInfo = PackageManager.GetApplicationInfo(packageName, 0);
|
2017-01-24 07:32:52 +03:00
|
|
|
targetName = appInfo != null ? PackageManager.GetApplicationLabel(appInfo) : packageName;
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
2017-01-24 07:32:52 +03:00
|
|
|
catch
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
|
|
|
targetName = packageName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
//targetName = KeePassLib.Utility.UrlUtil.GetHost(uri);
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
2016-12-23 06:37:16 +03:00
|
|
|
.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.")
|
2016-12-23 06:37:16 +03:00
|
|
|
.SetVisibility(NotificationVisibility.Secret)
|
2017-01-24 07:32:52 +03:00
|
|
|
.SetContentIntent(pendingIntent);
|
2016-12-23 06:37:16 +03:00
|
|
|
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
|
2017-01-24 07:32:52 +03:00
|
|
|
notificationManager.Notify(AutoFillNotificationId, builder.Build());
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
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);
|
|
|
|
}
|
2016-12-23 06:37:16 +03:00
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
AutofillActivity.LastCredentials = null;
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
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);
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private bool AnyNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
|
|
|
return GetNodeOrChildren(n, p).Any();
|
|
|
|
}
|
|
|
|
|
2017-01-24 07:32:52 +03:00
|
|
|
private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n,
|
|
|
|
Func<AccessibilityNodeInfo, bool> p)
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
|
|
|
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))
|
2016-12-23 06:37:16 +03:00
|
|
|
{
|
2017-01-24 07:32:52 +03:00
|
|
|
yield return node;
|
2016-12-23 06:37:16 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-08-27 01:56:09 +03:00
|
|
|
}
|
2017-01-28 07:32:48 +03:00
|
|
|
}
|