Support Android app targets in autofill service

This commit is contained in:
Joe Julian
2026-04-01 05:56:52 -07:00
parent d9d1cf134d
commit b0721e5af1
2 changed files with 86 additions and 5 deletions
@@ -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;
}
}
}
+54
View File
@@ -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")
}
}