From b0721e5af109262dac50f79279dad397f041bb93 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Wed, 1 Apr 2026 05:56:52 -0700 Subject: [PATCH] Support Android app targets in autofill service --- .../keepassgo/KeePassGOAutofillService.java | 37 +++++++++++-- autofillcache/cache_test.go | 54 +++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/androidsrc/org/julianfamily/keepassgo/KeePassGOAutofillService.java b/androidsrc/org/julianfamily/keepassgo/KeePassGOAutofillService.java index a2a48de..ecd83db 100644 --- a/androidsrc/org/julianfamily/keepassgo/KeePassGOAutofillService.java +++ b/androidsrc/org/julianfamily/keepassgo/KeePassGOAutofillService.java @@ -21,6 +21,7 @@ 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() { @@ -51,15 +52,21 @@ public final class KeePassGOAutofillService extends AutofillService { AssistStructure structure = contexts.get(contexts.size() - 1).getStructure(); ParsedFields fields = new ParsedFields(); - String webDomain = parseWindow(structure, fields); - Log.i(TAG, "parsed domain=" + webDomain + " usernameId=" + fields.usernameId + " passwordId=" + fields.passwordId); + 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, webDomain); + AutofillCacheStore.Entry entry = AutofillCacheStore.findBestMatch(this, target.matchTarget); if (entry == null) { Log.i(TAG, "no autofill cache match"); callback.onSuccess(null); @@ -96,7 +103,7 @@ public final class KeePassGOAutofillService extends AutofillService { callback.onSuccess(); } - private static String parseWindow(AssistStructure structure, ParsedFields fields) { + private static ParsedTarget parseWindow(AssistStructure structure, ParsedFields fields) { String domain = ""; final int windowCount = structure.getWindowNodeCount(); for (int i = 0; i < windowCount; i++) { @@ -106,7 +113,17 @@ public final class KeePassGOAutofillService extends AutofillService { domain = next; } } - return domain; + 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) { @@ -185,4 +202,14 @@ public final class KeePassGOAutofillService extends AutofillService { 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; + } + } } diff --git a/autofillcache/cache_test.go b/autofillcache/cache_test.go index f892cb0..face179 100644 --- a/autofillcache/cache_test.go +++ b/autofillcache/cache_test.go @@ -181,3 +181,57 @@ func TestMatchChoosesLongestPathPrefix(t *testing.T) { t.Fatalf("Match() entry = %q, want two", got.ID) } } + +func TestMatchSupportsAndroidAppPackageTargets(t *testing.T) { + t.Parallel() + + cache := File{ + Entries: []Entry{ + { + ID: "one", + Title: "Thunderbird", + Username: "mail-user", + Password: "secret1", + URL: "androidapp://org.mozilla.thunderbird/login", + Host: "org.mozilla.thunderbird", + }, + }, + } + + got, ok := Match(cache, "androidapp://org.mozilla.thunderbird") + if !ok { + t.Fatalf("Match() found no entry") + } + if got.ID != "one" { + t.Fatalf("Match() entry = %q, want one", got.ID) + } +} + +func TestMatchRejectsAmbiguousAndroidAppPackageTargets(t *testing.T) { + t.Parallel() + + cache := File{ + Entries: []Entry{ + { + ID: "one", + Title: "Thunderbird Primary", + Username: "mail-user", + Password: "secret1", + URL: "androidapp://org.mozilla.thunderbird", + Host: "org.mozilla.thunderbird", + }, + { + ID: "two", + Title: "Thunderbird Secondary", + Username: "other-user", + Password: "secret2", + URL: "androidapp://org.mozilla.thunderbird", + Host: "org.mozilla.thunderbird", + }, + }, + } + + if _, ok := Match(cache, "androidapp://org.mozilla.thunderbird"); ok { + t.Fatalf("Match() unexpectedly resolved ambiguous android app package target") + } +}