move accservice based auto fill plugin into own apk to distribte outside Google Play, closes #111

This commit is contained in:
Philipp Crocoll
2017-11-20 12:14:49 +01:00
parent abf5bfdd69
commit 9f7eaf22b0
28 changed files with 878 additions and 230 deletions

3
.gitignore vendored
View File

@@ -149,3 +149,6 @@ intermediates
*.iml
/build
/src/Kp2aKeyboardBinding/Jars
/src/java/Kp2aAccServiceLib/app/build
/src/java/Kp2aAccServiceLib/app/app.iml
/src/java/Kp2aAccServiceLib/gradle

View File

@@ -0,0 +1,5 @@
As of December 2017, Google does not accept the use of Accessibility services for anything except helping people with disabilities. This means that Keepass2Android can no longer provide the accessibility service based AutoFill feature. Otherwise, Google would remove Keepass2Android from Play Store.
If you want to continue using this feature, please [install the Accessibility service based AutoFill plugin] (https://github.com/PhilippC/kp2a_accservice_autofill/releases/).
After installation, please enable the accessibility service "KP2A AutoFillPlugin" in the Android system settings. When trying to use the plugin for the first time, KP2A will ask you if the plugin may access the Keepass database. Please accept this to use the plugin.

View File

@@ -6,6 +6,10 @@ Displays password entries as QR code; can be used to scan QR codes which can the
Allows to switch input method automatically on non-rooted devices.
[https://play.google.com/store/apps/details?id=keepass2android.plugin.keyboardswap2](https://play.google.com/store/apps/details?id=keepass2android.plugin.keyboardswap2)
# AutoFill Plug-in
Uses Android Accessibility Service to provide an option to AutoFill forms (e.g. on Chrome) or any Android app.
[https://philippc.github.io/keepass2android/AccServiceAutoFill.md]
# InputStick Plug-in
Allows to send text from KP2A via InputStick to your PC.
[https://play.google.com/store/apps/details?id=com.inputstick.apps.kp2aplugin](https://play.google.com/store/apps/details?id=com.inputstick.apps.kp2aplugin)

View File

@@ -44,6 +44,7 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Java.Interop" />
<Reference Include="Mono.Android" />
<Reference Include="System" />
<Reference Include="System.Core" />

View File

@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="Keepass2AndroidPluginSDK2" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<afterSyncTasks>
<task>generateDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
<option name="LIBRARY_PROJECT" value="true" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/mockable-Google-Inc.-Google-APIs-23.jar" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" scope="TEST" name="mockable-Google-Inc.-Google-APIs-23" level="project" />
<orderEntry type="library" exported="" name="effects-android-23" level="project" />
<orderEntry type="library" exported="" name="usb-android-23" level="project" />
<orderEntry type="library" exported="" name="maps-android-23" level="project" />
</component>
</module>

View File

@@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion '23.0.2'
defaultConfig {
minSdkVersion 18
targetSdkVersion 23
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
dependencies {
}

View File

@@ -0,0 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="keepass2android.softkeyboard">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-sdk android:targetSdkVersion="14" android:minSdkVersion="14"/>
<application
android:killAfterRestore="false">
<!--service android:name="keepass2android.autofill.AutoFillService"
android:enabled="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accserviceconfig" />
</service-->
</application>
</manifest>

View File

@@ -0,0 +1,439 @@
package keepass2android.autofill;
import android.accessibilityservice.AccessibilityService;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
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;
}
}
boolean isLauncherPackage(CharSequence packageName)
{
return "com.android.systemui".equals(packageName)
|| "com.android.launcher3".equals(packageName);
}
@TargetApi(21)
class SystemUiCondition implements NodeCondition
{
@Override
public boolean check(AccessibilityNodeInfo n) {
return (n.getViewIdResourceName() != null) && (
(n.getViewIdResourceName().startsWith("com.android.systemui")) || (n.getViewIdResourceName().startsWith("com.android.launcher3")));
}
}
private class PasswordFieldCondition implements NodeCondition {
@Override
public boolean check(AccessibilityNodeInfo n) {
return n.isPassword();
}
}
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");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
{
android.util.Log.d(_logTag, "AndroidVersion not supported");
return;
}
handleAccessibilityEvent(event);
}
@TargetApi(21)
private void handleAccessibilityEvent(AccessibilityEvent event) {
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 ( isLauncherPackage(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, "event package is no launcher");
}
if ((packageName != null)
&& (packageName.toString().startsWith("keepass2android.")))
{
android.util.Log.d(_logTag, "don't autofill kp2a.");
return;
}
AccessibilityNodeInfo root = getRootInActiveWindow();
if ( isLauncherPackage(root.getPackageName()) )
{
android.util.Log.d(_logTag, "return, root is from launcher.");
return; //avoid that the notification is cancelled when pulling down notif drawer
}
else
{
android.util.Log.d(_logTag, "root package is no launcher");
}
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 (packageName == "com.sec.android.app.sbrowser")
{
List<AccessibilityNodeInfo> urlFields = root.findAccessibilityNodeInfosByViewId("com.sec.android.app.sbrowser:id/location_bar_edit_text");
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);
}
android.util.Log.d(_logTag, "URL=" + 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());*/
}
}
@TargetApi(21)
private void AskFillPassword(String url)
{
android.util.Log.d("KP2AAF", "asking for password for " + url);
Intent startKp2aIntent = new Intent();
startKp2aIntent.setComponent(new ComponentName(this, "md58dca69cf5ce118dfdacac1ed5b2bbacf.LookupCredentialsActivity"));
if (startKp2aIntent != null)
{
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
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());
}
@TargetApi(21)
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;
}
}
@TargetApi(21)
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);
}
}

