diff --git a/src/keepass2android/Credentials.cs b/src/keepass2android/Credentials.cs
new file mode 100644
index 00000000..8f09f772
--- /dev/null
+++ b/src/keepass2android/Credentials.cs
@@ -0,0 +1,21 @@
+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.Widget;
+
+namespace keepass2android.AutoFillPlugin
+{
+ public class Credentials
+ {
+ public string User;
+ public string Password;
+ public string Url;
+ }
+}
\ No newline at end of file
diff --git a/src/keepass2android/Kp2aAccessibilityService.cs b/src/keepass2android/Kp2aAccessibilityService.cs
new file mode 100644
index 00000000..645b58e0
--- /dev/null
+++ b/src/keepass2android/Kp2aAccessibilityService.cs
@@ -0,0 +1,272 @@
+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
+{
+ //
+ [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;
+ if ((ExistsNodeOrChildren(root, n => n.WindowId == e.WindowId) && !ExistsNodeOrChildren(root, n => (n.ViewIdResourceName != null) && (n.ViewIdResourceName.StartsWith("com.android.systemui")))))
+ {
+ bool cancelNotification = true;
+
+ var allEditTexts = GetNodeOrChildren(root, n=> { return IsEditText(n); });
+
+ 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 emptyPasswordFields = GetNodeOrChildren(root, n => { return IsPasswordField(n); }).ToList();
+ 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 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, IEnumerable 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, IEnumerable 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().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 p)
+ {
+ return GetNodeOrChildren(n, p).Any();
+ }
+
+ private IEnumerable GetNodeOrChildren(AccessibilityNodeInfo n, Func p)
+ {
+ if (n != null)
+ {
+ if (p(n))
+ yield return n;
+ for (int i = 0; i < n.ChildCount; i++)
+ {
+ foreach (var x in GetNodeOrChildren(n.GetChild(i), p))
+ yield return x;
+ }
+ }
+
+ }
+
+ public override void OnInterrupt()
+ {
+
+ }
+
+ public void OnCancel(IDialogInterface dialog)
+ {
+
+ }
+
+ public static void NotifyNewData()
+ {
+ _hasUsedData = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/keepass2android/Resources/drawable-xhdpi/ic_notify_autofill.png b/src/keepass2android/Resources/drawable-xhdpi/ic_notify_autofill.png
new file mode 100644
index 00000000..b7ffa486
Binary files /dev/null and b/src/keepass2android/Resources/drawable-xhdpi/ic_notify_autofill.png differ
diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml
index 35026397..55ad7872 100644
--- a/src/keepass2android/Resources/values/strings.xml
+++ b/src/keepass2android/Resources/values/strings.xml
@@ -646,7 +646,12 @@
Disable
Ask after error
Send error reports
-
+
+ Look up credentials
+ KP2A AutoFillPlugin
+ Monitors apps and websites for password fields. Offers to look up credentials from Keepass2Android and auto-fill them into the forms.
+ Keepass2Android AutoFill
+ AutoFill form for %1$s
Version 0.9.8c\n
diff --git a/src/keepass2android/Resources/xml/accserviceconfig.xml b/src/keepass2android/Resources/xml/accserviceconfig.xml
new file mode 100644
index 00000000..ec890aac
--- /dev/null
+++ b/src/keepass2android/Resources/xml/accserviceconfig.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj
index d1dfb0da..911b0e54 100644
--- a/src/keepass2android/keepass2android.csproj
+++ b/src/keepass2android/keepass2android.csproj
@@ -128,6 +128,7 @@
+
@@ -154,6 +155,7 @@
+
@@ -1669,6 +1671,14 @@
+
+
+
+
+
+ Designer
+
+
diff --git a/src/keepass2android/services/CopyToClipboardService.cs b/src/keepass2android/services/CopyToClipboardService.cs
index 278913b5..e6a7e6e1 100644
--- a/src/keepass2android/services/CopyToClipboardService.cs
+++ b/src/keepass2android/services/CopyToClipboardService.cs
@@ -30,6 +30,7 @@ using Android.Preferences;
using KeePassLib;
using KeePassLib.Utility;
using Android.Views.InputMethods;
+using keepass2android.AutoFill;
using KeePass.Util.Spr;
namespace keepass2android
@@ -527,6 +528,8 @@ namespace keepass2android
kbdataBuilder.Commit();
Keepass2android.Kbbridge.KeyboardData.EntryName = entry.OutputStrings.ReadSafe(PwDefs.TitleField);
Keepass2android.Kbbridge.KeyboardData.EntryId = entry.Uuid.ToHexString();
+ if (hasData)
+ Kp2aAccessibilityService.NotifyNewData();
return hasData;
#endif