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 urlFields = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar"); url = urlFromAddressFields(urlFields, url); } else if ("com.android.browser".equals(root.getPackageName())) { List 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 emptyPasswordFields = new ArrayList<>(); GetNodeOrChildren(root, new PasswordFieldCondition(), emptyPasswordFields); List allEditTexts = new ArrayList<>(); GetNodeOrChildren(root, new EditTextCondition(), allEditTexts); AccessibilityNodeInfo usernameEdit = null; for (int i=0;i passwordFields) { if ((keepass2android.kbbridge.KeyboardData.hasData()) && (_hasUsedData == false)) { fillDataInTextField(usernameEdit, getLastReceivedCredentialsUser()); for (int i=0;i 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 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); } }