Files
keepassgo/androidsrc/org/julianfamily/keepassgo/KeePassGOAutofillService.java
T
2026-04-01 05:56:52 -07:00

216 lines
7.7 KiB
Java

package org.julianfamily.keepassgo;
import android.app.assist.AssistStructure;
import android.os.CancellationSignal;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
import android.service.autofill.FillCallback;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.SaveCallback;
import android.service.autofill.SaveRequest;
import android.text.InputType;
import android.util.Log;
import android.view.View;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;
import java.util.List;
public final class KeePassGOAutofillService extends AutofillService {
private static final String TAG = "KeePassGOAutofill";
private static final String APP_SCHEME = "androidapp://";
@Override
public void onConnected() {
super.onConnected();
Log.i(TAG, "service connected");
}
@Override
public void onDisconnected() {
Log.i(TAG, "service disconnected");
super.onDisconnected();
}
@Override
public void onFillRequest(
FillRequest request,
CancellationSignal cancellationSignal,
FillCallback callback
) {
try {
Log.i(TAG, "fill request flags=" + request.getFlags());
List<FillContext> contexts = request.getFillContexts();
if (contexts.isEmpty()) {
Log.i(TAG, "fill request had no contexts");
callback.onSuccess(null);
return;
}
AssistStructure structure = contexts.get(contexts.size() - 1).getStructure();
ParsedFields fields = new ParsedFields();
ParsedTarget target = parseWindow(structure, fields);
Log.i(
TAG,
"parsed target=" + target.matchTarget
+ " package=" + target.packageName
+ " usernameId=" + fields.usernameId
+ " passwordId=" + fields.passwordId
);
if (fields.passwordId == null) {
Log.i(TAG, "no password field found");
callback.onSuccess(null);
return;
}
AutofillCacheStore.Entry entry = AutofillCacheStore.findBestMatch(this, target.matchTarget);
if (entry == null) {
Log.i(TAG, "no autofill cache match");
callback.onSuccess(null);
return;
}
Log.i(TAG, "matched entry title=" + entry.title + " user=" + entry.username + " host=" + entry.host);
RemoteViews presentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
presentation.setTextViewText(
android.R.id.text1,
entry.title + " (" + entry.username + ")"
);
Dataset.Builder dataset = new Dataset.Builder(presentation);
if (fields.usernameId != null) {
dataset.setValue(fields.usernameId, AutofillValue.forText(entry.username));
}
dataset.setValue(fields.passwordId, AutofillValue.forText(entry.password));
FillResponse response = new FillResponse.Builder()
.addDataset(dataset.build())
.build();
Log.i(TAG, "returning dataset");
callback.onSuccess(response);
} catch (Exception err) {
Log.e(TAG, "fill request failed", err);
callback.onFailure(err.getMessage());
}
}
@Override
public void onSaveRequest(SaveRequest request, SaveCallback callback) {
Log.i(TAG, "save request");
callback.onSuccess();
}
private static ParsedTarget parseWindow(AssistStructure structure, ParsedFields fields) {
String domain = "";
final int windowCount = structure.getWindowNodeCount();
for (int i = 0; i < windowCount; i++) {
AssistStructure.ViewNode root = structure.getWindowNodeAt(i).getRootViewNode();
String next = parseNode(root, fields);
if (!next.isEmpty()) {
domain = next;
}
}
String packageName = "";
if (structure.getActivityComponent() != null) {
packageName = structure.getActivityComponent().getPackageName();
}
if (!domain.isEmpty()) {
return new ParsedTarget(domain, packageName);
}
if (packageName != null && !packageName.isEmpty()) {
return new ParsedTarget(APP_SCHEME + packageName, packageName);
}
return new ParsedTarget("", "");
}
private static String parseNode(AssistStructure.ViewNode node, ParsedFields fields) {
String domain = "";
if (node.getWebDomain() != null) {
domain = node.getWebDomain();
}
AutofillId id = node.getAutofillId();
if (id != null) {
if (fields.passwordId == null && isPasswordNode(node)) {
fields.passwordId = id;
} else if (fields.usernameId == null && isUsernameNode(node)) {
fields.usernameId = id;
}
}
for (int i = 0; i < node.getChildCount(); i++) {
String childDomain = parseNode(node.getChildAt(i), fields);
if (!childDomain.isEmpty()) {
domain = childDomain;
}
}
return domain;
}
private static boolean isPasswordNode(AssistStructure.ViewNode node) {
int inputType = node.getInputType();
int variation = inputType & InputType.TYPE_MASK_VARIATION;
if (variation == InputType.TYPE_TEXT_VARIATION_PASSWORD
|| variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|| variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) {
return true;
}
return nodeMatches(node, "password");
}
private static boolean isUsernameNode(AssistStructure.ViewNode node) {
int autofillType = node.getAutofillType();
if (autofillType != View.AUTOFILL_TYPE_TEXT) {
return false;
}
if (isPasswordNode(node)) {
return false;
}
return nodeMatches(node, "username")
|| nodeMatches(node, "email")
|| nodeMatches(node, "login")
|| nodeMatches(node, "user");
}
private static boolean nodeMatches(AssistStructure.ViewNode node, String term) {
String lowerTerm = term.toLowerCase();
String[] hints = node.getAutofillHints();
if (hints != null) {
for (String hint : hints) {
if (hint != null && hint.toLowerCase().contains(lowerTerm)) {
return true;
}
}
}
if (containsLower(node.getHint(), lowerTerm)
|| containsLower(node.getIdEntry(), lowerTerm)
|| containsLower(node.getText(), lowerTerm)
|| containsLower(node.getContentDescription(), lowerTerm)) {
return true;
}
return false;
}
private static boolean containsLower(CharSequence value, String term) {
return value != null && value.toString().toLowerCase().contains(term);
}
private static final class ParsedFields {
AutofillId usernameId;
AutofillId passwordId;
}
private static final class ParsedTarget {
final String matchTarget;
final String packageName;
ParsedTarget(String matchTarget, String packageName) {
this.matchTarget = matchTarget;
this.packageName = packageName;
}
}
}