From 2c065a04a455d684831380fa81abe7c92d46c2fc Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Thu, 23 Apr 2026 21:02:34 -0700 Subject: [PATCH] Narrow Android autofill chooser results --- .../keepassgo/AutofillTargetMatcher.java | 30 ++++++- .../AutofillCacheStoreBehaviorTest.java | 78 +++++++++++++++++++ docs/android-autofill.md | 19 +++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/androidsrc/org/julianfamily/keepassgo/AutofillTargetMatcher.java b/androidsrc/org/julianfamily/keepassgo/AutofillTargetMatcher.java index 404527d..706da68 100644 --- a/androidsrc/org/julianfamily/keepassgo/AutofillTargetMatcher.java +++ b/androidsrc/org/julianfamily/keepassgo/AutofillTargetMatcher.java @@ -2,6 +2,7 @@ package org.julianfamily.keepassgo; import java.net.URI; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Locale; @@ -45,7 +46,25 @@ final class AutofillTargetMatcher { resolved.add(direct); return resolved; } - return new ArrayList<>(entries); + NormalizedTarget target = normalize(rawTarget); + List exactHost = new ArrayList<>(); + List parentHost = new ArrayList<>(); + for (Entry entry : entries) { + if (entryMatchesHost(entry, target.host)) { + exactHost.add(entry); + continue; + } + if (entryMatchesParentHost(entry, target.host)) { + parentHost.add(entry); + } + } + if (!exactHost.isEmpty()) { + return sortEntries(exactHost); + } + if (!parentHost.isEmpty()) { + return sortEntries(parentHost); + } + return sortEntries(entries); } static NormalizedTarget normalize(String raw) { @@ -186,6 +205,15 @@ final class AutofillTargetMatcher { return new MatchQuality(false, bestPrefixLen); } + private static List sortEntries(List entries) { + List sorted = new ArrayList<>(entries); + sorted.sort(Comparator + .comparing((Entry entry) -> entry.title.toLowerCase(Locale.US)) + .thenComparing(entry -> String.join("/", entry.path).toLowerCase(Locale.US)) + .thenComparing(entry -> entry.id)); + return sorted; + } + static final class NormalizedTarget { final String host; final String path; diff --git a/androidtestsrc/org/julianfamily/keepassgo/AutofillCacheStoreBehaviorTest.java b/androidtestsrc/org/julianfamily/keepassgo/AutofillCacheStoreBehaviorTest.java index 2ba5e77..6c29946 100644 --- a/androidtestsrc/org/julianfamily/keepassgo/AutofillCacheStoreBehaviorTest.java +++ b/androidtestsrc/org/julianfamily/keepassgo/AutofillCacheStoreBehaviorTest.java @@ -8,6 +8,8 @@ public final class AutofillCacheStoreBehaviorTest { public static void main(String[] args) { testFindBestMatchUsesAndroidAppTargets(); testChooserCandidatesCollapseToExactAndroidAppMatch(); + testChooserCandidatesStayScopedToExactHostMatches(); + testChooserCandidatesStayScopedToParentHostMatches(); } private static void testFindBestMatchUsesAndroidAppTargets() { @@ -58,6 +60,74 @@ public final class AutofillCacheStoreBehaviorTest { } } + private static void testChooserCandidatesStayScopedToExactHostMatches() { + List entries = new ArrayList<>(); + entries.add(new AutofillTargetMatcher.Entry( + "bellagio-primary", + "Bellagio Primary", + "dannyocean", + "vault-code", + "bellagio.example.invalid", + "https://bellagio.example.invalid/login", + Arrays.asList("https://bellagio.example.invalid/login"), + Arrays.asList("Crew", "Internet") + )); + entries.add(new AutofillTargetMatcher.Entry( + "bellagio-backup", + "Bellagio Backup", + "rustyryan", + "backup-code", + "bellagio.example.invalid", + "https://bellagio.example.invalid/admin", + Arrays.asList("https://bellagio.example.invalid/admin"), + Arrays.asList("Crew", "Internet") + )); + entries.add(new AutofillTargetMatcher.Entry( + "night-fox-entry", + "Night Fox", + "nightfox", + "vault-code", + "gitlab.com", + "https://gitlab.com", + Arrays.asList("https://gitlab.com"), + Arrays.asList("Crew", "Internet") + )); + + List got = AutofillTargetMatcher.chooserCandidates(entries, "https://bellagio.example.invalid/security"); + if (got.size() != 2 || !containsIDs(got, "bellagio-primary", "bellagio-backup")) { + throw new AssertionError("chooserCandidates(entries, exact host) = " + describe(got) + ", want only Bellagio entries"); + } + } + + private static void testChooserCandidatesStayScopedToParentHostMatches() { + List entries = new ArrayList<>(); + entries.add(new AutofillTargetMatcher.Entry( + "bellagio-parent", + "Bellagio Parent", + "linuscaldwell", + "chip-stack", + "example.invalid", + "https://example.invalid/login", + Arrays.asList("https://example.invalid/login"), + Arrays.asList("Crew", "Internet") + )); + entries.add(new AutofillTargetMatcher.Entry( + "night-fox-entry", + "Night Fox", + "nightfox", + "vault-code", + "gitlab.com", + "https://gitlab.com", + Arrays.asList("https://gitlab.com"), + Arrays.asList("Crew", "Internet") + )); + + List got = AutofillTargetMatcher.chooserCandidates(entries, "https://bellagio.example.invalid/security"); + if (got.size() != 1 || !"bellagio-parent".equals(got.get(0).id)) { + throw new AssertionError("chooserCandidates(entries, parent host) = " + describe(got) + ", want [bellagio-parent]"); + } + } + private static String describe(AutofillTargetMatcher.Entry entry) { if (entry == null) { return "null"; @@ -72,4 +142,12 @@ public final class AutofillCacheStoreBehaviorTest { } return ids.toString(); } + + private static boolean containsIDs(List entries, String... wantIDs) { + List ids = new ArrayList<>(); + for (AutofillTargetMatcher.Entry entry : entries) { + ids.add(entry.id); + } + return ids.containsAll(Arrays.asList(wantIDs)) && ids.size() == wantIDs.length; + } } diff --git a/docs/android-autofill.md b/docs/android-autofill.md index f3829e6..32ef869 100644 --- a/docs/android-autofill.md +++ b/docs/android-autofill.md @@ -57,3 +57,22 @@ Expected behavior: immediately act on. - The pending lookup is consumed once and does not keep reappearing on later launches. + +## Chooser Relevance + +User story: + +- When Android autofill cannot resolve a single direct match, KeePassGO should + still keep the chooser focused on entries that are relevant to the current + site or app. +- The picker should not fall back to an alphabetized dump of unrelated vault + entries when KeePassGO already knows the current host or package target. + +Expected behavior: + +- If multiple entries exactly match the current web host or Android app target, + the chooser shows only those relevant entries. +- If there are no exact matches but there are parent-host matches, the chooser + shows only those related entries. +- KeePassGO falls back to the full chooser list only when it has no related + host or app-target candidates at all.