Add Android autofill chooser and app binding
This commit is contained in:
@@ -22,6 +22,10 @@
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/keepassgo_accessibility_service" />
|
||||
</service>
|
||||
<activity
|
||||
android:name="org.julianfamily.keepassgo.KeePassGOAutofillPickerActivity"
|
||||
android:exported="false"
|
||||
android:label="Search KeePassGO" />
|
||||
<provider
|
||||
android:name="org.julianfamily.keepassgo.SharedVaultProvider"
|
||||
android:authorities="org.julianfamily.keepassgo.share"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class AutofillBindingStore {
|
||||
private static final String TAG = "KeePassGOAutofill";
|
||||
|
||||
private AutofillBindingStore() {
|
||||
}
|
||||
|
||||
static String entryIDForTarget(Context context, String rawTarget) {
|
||||
Bindings bindings = read(context);
|
||||
return bindings.apps.getOrDefault(normalize(rawTarget), "");
|
||||
}
|
||||
|
||||
static void rememberBinding(Context context, String rawTarget, String entryID) {
|
||||
String target = normalize(rawTarget);
|
||||
if (target.isEmpty() || entryID == null || entryID.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Bindings bindings = read(context);
|
||||
bindings.updatedAt = Instant.now().toString();
|
||||
bindings.apps.put(target, entryID.trim());
|
||||
write(context, bindings);
|
||||
}
|
||||
|
||||
private static Bindings read(Context context) {
|
||||
File path = path(context);
|
||||
if (!path.isFile()) {
|
||||
return new Bindings();
|
||||
}
|
||||
try (JsonReader reader = new JsonReader(new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
|
||||
Bindings bindings = new Bindings();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
if ("updatedAt".equals(name)) {
|
||||
bindings.updatedAt = nextString(reader);
|
||||
continue;
|
||||
}
|
||||
if ("apps".equals(name)) {
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
bindings.apps.put(normalize(reader.nextName()), nextString(reader));
|
||||
}
|
||||
reader.endObject();
|
||||
continue;
|
||||
}
|
||||
reader.skipValue();
|
||||
}
|
||||
reader.endObject();
|
||||
return bindings;
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to read autofill bindings", err);
|
||||
return new Bindings();
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(Context context, Bindings bindings) {
|
||||
File path = path(context);
|
||||
File parent = path.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
Log.e(TAG, "failed to create autofill binding directory " + parent.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(path, false), StandardCharsets.UTF_8))) {
|
||||
writer.setIndent(" ");
|
||||
writer.beginObject();
|
||||
writer.name("updatedAt").value(bindings.updatedAt);
|
||||
writer.name("apps");
|
||||
writer.beginObject();
|
||||
for (Map.Entry<String, String> entry : bindings.apps.entrySet()) {
|
||||
writer.name(entry.getKey()).value(entry.getValue());
|
||||
}
|
||||
writer.endObject();
|
||||
writer.endObject();
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to write autofill bindings", err);
|
||||
}
|
||||
}
|
||||
|
||||
private static String nextString(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == android.util.JsonToken.NULL) {
|
||||
reader.nextNull();
|
||||
return "";
|
||||
}
|
||||
return reader.nextString();
|
||||
}
|
||||
|
||||
private static File path(Context context) {
|
||||
return new File(new File(context.getFilesDir(), "keepassgo"), "autofill-bindings.json");
|
||||
}
|
||||
|
||||
private static String normalize(String rawTarget) {
|
||||
return rawTarget == null ? "" : rawTarget.trim();
|
||||
}
|
||||
|
||||
private static final class Bindings {
|
||||
String updatedAt = "";
|
||||
final Map<String, String> apps = new LinkedHashMap<>();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -20,18 +21,7 @@ final class AutofillCacheStore {
|
||||
}
|
||||
|
||||
static Entry findBestMatch(Context context, String webDomain) {
|
||||
File cacheFile = findCacheFile(context);
|
||||
if (cacheFile == null) {
|
||||
Log.i(TAG, "autofill cache file not found");
|
||||
return null;
|
||||
}
|
||||
List<Entry> entries;
|
||||
try {
|
||||
entries = readEntries(cacheFile);
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to read autofill cache", err);
|
||||
return null;
|
||||
}
|
||||
List<Entry> entries = readEntries(context);
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
@@ -57,6 +47,50 @@ final class AutofillCacheStore {
|
||||
return chooseEntry(target, parentHost);
|
||||
}
|
||||
|
||||
static Entry findByID(Context context, String entryID) {
|
||||
if (entryID == null || entryID.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (Entry entry : readEntries(context)) {
|
||||
if (entryID.equals(entry.id)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<Entry> chooserCandidates(Context context, String rawTarget) {
|
||||
List<Entry> entries = readEntries(context);
|
||||
if (entries.isEmpty()) {
|
||||
return entries;
|
||||
}
|
||||
Entry direct = findBestMatch(context, rawTarget);
|
||||
if (direct != null) {
|
||||
List<Entry> resolved = new ArrayList<>();
|
||||
resolved.add(direct);
|
||||
return resolved;
|
||||
}
|
||||
entries.sort(Comparator
|
||||
.comparing((Entry entry) -> entry.title.toLowerCase(Locale.US))
|
||||
.thenComparing(entry -> String.join("/", entry.path).toLowerCase(Locale.US))
|
||||
.thenComparing(entry -> entry.id));
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<Entry> readEntries(Context context) {
|
||||
File cacheFile = findCacheFile(context);
|
||||
if (cacheFile == null) {
|
||||
Log.i(TAG, "autofill cache file not found");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return readEntries(cacheFile);
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to read autofill cache", err);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private static File findCacheFile(Context context) {
|
||||
List<File> candidates = new ArrayList<>();
|
||||
File filesDir = context.getFilesDir();
|
||||
@@ -103,16 +137,21 @@ final class AutofillCacheStore {
|
||||
}
|
||||
|
||||
private static Entry readEntry(JsonReader reader) throws IOException {
|
||||
String id = "";
|
||||
String title = "";
|
||||
String username = "";
|
||||
String password = "";
|
||||
String host = "";
|
||||
String url = "";
|
||||
List<String> targets = new ArrayList<>();
|
||||
List<String> path = new ArrayList<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
switch (name) {
|
||||
case "id":
|
||||
id = nextString(reader);
|
||||
break;
|
||||
case "title":
|
||||
title = nextString(reader);
|
||||
break;
|
||||
@@ -135,13 +174,20 @@ final class AutofillCacheStore {
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
case "path":
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
path.add(nextString(reader));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
default:
|
||||
reader.skipValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
return new Entry(title, username, password, host, url, targets);
|
||||
return new Entry(id, title, username, password, host, url, targets, path);
|
||||
}
|
||||
|
||||
private static String nextString(JsonReader reader) throws IOException {
|
||||
@@ -293,20 +339,24 @@ final class AutofillCacheStore {
|
||||
}
|
||||
|
||||
static final class Entry {
|
||||
final String id;
|
||||
final String title;
|
||||
final String username;
|
||||
final String password;
|
||||
final String host;
|
||||
final String url;
|
||||
final List<String> targets;
|
||||
final List<String> path;
|
||||
|
||||
Entry(String title, String username, String password, String host, String url, List<String> targets) {
|
||||
Entry(String id, String title, String username, String password, String host, String url, List<String> targets, List<String> path) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.host = host;
|
||||
this.url = url;
|
||||
this.targets = new ArrayList<>(targets);
|
||||
this.path = new ArrayList<>(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.autofill.AutofillId;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.view.autofill.AutofillValue;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.service.autofill.Dataset;
|
||||
|
||||
public final class KeePassGOAutofillPickerActivity extends Activity {
|
||||
static final String EXTRA_TARGET = "org.julianfamily.keepassgo.extra.AUTOFILL_TARGET";
|
||||
static final String EXTRA_PACKAGE_NAME = "org.julianfamily.keepassgo.extra.AUTOFILL_PACKAGE";
|
||||
static final String EXTRA_USERNAME_ID = "org.julianfamily.keepassgo.extra.USERNAME_ID";
|
||||
static final String EXTRA_PASSWORD_ID = "org.julianfamily.keepassgo.extra.PASSWORD_ID";
|
||||
|
||||
private final List<AutofillCacheStore.Entry> allEntries = new ArrayList<>();
|
||||
private final List<AutofillCacheStore.Entry> visibleEntries = new ArrayList<>();
|
||||
private ArrayAdapter<String> adapter;
|
||||
private String matchTarget = "";
|
||||
private String packageName = "";
|
||||
private AutofillId usernameID;
|
||||
private AutofillId passwordID;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
matchTarget = intent.getStringExtra(EXTRA_TARGET);
|
||||
packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||
usernameID = intent.getParcelableExtra(EXTRA_USERNAME_ID, AutofillId.class);
|
||||
passwordID = intent.getParcelableExtra(EXTRA_PASSWORD_ID, AutofillId.class);
|
||||
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
int padding = dp(16);
|
||||
root.setPadding(padding, padding, padding, padding);
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||
title.setGravity(Gravity.START);
|
||||
title.setPadding(0, 0, 0, dp(8));
|
||||
title.setText(packageName == null || packageName.trim().isEmpty() ? "Search KeePassGO" : "Search KeePassGO for " + packageName);
|
||||
root.addView(title, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
EditText search = new EditText(this);
|
||||
search.setHint("Search entries");
|
||||
root.addView(search, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
allEntries.clear();
|
||||
allEntries.addAll(AutofillCacheStore.chooserCandidates(this, matchTarget));
|
||||
visibleEntries.clear();
|
||||
visibleEntries.addAll(allEntries);
|
||||
|
||||
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, labelsFor(visibleEntries));
|
||||
ListView list = new ListView(this);
|
||||
list.setAdapter(adapter);
|
||||
list.setOnItemClickListener(this::onEntrySelected);
|
||||
|
||||
TextView empty = new TextView(this);
|
||||
empty.setPadding(0, dp(16), 0, 0);
|
||||
empty.setText("No KeePassGO entries are available for autofill.");
|
||||
empty.setVisibility(allEntries.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
list.setVisibility(allEntries.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
root.addView(empty, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
LinearLayout.LayoutParams listParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0
|
||||
);
|
||||
listParams.weight = 1f;
|
||||
listParams.topMargin = dp(12);
|
||||
root.addView(list, listParams);
|
||||
|
||||
search.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterEntries(s == null ? "" : s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(root);
|
||||
}
|
||||
|
||||
private void filterEntries(String rawQuery) {
|
||||
String query = rawQuery == null ? "" : rawQuery.trim().toLowerCase(Locale.US);
|
||||
visibleEntries.clear();
|
||||
if (query.isEmpty()) {
|
||||
visibleEntries.addAll(allEntries);
|
||||
} else {
|
||||
for (AutofillCacheStore.Entry entry : allEntries) {
|
||||
if (entryLabel(entry).toLowerCase(Locale.US).contains(query)) {
|
||||
visibleEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.clear();
|
||||
adapter.addAll(labelsFor(visibleEntries));
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void onEntrySelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position < 0 || position >= visibleEntries.size() || passwordID == null) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
AutofillCacheStore.Entry entry = visibleEntries.get(position);
|
||||
if (matchTarget != null && matchTarget.startsWith("androidapp://")) {
|
||||
AutofillBindingStore.rememberBinding(this, matchTarget, entry.id);
|
||||
}
|
||||
|
||||
Dataset.Builder dataset = new Dataset.Builder();
|
||||
dataset.setId(entry.id);
|
||||
if (usernameID != null && entry.username != null && !entry.username.isEmpty()) {
|
||||
dataset.setValue(usernameID, AutofillValue.forText(entry.username));
|
||||
}
|
||||
dataset.setValue(passwordID, AutofillValue.forText(entry.password));
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset.build());
|
||||
setResult(Activity.RESULT_OK, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
private static List<String> labelsFor(List<AutofillCacheStore.Entry> entries) {
|
||||
List<String> labels = new ArrayList<>(entries.size());
|
||||
for (AutofillCacheStore.Entry entry : entries) {
|
||||
labels.add(entryLabel(entry));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private static String entryLabel(AutofillCacheStore.Entry entry) {
|
||||
StringBuilder label = new StringBuilder();
|
||||
label.append(entry.title == null || entry.title.trim().isEmpty() ? "Untitled entry" : entry.title.trim());
|
||||
if (entry.username != null && !entry.username.trim().isEmpty()) {
|
||||
label.append(" (").append(entry.username.trim()).append(")");
|
||||
}
|
||||
if (entry.path != null && !entry.path.isEmpty()) {
|
||||
label.append(" • ").append(String.join(" / ", entry.path));
|
||||
}
|
||||
return label.toString();
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.assist.AssistStructure;
|
||||
import android.content.Intent;
|
||||
import android.os.CancellationSignal;
|
||||
import android.service.autofill.AutofillService;
|
||||
import android.service.autofill.Dataset;
|
||||
@@ -66,29 +68,21 @@ public final class KeePassGOAutofillService extends AutofillService {
|
||||
return;
|
||||
}
|
||||
|
||||
AutofillCacheStore.Entry entry = AutofillCacheStore.findBestMatch(this, target.matchTarget);
|
||||
AutofillCacheStore.Entry entry = findBoundOrBestMatch(target.matchTarget);
|
||||
if (entry == null) {
|
||||
FillResponse chooser = chooserResponse(target, fields);
|
||||
if (chooser == null) {
|
||||
Log.i(TAG, "no autofill cache match");
|
||||
callback.onSuccess(null);
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "returning chooser dataset for " + target.matchTarget);
|
||||
callback.onSuccess(chooser);
|
||||
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();
|
||||
FillResponse response = directFillResponse(entry, fields);
|
||||
Log.i(TAG, "returning dataset");
|
||||
callback.onSuccess(response);
|
||||
} catch (Exception err) {
|
||||
@@ -103,6 +97,72 @@ public final class KeePassGOAutofillService extends AutofillService {
|
||||
callback.onSuccess();
|
||||
}
|
||||
|
||||
private AutofillCacheStore.Entry findBoundOrBestMatch(String matchTarget) {
|
||||
String entryID = AutofillBindingStore.entryIDForTarget(this, matchTarget);
|
||||
if (!entryID.isEmpty()) {
|
||||
AutofillCacheStore.Entry bound = AutofillCacheStore.findByID(this, entryID);
|
||||
if (bound != null) {
|
||||
return bound;
|
||||
}
|
||||
}
|
||||
return AutofillCacheStore.findBestMatch(this, matchTarget);
|
||||
}
|
||||
|
||||
private FillResponse directFillResponse(AutofillCacheStore.Entry entry, ParsedFields fields) {
|
||||
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);
|
||||
dataset.setId(entry.id);
|
||||
if (fields.usernameId != null) {
|
||||
dataset.setValue(fields.usernameId, AutofillValue.forText(entry.username));
|
||||
}
|
||||
dataset.setValue(fields.passwordId, AutofillValue.forText(entry.password));
|
||||
|
||||
return new FillResponse.Builder()
|
||||
.addDataset(dataset.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private FillResponse chooserResponse(ParsedTarget target, ParsedFields fields) {
|
||||
if (fields.passwordId == null) {
|
||||
return null;
|
||||
}
|
||||
List<AutofillCacheStore.Entry> candidates = AutofillCacheStore.chooserCandidates(this, target.matchTarget);
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RemoteViews presentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
|
||||
presentation.setTextViewText(android.R.id.text1, "Search KeePassGO");
|
||||
|
||||
Intent intent = new Intent(this, KeePassGOAutofillPickerActivity.class);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_TARGET, target.matchTarget);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_PACKAGE_NAME, target.packageName);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_USERNAME_ID, fields.usernameId);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_PASSWORD_ID, fields.passwordId);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
target.matchTarget.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
|
||||
);
|
||||
|
||||
AutofillId[] ids;
|
||||
if (fields.usernameId != null) {
|
||||
ids = new AutofillId[]{fields.usernameId, fields.passwordId};
|
||||
} else {
|
||||
ids = new AutofillId[]{fields.passwordId};
|
||||
}
|
||||
return new FillResponse.Builder()
|
||||
.setAuthentication(ids, pendingIntent.getIntentSender(), presentation)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static ParsedTarget parseWindow(AssistStructure structure, ParsedFields fields) {
|
||||
String domain = "";
|
||||
final int windowCount = structure.getWindowNodeCount();
|
||||
|
||||
Reference in New Issue
Block a user