mirror of
https://github.com/moparisthebest/keepass2android
synced 2024-11-29 04:22:22 -05:00
integrate KP2A Accessibility Service for AutoFill into main app
This commit is contained in:
parent
d4a6e37117
commit
8cda4e8919
21
src/keepass2android/Credentials.cs
Normal file
21
src/keepass2android/Credentials.cs
Normal 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;
|
||||
}
|
||||
}
|
272
src/keepass2android/Kp2aAccessibilityService.cs
Normal file
272
src/keepass2android/Kp2aAccessibilityService.cs
Normal 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 |
@ -646,7 +646,12 @@
|
||||
<string name="ErrorReportDisable">Disable</string>
|
||||
<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
|
||||
|
10
src/keepass2android/Resources/xml/accserviceconfig.xml
Normal file
10
src/keepass2android/Resources/xml/accserviceconfig.xml
Normal 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"
|
||||
|
||||
/>
|
@ -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">
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user