integrate KP2A Accessibility Service for AutoFill into main app

This commit is contained in:
Philipp Crocoll 2016-01-20 21:14:24 +01:00
parent d4a6e37117
commit 8cda4e8919
7 changed files with 322 additions and 1 deletions

View File

@ -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;
}
}

View File

@ -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
{
//<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;
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<AccessibilityNodeInfo> 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<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, IEnumerable<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)
{
return GetNodeOrChildren(n, p).Any();
}
private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> 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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -647,6 +647,11 @@
<string name="ErrorReportAsk">Ask after error</string>
<string name="ErrorReportPrefTitle">Send error reports</string>
<string name="LookupTitle">Look up credentials</string>
<string name="ApplicationName">KP2A AutoFillPlugin</string>
<string name="AutoFillServiceDescription">Monitors apps and websites for password fields. Offers to look up credentials from Keepass2Android and auto-fill them into the forms.</string>
<string name="NotificationTitle">Keepass2Android AutoFill</string>
<string name="NotificationContentText">AutoFill form for %1$s</string>
<string name="ChangeLog_0_9_8c">
Version 0.9.8c\n

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/AutoFillServiceDescription"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
/>

View File

@ -128,6 +128,7 @@
<Compile Include="ChallengeInfo.cs" />
<Compile Include="CreateDatabaseActivity.cs" />
<Compile Include="CreateNewFilename.cs" />
<Compile Include="Credentials.cs" />
<Compile Include="EntryActivityClasses\CopyToClipboardPopupMenuIcon.cs" />
<Compile Include="EntryActivityClasses\ExtraStringView.cs" />
<Compile Include="EntryActivityClasses\GotoUrlMenuItem.cs" />
@ -154,6 +155,7 @@
<Compile Include="icons\DrawableFactory.cs" />
<Compile Include="KeeChallenge.cs" />
<Compile Include="FixedDrawerLayout.cs" />
<Compile Include="Kp2aAccessibilityService.cs" />
<Compile Include="KpEntryTemplatedEdit.cs" />
<Compile Include="MeasuringRelativeLayout.cs" />
<Compile Include="NfcOtpActivity.cs" />
@ -1669,6 +1671,14 @@
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\ic_fp_40px.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\ic_notify_autofill.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\xml\accserviceconfig.xml">
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets" Condition="Exists('..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View File

@ -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