286 lines
10 KiB
C#
286 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
using Android.App;
|
|
using Android.Content;
|
|
using Android.OS;
|
|
using Android.Runtime;
|
|
using Android.Views;
|
|
using Android.Views.Accessibility;
|
|
using Android.Widget;
|
|
using KeePassLib;
|
|
|
|
|
|
namespace keepass2android.AutoFill
|
|
{
|
|
//<meta-data android:name="android.accessibilityservice" android:resource="@xml/serviceconfig" />
|
|
[Service(Enabled =true, Permission= "android.permission.BIND_ACCESSIBILITY_SERVICE")]
|
|
[IntentFilter(new[] { "android.accessibilityservice.AccessibilityService" })]
|
|
[MetaData("android.accessibilityservice", Resource = "@xml/accserviceconfig")]
|
|
public class Kp2aAccessibilityService : Android.AccessibilityServices.AccessibilityService
|
|
{
|
|
private static bool _hasUsedData;
|
|
const string _logTag = "KP2AAS";
|
|
private const int autoFillNotificationId = 98810;
|
|
private const string androidAppPrefix = "androidapp://";
|
|
|
|
public override void OnCreate()
|
|
{
|
|
base.OnCreate();
|
|
Android.Util.Log.Debug(_logTag, "OnCreate Service");
|
|
}
|
|
|
|
protected override void OnServiceConnected()
|
|
{
|
|
Android.Util.Log.Debug(_logTag, "service connected");
|
|
base.OnServiceConnected();
|
|
}
|
|
|
|
public override void OnAccessibilityEvent(AccessibilityEvent e)
|
|
{
|
|
|
|
Android.Util.Log.Debug(_logTag, "OnAccEvent");
|
|
if (e.EventType == EventTypes.WindowContentChanged || e.EventType == EventTypes.WindowStateChanged)
|
|
{
|
|
Android.Util.Log.Debug(_logTag, "event: " + e.EventType + ", package = " + e.PackageName);
|
|
if (e.PackageName == "com.android.systemui")
|
|
return; //avoid that the notification is cancelled when pulling down notif drawer
|
|
var root = RootInActiveWindow;
|
|
int eventWindowId = e.WindowId;
|
|
if ((ExistsNodeOrChildren(root, n => n.WindowId == eventWindowId) && !ExistsNodeOrChildren(root, IsSystemUi)))
|
|
{
|
|
bool cancelNotification = true;
|
|
|
|
var allEditTexts = GetNodeOrChildren(root, IsEditText);
|
|
|
|
var usernameEdit = allEditTexts.TakeWhile(edit => (edit.Password == false)).LastOrDefault();
|
|
|
|
string searchString = androidAppPrefix + root.PackageName;
|
|
|
|
string url = androidAppPrefix + root.PackageName;
|
|
|
|
if (root.PackageName == "com.android.chrome")
|
|
{
|
|
var addressField = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar").FirstOrDefault();
|
|
UrlFromAddressField(ref url, addressField);
|
|
|
|
}
|
|
else if (root.PackageName == "com.android.browser")
|
|
{
|
|
var addressField = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url").FirstOrDefault();
|
|
UrlFromAddressField(ref url, addressField);
|
|
}
|
|
|
|
List<AccessibilityNodeInfo> emptyPasswordFields = GetNodeOrChildren(root, IsPasswordField);
|
|
if (emptyPasswordFields.Any())
|
|
{
|
|
if ((LastReceivedCredentialsUser != null) && IsSame(GetCredentialsField(PwDefs.UrlField), url))
|
|
{
|
|
Android.Util.Log.Debug ("KP2AAS", "Filling credentials for " + url);
|
|
|
|
FillPassword(url, usernameEdit, emptyPasswordFields);
|
|
}
|
|
else
|
|
{
|
|
Android.Util.Log.Debug ("KP2AAS", "Notif for " + url );
|
|
if (LastReceivedCredentialsUser != null)
|
|
{
|
|
Android.Util.Log.Debug ("KP2AAS", GetCredentialsField(PwDefs.UrlField));
|
|
Android.Util.Log.Debug ("KP2AAS", url);
|
|
}
|
|
|
|
AskFillPassword(url, usernameEdit, emptyPasswordFields);
|
|
cancelNotification = false;
|
|
}
|
|
|
|
}
|
|
if (cancelNotification)
|
|
{
|
|
((NotificationManager)GetSystemService(NotificationService)).Cancel(autoFillNotificationId);
|
|
Android.Util.Log.Debug ("KP2AAS","Cancel notif");
|
|
}
|
|
}
|
|
|
|
}
|
|
GC.Collect();
|
|
|
|
|
|
}
|
|
|
|
private bool IsSystemUi(AccessibilityNodeInfo n)
|
|
{
|
|
return (n.ViewIdResourceName != null) && (n.ViewIdResourceName.StartsWith("com.android.systemui"));
|
|
}
|
|
|
|
private static void UrlFromAddressField(ref string url, AccessibilityNodeInfo addressField)
|
|
{
|
|
if (addressField != null)
|
|
{
|
|
url = addressField.Text;
|
|
if (!url.Contains("://"))
|
|
url = "http://" + url;
|
|
}
|
|
|
|
}
|
|
|
|
private bool IsSame(string url1, string url2)
|
|
{
|
|
if (url1.StartsWith ("androidapp://"))
|
|
return url1 == url2;
|
|
return KeePassLib.Utility.UrlUtil.GetHost (url1) == KeePassLib.Utility.UrlUtil.GetHost (url2);
|
|
}
|
|
|
|
private static bool IsPasswordField(AccessibilityNodeInfo n)
|
|
{
|
|
//if (n.Password) Android.Util.Log.Debug(_logTag, "pwdx with " + (n.Text == null ? "null" : n.Text));
|
|
var res = n.Password && string.IsNullOrEmpty(n.Text);
|
|
// if (n.Password) Android.Util.Log.Debug(_logTag, "pwd with " + n.Text + res);
|
|
return res;
|
|
}
|
|
|
|
private static bool IsEditText(AccessibilityNodeInfo n)
|
|
{
|
|
//it seems like n.Editable is not a good check as this is false for some fields which are actually editable, at least in tests with Chrome.
|
|
return (n.ClassName != null) && (n.ClassName.Contains("EditText"));
|
|
}
|
|
|
|
private void AskFillPassword(string url, AccessibilityNodeInfo usernameEdit, List<AccessibilityNodeInfo> passwordFields)
|
|
{
|
|
|
|
Intent startKp2aIntent = PackageManager.GetLaunchIntentForPackage(ApplicationContext.PackageName);
|
|
if (startKp2aIntent != null)
|
|
{
|
|
startKp2aIntent.AddCategory(Intent.CategoryLauncher);
|
|
startKp2aIntent.AddFlags(ActivityFlags.NewTask | ActivityFlags.ClearTask);
|
|
string taskName = "SearchUrlTask";
|
|
startKp2aIntent.PutExtra("KP2A_APPTASK", taskName);
|
|
startKp2aIntent.PutExtra("UrlToSearch", url);
|
|
}
|
|
|
|
|
|
var pending = PendingIntent.GetActivity(this, 0, startKp2aIntent, PendingIntentFlags.UpdateCurrent);
|
|
var targetName = url;
|
|
|
|
if (url.StartsWith(androidAppPrefix))
|
|
{
|
|
var packageName = url.Substring(androidAppPrefix.Length);
|
|
try
|
|
{
|
|
var appInfo = PackageManager.GetApplicationInfo(packageName, 0);
|
|
targetName = (string) (appInfo != null ? PackageManager.GetApplicationLabel(appInfo) : packageName);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Android.Util.Log.Debug(_logTag, e.ToString());
|
|
targetName = packageName;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
targetName = KeePassLib.Utility.UrlUtil.GetHost(url);
|
|
}
|
|
|
|
|
|
var builder = new Notification.Builder(this);
|
|
//TODO icon
|
|
//TODO plugin icon
|
|
builder.SetSmallIcon(Resource.Drawable.ic_notify_autofill)
|
|
.SetContentText(GetString(Resource.String.NotificationContentText, new Java.Lang.Object[] { targetName }))
|
|
.SetContentTitle(GetString(Resource.String.NotificationTitle))
|
|
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
|
|
.SetVisibility(Android.App.NotificationVisibility.Secret)
|
|
.SetContentIntent(pending);
|
|
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
|
|
notificationManager.Notify(autoFillNotificationId, builder.Build());
|
|
|
|
}
|
|
|
|
private void FillPassword(string url, AccessibilityNodeInfo usernameEdit, List<AccessibilityNodeInfo> passwordFields)
|
|
{
|
|
if ((Keepass2android.Kbbridge.KeyboardData.HasData) && (_hasUsedData == false))
|
|
{
|
|
FillDataInTextField(usernameEdit, LastReceivedCredentialsUser);
|
|
foreach (var pwd in passwordFields)
|
|
FillDataInTextField(pwd, LastReceivedCredentialsPassword);
|
|
|
|
_hasUsedData = true;
|
|
}
|
|
|
|
|
|
|
|
//LookupCredentialsActivity.LastReceivedCredentials = null;
|
|
}
|
|
|
|
public string LastReceivedCredentialsPassword
|
|
{
|
|
get { return GetCredentialsField(PwDefs.PasswordField); }
|
|
}
|
|
|
|
public string GetCredentialsField(string key)
|
|
{
|
|
var field = Keepass2android.Kbbridge.KeyboardData.AvailableFields
|
|
.Cast<Keepass2android.Kbbridge.StringForTyping>().SingleOrDefault(x => x.Key == key);
|
|
if (field == null)
|
|
return null;
|
|
return field.Value;
|
|
}
|
|
|
|
public string LastReceivedCredentialsUser
|
|
{
|
|
get { return GetCredentialsField(PwDefs.UserNameField); }
|
|
}
|
|
|
|
private static void FillDataInTextField(AccessibilityNodeInfo edit, string newValue)
|
|
{
|
|
if (newValue == null)
|
|
return;
|
|
Bundle b = new Bundle();
|
|
b.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, newValue);
|
|
edit.PerformAction(Android.Views.Accessibility.Action.SetText, b);
|
|
}
|
|
|
|
private bool ExistsNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
|
|
{
|
|
if (p(n))
|
|
return true;
|
|
for (int i = 0; i < n.ChildCount; i++)
|
|
{
|
|
if (ExistsNodeOrChildren(n.GetChild(i), p))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private List<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
|
|
{
|
|
List<AccessibilityNodeInfo> result = new List<AccessibilityNodeInfo>();
|
|
if (n != null)
|
|
{
|
|
if (p(n))
|
|
result.Add(n);
|
|
for (int i = 0; i < n.ChildCount; i++)
|
|
{
|
|
result.AddRange(GetNodeOrChildren(n.GetChild(i), p));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public override void OnInterrupt()
|
|
{
|
|
|
|
}
|
|
|
|
public void OnCancel(IDialogInterface dialog)
|
|
{
|
|
|
|
}
|
|
|
|
public static void NotifyNewData()
|
|
{
|
|
_hasUsedData = false;
|
|
}
|
|
}
|
|
} |