View File

@@ -0,0 +1,24 @@
package keepass2android.kbbridge;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import android.text.TextUtils;
public class KeyboardData
{
public static List<StringForTyping> availableFields = new ArrayList<StringForTyping>();
public static String entryName;
public static String entryId;
public static boolean hasData()
{
return !TextUtils.isEmpty(entryId);
}
public static void clear()
{
availableFields.clear();
entryName = entryId = "";
}
}

View File

@@ -0,0 +1,20 @@
package keepass2android.kbbridge;
import java.util.ArrayList;
import java.util.HashMap;
public class KeyboardDataBuilder {
private ArrayList<StringForTyping> availableFields = new ArrayList<StringForTyping>();
public void addString(String key, String displayName, String valueToType)
{
StringForTyping stringToType = new StringForTyping();
stringToType.key = key;
stringToType.displayName = displayName;
stringToType.value = valueToType;
availableFields.add(stringToType);
}
public void commit()
{
KeyboardData.availableFields = this.availableFields;
}
}

View File

@@ -0,0 +1,20 @@
package keepass2android.kbbridge;
public class StringForTyping {
public String key; //internal identifier (PwEntry string field key)
public String displayName; //display name for displaying the key (might be translated)
public String value;
@Override
public StringForTyping clone(){
StringForTyping theClone = new StringForTyping();
theClone.key = key;
theClone.displayName = displayName;
theClone.value = value;
return theClone;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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="LookupTitle">Look up credentials</string>
<string name="ApplicationName">KP2A AutoFillPlugin</string>
<string name="NotificationTitle">Keepass2Android AutoFill</string>
<string name="NotificationContentText">AutoFill form for %1$s</string>
</resources>

View File

@@ -0,0 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}

View File

@@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx1024m

164
src/java/Kp2aAccServiceLib/gradlew vendored Normal file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
src/java/Kp2aAccServiceLib/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,2 @@
include ':app'

View File

@@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Views;
using Android.Widget;
namespace keepass2android
{
[Activity(Label = AppNames.AppName, Theme = "@style/MyTheme_ActionBar")]
public class ActivateAutoFillActivity : LifecycleDebugActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
new ActivityDesign(this).ApplyTheme();
base.OnCreate(savedInstanceState);
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
App.Kp2a.AskYesNoCancel(UiStringKey.ActivateAutoFillService_title,
UiStringKey.ActivateAutoFillService_message,
UiStringKey.ActivateAutoFillService_btnKeyboard,
UiStringKey.ActivateAutoFillService_btnAutoFill,
delegate
{
//yes
CopyToClipboardService.ActivateKeyboard(this);
Finish();
},
delegate
{
//no
Intent intent = new Intent(Android.Provider.Settings.ActionAccessibilitySettings);
StartActivity(intent);
prefs.Edit().PutBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomatically_key), false).Commit();
Toast.MakeText(this, Resource.String.ActivateAutoFillService_toast, ToastLength.Long).Show();
Finish();
},
delegate
{
//cancel
Finish();
},
(sender, args) => Finish() //dismiss
,this);
}
}
}

View File

@@ -1,21 +0,0 @@
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

