bitwarden-android/src/Android/AutofillService.cs

320 lines
13 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 SystemUiPackage = "com.android.systemui";
2017-01-31 03:26:39 +03:00
private const string BitwardenPackage = "com.x8bit.bitwarden";
2017-02-01 04:45:51 +03:00
private const string BitwardenWebsite = "bitwarden.com";
private static Dictionary<string, Browser> SupportedBrowsers => new List<Browser>
{
new Browser("com.android.chrome", "url_bar"),
new Browser("com.chrome.beta", "url_bar"),
new Browser("com.android.browser", "url"),
new Browser("com.brave.browser", "url_bar"),
new Browser("com.opera.browser", "url_field"),
new Browser("com.opera.browser.beta", "url_field"),
new Browser("com.opera.mini.native", "url_field"),
new Browser("com.chrome.dev", "url_bar"),
new Browser("com.chrome.canary", "url_bar"),
new Browser("com.google.android.apps.chrome", "url_bar"),
new Browser("com.google.android.apps.chrome_dev", "url_bar"),
new Browser("org.iron.srware", "url_bar"),
new Browser("com.sec.android.app.sbrowser", "sbrowser_url_bar"),
new Browser("com.yandex.browser", "bro_omnibar_address_title_text",
(s) => s.Split(' ').FirstOrDefault()),
new Browser("org.mozilla.firefox", "url_bar_title"),
new Browser("org.mozilla.firefox_beta", "url_bar_title"),
new Browser("com.ghostery.android.ghostery", "search_field"),
new Browser("org.adblockplus.browser", "url_bar_title"),
new Browser("com.htc.sense.browser", "title"),
new Browser("com.amazon.cloud9", "url"),
new Browser("mobi.mgeek.TunnyBrowser", "title"),
new Browser("com.nubelacorp.javelin", "enterUrl"),
new Browser("com.jerky.browser2", "enterUrl"),
new Browser("com.mx.browser", "address_editor_with_progress"),
new Browser("com.mx.browser.tablet", "address_editor_with_progress"),
2017-02-17 07:09:40 +03:00
new Browser("com.linkbubble.playstore", "url_text"),
new Browser("com.ksmobile.cb", "address_bar_edit_text")
}.ToDictionary(n => n.PackageName);
2017-02-01 08:38:35 +03:00
public override void OnAccessibilityEvent(AccessibilityEvent e)
{
try
2017-01-24 07:32:52 +03:00
{
var root = RootInActiveWindow;
if(e == null || root == null || string.IsNullOrWhiteSpace(e.PackageName) ||
e.PackageName == SystemUiPackage || root.PackageName != e.PackageName)
{
return;
}
2017-02-17 07:09:40 +03:00
/*
var testNodes = GetWindowNodes(root, e, n => n.ViewIdResourceName != null && n.Text != null, false);
var testNodesData = testNodes.Select(n => new { id = n.ViewIdResourceName, text = n.Text });
testNodes.Dispose();
*/
2017-02-09 02:19:59 +03:00
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
switch(e.EventType)
{
case EventTypes.WindowContentChanged:
case EventTypes.WindowStateChanged:
var cancelNotification = true;
2017-02-03 07:36:40 +03:00
if(e.PackageName == BitwardenPackage)
{
notificationManager?.Cancel(AutoFillNotificationId);
break;
}
2017-02-20 01:29:00 +03:00
var passwordNodes = GetWindowNodes(root, e, n => n.Password, false);
if(passwordNodes.Count > 0)
{
var uri = GetUri(root);
if(uri != null && !uri.Contains(BitwardenWebsite))
{
if(NeedToAutofill(AutofillActivity.LastCredentials, uri))
{
var allEditTexts = GetWindowNodes(root, e, n => EditText(n), false);
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
FillCredentials(usernameEditText, passwordNodes);
allEditTexts.Dispose();
usernameEditText.Dispose();
}
else
{
NotifyToAutofill(uri, notificationManager);
cancelNotification = false;
}
}
AutofillActivity.LastCredentials = null;
}
2017-01-31 03:26:39 +03:00
passwordNodes.Dispose();
2017-01-24 07:32:52 +03:00
if(cancelNotification)
{
notificationManager?.Cancel(AutoFillNotificationId);
}
break;
default:
break;
}
2017-02-21 02:22:24 +03:00
notificationManager?.Dispose();
root.Dispose();
e.Dispose();
}
// Some unknown condition is causing NullReferenceException's in production. Suppress it for now.
catch(NullReferenceException) { }
}
public override void OnInterrupt()
{
}
2017-02-01 04:45:51 +03:00
private string GetUri(AccessibilityNodeInfo root)
{
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName);
if(SupportedBrowsers.ContainsKey(root.PackageName))
2017-02-01 04:45:51 +03:00
{
var addressNode = root.FindAccessibilityNodeInfosByViewId(
$"{root.PackageName}:id/{SupportedBrowsers[root.PackageName].UriViewId}").FirstOrDefault();
if(addressNode != null)
{
uri = ExtractUri(uri, addressNode, SupportedBrowsers[root.PackageName]);
2017-02-21 02:22:24 +03:00
addressNode.Dispose();
}
2017-02-01 04:45:51 +03:00
}
return uri;
}
private string ExtractUri(string uri, AccessibilityNodeInfo addressNode, Browser browser)
{
if(addressNode?.Text != null)
{
2017-02-18 05:18:59 +03:00
uri = browser.GetUriFunction(addressNode.Text).Trim();
if(uri != null && uri.Contains("."))
2017-02-01 06:53:32 +03:00
{
2017-02-18 05:18:59 +03:00
if(!uri.Contains("://") && !uri.Contains(" "))
2017-02-01 06:53:32 +03:00
{
uri = string.Concat("http://", uri);
}
else if(Build.VERSION.SdkInt <= BuildVersionCodes.KitkatWatch)
{
var parts = uri.Split(new string[] { ". " }, StringSplitOptions.None);
if(parts.Length > 1)
2017-02-01 06:53:32 +03:00
{
var urlPart = parts.FirstOrDefault(p => p.StartsWith("http"));
if(urlPart != null)
{
uri = urlPart.Trim();
}
2017-02-01 06:53:32 +03:00
}
}
}
}
2017-01-24 07:32:52 +03:00
return uri;
}
2017-01-31 03:26:39 +03:00
/// <summary>
/// Check to make sure it is ok to autofill still on the current screen
/// </summary>
private bool NeedToAutofill(AutofillCredentials creds, string currentUriString)
{
2017-01-31 03:26:39 +03:00
if(creds == null)
{
return false;
}
Uri lastUri, currentUri;
if(Uri.TryCreate(creds.LastUri, UriKind.Absolute, out lastUri) &&
2017-01-31 03:26:39 +03:00
Uri.TryCreate(currentUriString, UriKind.Absolute, out currentUri) &&
lastUri.Host == currentUri.Host)
2017-01-24 07:32:52 +03:00
{
return true;
}
return false;
}
2017-01-24 07:32:52 +03:00
private static bool EditText(AccessibilityNodeInfo n)
{
return n?.ClassName?.Contains("EditText") ?? false;
}
2017-02-21 02:22:24 +03:00
private void NotifyToAutofill(string uri, NotificationManager notificationManager)
{
if(notificationManager == null || string.IsNullOrWhiteSpace(uri))
{
return;
}
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);
var builder = new Notification.Builder(this);
2017-01-29 07:58:26 +03:00
builder.SetSmallIcon(Resource.Drawable.notification_sm)
2017-02-01 08:38:35 +03:00
.SetContentTitle(App.Resources.AppResources.BitwardenAutofillService)
.SetContentText(App.Resources.AppResources.BitwardenAutofillServiceNotificationContent)
.SetTicker(App.Resources.AppResources.BitwardenAutofillServiceNotificationContent)
2017-01-31 03:26:39 +03:00
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
2017-01-24 07:32:52 +03:00
.SetContentIntent(pendingIntent);
2017-01-29 07:58:26 +03:00
2017-02-01 04:45:51 +03:00
if(Build.VERSION.SdkInt > BuildVersionCodes.KitkatWatch)
{
builder.SetVisibility(NotificationVisibility.Secret)
.SetColor(global::Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
Resource.Color.primary));
}
2017-01-24 07:32:52 +03:00
notificationManager.Notify(AutoFillNotificationId, builder.Build());
2017-02-21 02:22:24 +03:00
builder.Dispose();
}
2017-01-24 07:32:52 +03:00
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
{
FillEditText(usernameNode, AutofillActivity.LastCredentials?.Username);
2017-01-31 03:26:39 +03:00
foreach(var n in passwordNodes)
2017-01-24 07:32:52 +03:00
{
FillEditText(n, AutofillActivity.LastCredentials?.Password);
2017-01-24 07:32:52 +03:00
}
}
2017-01-24 07:32:52 +03:00
private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
{
2017-01-31 03:26:39 +03:00
if(editTextNode == null || value == null)
{
return;
}
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-02-21 02:22:24 +03:00
private NodeList GetWindowNodes(AccessibilityNodeInfo n, AccessibilityEvent e,
Func<AccessibilityNodeInfo, bool> condition, bool disposeIfUnused, NodeList nodes = null)
{
2017-02-18 18:50:27 +03:00
if(nodes == null)
{
2017-02-21 02:22:24 +03:00
nodes = new NodeList();
2017-02-18 18:50:27 +03:00
}
if(n != null)
{
2017-02-21 02:22:24 +03:00
var dispose = disposeIfUnused;
2017-02-17 06:22:19 +03:00
if(n.WindowId == e.WindowId && !(n.ViewIdResourceName?.StartsWith(SystemUiPackage) ?? false) && condition(n))
{
2017-02-21 02:22:24 +03:00
dispose = false;
2017-02-18 18:50:27 +03:00
nodes.Add(n);
}
2017-02-18 23:48:24 +03:00
for(var i = 0; i < n.ChildCount; i++)
{
2017-02-21 02:22:24 +03:00
GetWindowNodes(n.GetChild(i), e, condition, true, nodes);
}
if(dispose)
{
n.Dispose();
}
}
2017-02-18 18:50:27 +03:00
return nodes;
}
public class Browser
{
public Browser(string packageName, string uriViewId)
{
PackageName = packageName;
UriViewId = uriViewId;
}
public Browser(string packageName, string uriViewId, Func<string, string> getUriFunction)
: this(packageName, uriViewId)
{
GetUriFunction = getUriFunction;
}
public string PackageName { get; set; }
public string UriViewId { get; set; }
public Func<string, string> GetUriFunction { get; set; } = (s) => s;
}
2017-02-21 02:22:24 +03:00
public class NodeList : List<AccessibilityNodeInfo>, IDisposable
{
public void Dispose()
{
foreach(var item in this)
{
item.Dispose();
}
}
}
}
2017-01-28 07:32:48 +03:00
}