Narrow Android autofill chooser results
This commit is contained in:
@@ -2,6 +2,7 @@ package org.julianfamily.keepassgo;
|
|||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@@ -45,7 +46,25 @@ final class AutofillTargetMatcher {
|
|||||||
resolved.add(direct);
|
resolved.add(direct);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
return new ArrayList<>(entries);
|
NormalizedTarget target = normalize(rawTarget);
|
||||||
|
List<Entry> exactHost = new ArrayList<>();
|
||||||
|
List<Entry> 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) {
|
static NormalizedTarget normalize(String raw) {
|
||||||
@@ -186,6 +205,15 @@ final class AutofillTargetMatcher {
|
|||||||
return new MatchQuality(false, bestPrefixLen);
|
return new MatchQuality(false, bestPrefixLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<Entry> sortEntries(List<Entry> entries) {
|
||||||
|
List<Entry> 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 {
|
static final class NormalizedTarget {
|
||||||
final String host;
|
final String host;
|
||||||
final String path;
|
final String path;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ public final class AutofillCacheStoreBehaviorTest {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
testFindBestMatchUsesAndroidAppTargets();
|
testFindBestMatchUsesAndroidAppTargets();
|
||||||
testChooserCandidatesCollapseToExactAndroidAppMatch();
|
testChooserCandidatesCollapseToExactAndroidAppMatch();
|
||||||
|
testChooserCandidatesStayScopedToExactHostMatches();
|
||||||
|
testChooserCandidatesStayScopedToParentHostMatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testFindBestMatchUsesAndroidAppTargets() {
|
private static void testFindBestMatchUsesAndroidAppTargets() {
|
||||||
@@ -58,6 +60,74 @@ public final class AutofillCacheStoreBehaviorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void testChooserCandidatesStayScopedToExactHostMatches() {
|
||||||
|
List<AutofillTargetMatcher.Entry> 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<AutofillTargetMatcher.Entry> 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<AutofillTargetMatcher.Entry> 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<AutofillTargetMatcher.Entry> 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) {
|
private static String describe(AutofillTargetMatcher.Entry entry) {
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
return "null";
|
return "null";
|
||||||
@@ -72,4 +142,12 @@ public final class AutofillCacheStoreBehaviorTest {
|
|||||||
}
|
}
|
||||||
return ids.toString();
|
return ids.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean containsIDs(List<AutofillTargetMatcher.Entry> entries, String... wantIDs) {
|
||||||
|
List<String> ids = new ArrayList<>();
|
||||||
|
for (AutofillTargetMatcher.Entry entry : entries) {
|
||||||
|
ids.add(entry.id);
|
||||||
|
}
|
||||||
|
return ids.containsAll(Arrays.asList(wantIDs)) && ids.size() == wantIDs.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,3 +57,22 @@ Expected behavior:
|
|||||||
immediately act on.
|
immediately act on.
|
||||||
- The pending lookup is consumed once and does not keep reappearing on later
|
- The pending lookup is consumed once and does not keep reappearing on later
|
||||||
launches.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user