@@ -31,16 +31,6 @@
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<service android:name="keepass2android.autofill.AutoFillService"
android:enabled="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accserviceconfig" />
</service>
<service android:name="keepass2android.softkeyboard.KP2AKeyboard" android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="99"
android:versionName="1.03-pre1"
android:versionCode="101"
android:versionName="1.03-pre3"
package="keepass2android.keepass2android"
android:installLocation="auto">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
@@ -35,16 +35,6 @@
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<service android:name="keepass2android.autofill.AutoFillService"
android:enabled="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accserviceconfig" />
</service>
<service android:name="keepass2android.softkeyboard.KP2AKeyboard" android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>

View File

@@ -17,16 +17,6 @@
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<service android:name="keepass2android.autofill.AutoFillService"
android:enabled="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accserviceconfig" />
</service>
<service android:name="keepass2android.softkeyboard.KP2AKeyboard" android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>

View File

@@ -32,6 +32,16 @@ namespace keepass2android
private string _requestedUrl;
private string _pluginPackage;
public QueryCredentialsActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public QueryCredentialsActivity()
{
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
@@ -164,6 +174,7 @@ namespace keepass2android
//return credentials to caller:
Intent credentialData = new Intent();
PluginHost.AddEntryToIntent(credentialData, App.Kp2a.GetDb().LastOpenedEntry);
credentialData.PutExtra(Strings.ExtraQueryString,_requestedUrl);
SetResult(Result.Ok, credentialData);
Finish();
}

View File

@@ -366,9 +366,11 @@
<string name="ShowSeparateNotifications_title">Separate notifications</string>
<string name="ShowSeparateNotifications_summary">Show separate notifications for copying username and password to clipboard and activating the keyboard.</string>
<string name="AccServiceAutoFill_prefs">AutoFill Accessibility-Service</string>
<string name="ShowKp2aKeyboardNotification_title">Keyboard/AutoFill notification</string>
<string name="ShowKp2aKeyboardNotification_summary">Make full entry accessible through the KP2A keyboard and AutoFill service (recommended).</string>
<string name="ShowKp2aKeyboardNotification_title">KP2A keyboard notification</string>
<string name="ShowKp2aKeyboardNotification_summary">Make full entry accessible through the KP2A keyboard (recommended).</string>
<string name="OpenKp2aKeyboardAutomatically_title">Switch keyboard</string>
<string name="OpenKp2aKeyboardAutomatically_summary">Open keyboard selection dialog when entry is available through KP2A keyboard after search from the browser.</string>

View File

@@ -374,7 +374,13 @@
/>
</PreferenceScreen>
</PreferenceScreen>
<Preference android:title="@string/AccServiceAutoFill_prefs" >
<intent android:action="android.intent.action.VIEW"
android:data="https://philippc.github.io/keepass2android/AccServiceAutoFill.md" />
</Preference>
</PreferenceScreen>
<PreferenceScreen
android:key="@string/QuickUnlock_prefs_key"
android:title="@string/QuickUnlock_prefs"

View File

@@ -127,7 +127,6 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ActivateAutoFillActivity.cs" />
<Compile Include="EntryActivityClasses\ViewImagePopupItem.cs" />
<Compile Include="ImageViewActivity.cs" />
<Compile Include="addons\OtpKeyProv\EncodingUtil.cs" />
@@ -142,7 +141,6 @@
<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" />

View File

@@ -420,25 +420,24 @@ namespace keepass2android
if (hasKeyboardDataNow)
{
notBuilder.AddKeyboardAccess();
if (closeAfterCreate && Keepass2android.Autofill.AutoFillService.IsAvailable)
if (prefs.GetBoolean("kp2a_switch_rooted", false))
{
if (IsKp2aInputMethodEnabled)
//switch rooted
bool onlySwitchOnSearch = prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomaticallyOnlyAfterSearch_key), false);
if (closeAfterCreate || (!onlySwitchOnSearch))
{
ActivateKeyboardIfAppropriate(closeAfterCreate, prefs);
} else if (Keepass2android.Autofill.AutoFillService.IsRunning)
{
//don't do anything, service is notified
}
else //neither keyboard nor activity service are running/enabled. Ask the user what to do.
{
var i = new Intent(this, typeof(ActivateAutoFillActivity));
i.AddFlags(ActivityFlags.NewTask | ActivityFlags.ClearTask);
StartActivity(i);
prefs.Edit().PutBoolean("has_asked_autofillservice", true).Commit();
ActivateKp2aKeyboard();
}
}
else
{
//if the app is about to be closed again (e.g. after searching for a URL and returning to the browser:
// automatically bring up the Keyboard selection dialog
if ((closeAfterCreate) && prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomatically_key), Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default)))
{
ActivateKp2aKeyboard();
}
}
else ActivateKeyboardIfAppropriate(closeAfterCreate, prefs);
}