1
0
mirror of https://github.com/bitwarden/mobile.git synced 2025-01-07 18:58:35 +01:00
bitwarden-mobile/src/Android/AutofillService.cs

202 lines
8.0 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 05:32:52 +01: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 05:32:52 +01:00
private const int AutoFillNotificationId = 34573;
private const string SystemUiPackage = "com.android.systemui";
private const string ChromePackage = "com.android.chrome";
private const string BrowserPackage = "com.android.browser";
2017-01-31 01:26:39 +01:00
private const string BitwardenPackage = "com.x8bit.bitwarden";
public override void OnAccessibilityEvent(AccessibilityEvent e)
{
var eventType = e.EventType;
2017-01-24 05:32:52 +01:00
var packageName = e.PackageName;
2017-01-31 01:26:39 +01:00
if(packageName == SystemUiPackage || packageName == BitwardenPackage)
2017-01-24 05:32:52 +01:00
{
return;
}
switch(eventType)
{
case EventTypes.WindowContentChanged:
case EventTypes.WindowStateChanged:
2017-01-31 01:26:39 +01:00
var cancelNotification = true;
var root = RootInActiveWindow;
2017-01-24 05:32:52 +01:00
var isChrome = root == null ? false : root.PackageName == ChromePackage;
2017-01-31 01:26:39 +01:00
var avialablePasswordNodes = GetWindowNodes(root, e, n => AvailablePasswordField(n, isChrome));
2017-01-31 01:26:39 +01:00
if(avialablePasswordNodes.Any())
2017-01-24 05:32:52 +01:00
{
2017-01-31 01:26:39 +01:00
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName);
2017-01-24 05:32:52 +01:00
if(isChrome)
{
2017-01-24 05:32:52 +01:00
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
}
2017-01-24 05:32:52 +01:00
else if(root.PackageName == BrowserPackage)
{
2017-01-24 05:32:52 +01:00
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url")
.FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
}
2017-01-31 01:26:39 +01:00
if(NeedToAutofill(AutofillActivity.LastCredentials, uri))
2017-01-24 05:32:52 +01:00
{
2017-01-31 01:26:39 +01:00
var allEditTexts = GetWindowNodes(root, e, n => EditText(n));
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
2017-01-24 05:32:52 +01:00
FillCredentials(usernameEditText, avialablePasswordNodes);
}
2017-01-24 05:32:52 +01:00
else
{
2017-01-31 01:26:39 +01:00
NotifyToAutofill(uri);
2017-01-24 05:32:52 +01:00
cancelNotification = false;
}
2017-01-31 01:26:39 +01:00
AutofillActivity.LastCredentials = null;
}
2017-01-24 05:32:52 +01:00
if(cancelNotification)
{
2017-01-31 01:26:39 +01:00
var notificationManager = ((NotificationManager)GetSystemService(NotificationService));
notificationManager.Cancel(AutoFillNotificationId);
2017-01-24 05:32:52 +01:00
}
break;
default:
break;
}
}
public override void OnInterrupt()
{
}
2017-01-24 05:32:52 +01:00
private string ExtractUriFromAddressField(string uri, AccessibilityNodeInfo addressNode)
{
2017-01-24 05:32:52 +01:00
if(addressNode != null)
{
2017-01-24 05:32:52 +01:00
uri = addressNode.Text;
if(!uri.Contains("://"))
{
uri = string.Concat("http://", uri);
}
}
2017-01-24 05:32:52 +01:00
return uri;
}
2017-01-31 01:26:39 +01: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 01:26:39 +01:00
if(creds == null)
{
return false;
}
Uri credsUri, lastUri, currentUri;
if(Uri.TryCreate(creds.Uri, UriKind.Absolute, out credsUri) &&
Uri.TryCreate(creds.LastUri, UriKind.Absolute, out lastUri) &&
Uri.TryCreate(currentUriString, UriKind.Absolute, out currentUri) &&
credsUri.Host == currentUri.Host && lastUri.Host == currentUri.Host)
2017-01-24 05:32:52 +01:00
{
return true;
}
return false;
}
2017-01-24 05:32:52 +01:00
private static bool AvailablePasswordField(AccessibilityNodeInfo n, bool isChrome)
{
2017-01-24 05:32:52 +01: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 05:32:52 +01:00
private static bool EditText(AccessibilityNodeInfo n)
{
2017-01-24 05:32:52 +01:00
return n.ClassName != null && n.ClassName.Contains("EditText");
}
2017-01-31 01:26:39 +01:00
private void NotifyToAutofill(string uri)
{
2017-01-24 05:32:52 +01: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 05:58:26 +01:00
builder.SetSmallIcon(Resource.Drawable.notification_sm)
2017-01-24 05:32:52 +01:00
.SetContentTitle("bitwarden Autofill Service")
2017-01-31 01:26:39 +01:00
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
2017-01-24 05:32:52 +01:00
.SetTicker("Tap this notification to autofill a login from your bitwarden vault.")
2017-01-31 01:26:39 +01:00
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetVisibility(NotificationVisibility.Secret)
2017-01-31 05:41:39 +01:00
.SetColor(global::Android.Support.V4.Content.ContextCompat.GetColor(ApplicationContext,
Resource.Color.primary))
2017-01-24 05:32:52 +01:00
.SetContentIntent(pendingIntent);
2017-01-29 05:58:26 +01:00
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
2017-01-24 05:32:52 +01:00
notificationManager.Notify(AutoFillNotificationId, builder.Build());
}
2017-01-24 05:32:52 +01:00
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
{
2017-01-28 05:32:48 +01:00
FillEditText(usernameNode, AutofillActivity.LastCredentials.Username);
2017-01-31 01:26:39 +01:00
foreach(var n in passwordNodes)
2017-01-24 05:32:52 +01:00
{
2017-01-31 01:26:39 +01:00
FillEditText(n, AutofillActivity.LastCredentials.Password);
2017-01-24 05:32:52 +01:00
}
}
2017-01-24 05:32:52 +01:00
private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
{
2017-01-31 01:26:39 +01:00
if(editTextNode == null || value == null)
{
return;
}
2017-01-24 05:32:52 +01:00
var bundle = new Bundle();
bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value);
editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle);
}
2017-01-31 01:26:39 +01:00
private IEnumerable<AccessibilityNodeInfo> GetWindowNodes(AccessibilityNodeInfo n,
AccessibilityEvent e, Func<AccessibilityNodeInfo, bool> p)
{
if(n != null)
{
2017-01-31 01:26:39 +01:00
if(n.WindowId == e.WindowId && !(n.ViewIdResourceName?.StartsWith(SystemUiPackage) ?? false) && p(n))
{
yield return n;
}
for(int i = 0; i < n.ChildCount; i++)
{
2017-01-31 01:26:39 +01:00
foreach(var node in GetWindowNodes(n.GetChild(i), e, p))
{
2017-01-24 05:32:52 +01:00
yield return node;
}
}
}
}
}
2017-01-28 05:32:48 +01:00
}