402 lines
14 KiB
Java
402 lines
14 KiB
Java
package keepass2android.autofill;
|
|
|
|
import android.accessibilityservice.AccessibilityService;
|
|
import android.app.Notification;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.Intent;
|
|
import android.content.pm.ApplicationInfo;
|
|
import android.content.pm.PackageManager;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
|
|
import keepass2android.kbbridge.KeyboardData;
|
|
|
|
|
|
/**
|
|
* Created by Philipp on 25.01.2016.
|
|
*/
|
|
public class AutoFillService extends AccessibilityService {
|
|
|
|
|
|
private static boolean _hasUsedData = false;
|
|
private static String _lastSearchUrl;
|
|
private static final String _logTag = "KP2AAF";
|
|
private static boolean _isRunning;
|
|
|
|
private final int autoFillNotificationId = 798810;
|
|
private final String androidAppPrefix = "androidapp://";
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
_isRunning = true;
|
|
android.util.Log.d(_logTag, "OnCreate");
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
_isRunning = false;
|
|
}
|
|
|
|
interface NodeCondition
|
|
{
|
|
boolean check(AccessibilityNodeInfo n);
|
|
}
|
|
|
|
class WindowIdCondition implements NodeCondition
|
|
{
|
|
private int id;
|
|
|
|
public WindowIdCondition(int id)
|
|
{
|
|
this.id = id;
|
|
}
|
|
|
|
@Override
|
|
public boolean check(AccessibilityNodeInfo n) {
|
|
return n.getWindowId() == id;
|
|
}
|
|
}
|
|
|
|
class SystemUiCondition implements NodeCondition
|
|
{
|
|
@Override
|
|
public boolean check(AccessibilityNodeInfo n) {
|
|
return (n.getViewIdResourceName() != null) && (n.getViewIdResourceName().startsWith("com.android.systemui"));
|
|
}
|
|
}
|
|
|
|
private class PasswordFieldCondition implements NodeCondition {
|
|
@Override
|
|
public boolean check(AccessibilityNodeInfo n) {
|
|
return n.isPassword() && (
|
|
(n.getText() == null)
|
|
|| ("".equals(n.getText())));
|
|
}
|
|
}
|
|
|
|
private class EditTextCondition implements NodeCondition {
|
|
@Override
|
|
public boolean check(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.getClassName() != null) && (n.getClassName().toString().toLowerCase().contains("edittext"));
|
|
}
|
|
}
|
|
|
|
|
|
public static boolean isAvailable()
|
|
{
|
|
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
|
|
}
|
|
|
|
public static boolean isRunning()
|
|
{
|
|
return _isRunning;
|
|
}
|
|
|
|
@Override
|
|
public void onAccessibilityEvent(AccessibilityEvent event) {
|
|
android.util.Log.d(_logTag, "OnAccEvent");
|
|
|
|
try
|
|
{
|
|
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|
|| event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
|
{
|
|
CharSequence packageName = event.getPackageName();
|
|
android.util.Log.d(_logTag, "event: " + event.getEventType() + ", package = " + packageName);
|
|
if ( "com.android.systemui".equals(event.getPackageName()) )
|
|
{
|
|
android.util.Log.d(_logTag, "return.");
|
|
return; //avoid that the notification is cancelled when pulling down notif drawer
|
|
}
|
|
else
|
|
{
|
|
android.util.Log.d(_logTag, "no com.android.systemui");
|
|
}
|
|
|
|
if ((packageName != null)
|
|
&& (packageName.toString().startsWith("keepass2android.")))
|
|
{
|
|
android.util.Log.d(_logTag, "don't autofill kp2a.");
|
|
return;
|
|
}
|
|
|
|
AccessibilityNodeInfo root = getRootInActiveWindow();
|
|
int eventWindowId = event.getWindowId();
|
|
if ((ExistsNodeOrChildren(root, new WindowIdCondition(eventWindowId)) && !ExistsNodeOrChildren(root, new SystemUiCondition())))
|
|
{
|
|
boolean cancelNotification = true;
|
|
|
|
String url = androidAppPrefix + root.getPackageName();
|
|
|
|
if ( "com.android.chrome".equals(root.getPackageName()) )
|
|
{
|
|
List<AccessibilityNodeInfo> urlFields = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar");
|
|
url = urlFromAddressFields(urlFields, url);
|
|
}
|
|
else if ("com.android.browser".equals(root.getPackageName()))
|
|
{
|
|
List<AccessibilityNodeInfo> urlFields = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url");
|
|
url = urlFromAddressFields(urlFields, url);
|
|
}
|
|
|
|
if (ExistsNodeOrChildren(root, new PasswordFieldCondition()))
|
|
{
|
|
|
|
if ((getLastReceivedCredentialsUser() != null) &&
|
|
(Objects.equals(url, _lastSearchUrl)
|
|
|| isSame(getCredentialsField("URL"), url)))
|
|
{
|
|
android.util.Log.d(_logTag, "Filling credentials for " + url);
|
|
|
|
List<AccessibilityNodeInfo> emptyPasswordFields = new ArrayList<>();
|
|
GetNodeOrChildren(root, new PasswordFieldCondition(), emptyPasswordFields);
|
|
|
|
List<AccessibilityNodeInfo> allEditTexts = new ArrayList<>();
|
|
GetNodeOrChildren(root, new EditTextCondition(), allEditTexts);
|
|
|
|
AccessibilityNodeInfo usernameEdit = null;
|
|
for (int i=0;i<allEditTexts.size();i++)
|
|
{
|
|
if (allEditTexts.get(i).isPassword() == false)
|
|
{
|
|
usernameEdit = allEditTexts.get(i);
|
|
android.util.Log.d(_logTag, "setting usernameEdit = " + usernameEdit.getText() + " ");
|
|
}
|
|
else break;
|
|
}
|
|
|
|
FillPassword(url, usernameEdit, emptyPasswordFields);
|
|
}
|
|
else
|
|
{
|
|
android.util.Log.d (_logTag, "Notif for " + url );
|
|
AskFillPassword(url);
|
|
cancelNotification = false;
|
|
}
|
|
|
|
}
|
|
if (cancelNotification)
|
|
{
|
|
((NotificationManager)getSystemService(NOTIFICATION_SERVICE)).cancel(autoFillNotificationId);
|
|
android.util.Log.d (_logTag,"Cancel notif");
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
android.util.Log.e(_logTag, (e.toString() == null) ? "(null)" : e.toString() );
|
|
|
|
/*Intent intent = new Intent(Intent.ACTION_SEND);
|
|
intent.setType("message/rfc822");
|
|
String to = "crocoapps@gmail.com";
|
|
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{to});
|
|
intent.putExtra(Intent.EXTRA_SUBJECT, "Error report 7d+");
|
|
intent.putExtra(Intent.EXTRA_TEXT,
|
|
"Please send the following text as an error report to crocoapps@gmail.com. You may also add additional information about the workflow you tried to perform. This will help me improve the app. Thanks! \n"+e.toString() );
|
|
|
|
|
|
Notification.Builder builder = new Notification.Builder(this);
|
|
builder.setSmallIcon(keepass2android.softkeyboard.R.drawable.ic_notify_autofill)
|
|
.setContentText(e.toString())
|
|
.setContentTitle("error information")
|
|
.setWhen(java.lang.System.currentTimeMillis())
|
|
.setContentIntent(PendingIntent.getActivity(this, 0, Intent.createChooser(intent, "Send error report"), PendingIntent.FLAG_CANCEL_CURRENT));
|
|
|
|
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
notificationManager.notify(autoFillNotificationId+1, builder.build());*/
|
|
}
|
|
|
|
}
|
|
|
|
private void AskFillPassword(String url)
|
|
{
|
|
|
|
Intent startKp2aIntent = getPackageManager().getLaunchIntentForPackage(getApplicationContext().getPackageName());
|
|
if (startKp2aIntent != null)
|
|
{
|
|
startKp2aIntent.addCategory(Intent.CATEGORY_LAUNCHER);
|
|
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
String taskName = "SearchUrlTask";
|
|
startKp2aIntent.putExtra("KP2A_APPTASK", taskName);
|
|
startKp2aIntent.putExtra("UrlToSearch", url);
|
|
}
|
|
|
|
|
|
PendingIntent pending = PendingIntent.getActivity(this, 0, startKp2aIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
String targetName = url;
|
|
|
|
if (url.startsWith(androidAppPrefix))
|
|
{
|
|
String packageName = url.substring(androidAppPrefix.length());
|
|
try
|
|
{
|
|
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(packageName, 0);
|
|
targetName = (String) (appInfo != null ? getPackageManager().getApplicationLabel(appInfo) : packageName);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
android.util.Log.d(_logTag, (e.toString() == null) ? "(null)" : e.toString());
|
|
targetName = packageName;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
targetName = getHost(url);
|
|
}
|
|
|
|
|
|
Notification.Builder builder = new Notification.Builder(this);
|
|
//TODO icon
|
|
//TODO plugin icon
|
|
builder.setSmallIcon(keepass2android.softkeyboard.R.drawable.ic_notify_autofill)
|
|
.setContentText(getString(keepass2android.softkeyboard.R.string.NotificationContentText, new Object[]{targetName}))
|
|
.setContentTitle(getString(keepass2android.softkeyboard.R.string.NotificationTitle))
|
|
.setWhen(java.lang.System.currentTimeMillis())
|
|
.setVisibility(Notification.VISIBILITY_SECRET)
|
|
.setContentIntent(pending);
|
|
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
notificationManager.notify(autoFillNotificationId, builder.build());
|
|
|
|
}
|
|
|
|
private void FillPassword(String url, AccessibilityNodeInfo usernameEdit, List<AccessibilityNodeInfo> passwordFields)
|
|
{
|
|
if ((keepass2android.kbbridge.KeyboardData.hasData()) && (_hasUsedData == false))
|
|
{
|
|
fillDataInTextField(usernameEdit, getLastReceivedCredentialsUser());
|
|
for (int i=0;i<passwordFields.size();i++)
|
|
{
|
|
fillDataInTextField(passwordFields.get(i), getLastReceivedCredentialsPassword());
|
|
}
|
|
_hasUsedData = true;
|
|
}
|
|
|
|
|
|
|
|
//LookupCredentialsActivity.LastReceivedCredentials = null;
|
|
}
|
|
|
|
private void fillDataInTextField(AccessibilityNodeInfo edit, String value) {
|
|
if ((value == null) || (edit == null))
|
|
return;
|
|
Bundle b = new Bundle();
|
|
b.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value);
|
|
edit.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, b);
|
|
}
|
|
|
|
|
|
private boolean isSame(String url1, String url2) {
|
|
if (url1 == null)
|
|
return (url2 == null);
|
|
if (url2 == null)
|
|
return (url1 == null);
|
|
|
|
if (url1.startsWith("androidapp://"))
|
|
return url1.equals(url2);
|
|
|
|
return getHost(url1).equals(getHost(url2));
|
|
}
|
|
|
|
private String getHost(String url)
|
|
{
|
|
URI uri = null;
|
|
try {
|
|
uri = new URI(url);
|
|
String domain = uri.getHost();
|
|
if (domain == null)
|
|
return url;
|
|
return domain.startsWith("www.") ? domain.substring(4) : domain;
|
|
} catch (URISyntaxException e) {
|
|
android.util.Log.d(_logTag, "error parsing url: "+ url + e.toString());
|
|
return url;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private String getLastReceivedCredentialsUser() {
|
|
return getCredentialsField("UserName");
|
|
}
|
|
private String getLastReceivedCredentialsPassword() {
|
|
return getCredentialsField("Password");
|
|
}
|
|
|
|
private String getCredentialsField(String key) {
|
|
for (int i=0;i<KeyboardData.availableFields.size();i++)
|
|
{
|
|
if (key.equals(KeyboardData.availableFields.get(i).key))
|
|
{
|
|
if (KeyboardData.availableFields.get(i).value != null)
|
|
return KeyboardData.availableFields.get(i).value;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void GetNodeOrChildren(AccessibilityNodeInfo n, NodeCondition condition, List<AccessibilityNodeInfo> result) {
|
|
if (n != null)
|
|
{
|
|
if (condition.check(n))
|
|
result.add(n);
|
|
for (int i = 0; i < n.getChildCount(); i++)
|
|
{
|
|
GetNodeOrChildren(n.getChild(i), condition, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean ExistsNodeOrChildren(AccessibilityNodeInfo n, NodeCondition condition) {
|
|
if (n == null) return false;
|
|
if (condition.check(n))
|
|
return true;
|
|
for (int i = 0; i < n.getChildCount(); i++)
|
|
{
|
|
if (ExistsNodeOrChildren(n.getChild(i), condition))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private String urlFromAddressFields(List<AccessibilityNodeInfo> urlFields, String url) {
|
|
if (!urlFields.isEmpty())
|
|
{
|
|
AccessibilityNodeInfo addressField = urlFields.get(0);
|
|
CharSequence text = addressField.getText();
|
|
if (text != null)
|
|
{
|
|
url = text.toString();
|
|
if (!url.contains("://"))
|
|
url = "http://" + url;
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
@Override
|
|
public void onInterrupt() {
|
|
|
|
}
|
|
|
|
public static void NotifyNewData(String searchUrl)
|
|
{
|
|
_hasUsedData = false;
|
|
_lastSearchUrl = searchUrl;
|
|
android.util.Log.d(_logTag, "Notify new data: " + searchUrl);
|
|
}
|
|
|
|
}
|