keepass2android/src/java/KP2ASoftkeyboard_AS/app/src/main/java/keepass2android/autofill/AutoFillService.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);
}
}