Update sample IME and autofill provider to use the new support lib APIs

* refactor the autofill provider code
* keyboard now uses wrap_content when inflating

Test: manual
Bug: 154178486

Change-Id: Ie540178d8f063e3a8e9afb76284b15ab40ada344
This commit is contained in:
Feng Cao
2020-04-27 16:30:59 -07:00
parent 44b11dd142
commit 0c501136ca
9 changed files with 577 additions and 253 deletions

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/transparent">
<item
android:bottom="4dp"
android:left="4dp"
android:right="4dp"
android:shape="rectangle"
android:top="4dp">
<shape>
<corners android:radius="32dp" />
<solid android:color="#1F000000" />
</shape>
</item>
<item
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:shape="rectangle"
android:top="5dp">
<shape>
<corners android:radius="32dp" />
<solid android:color="#FFFFFFFF" />
</shape>
</item>
</ripple>

View File

@@ -16,11 +16,16 @@
package com.example.android.autofillkeyboard;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.Size;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -32,6 +37,14 @@ import android.view.inputmethod.InlineSuggestionsRequest;
import android.view.inputmethod.InlineSuggestionsResponse;
import android.widget.Toast;
import androidx.autofill.inline.UiVersions;
import androidx.autofill.inline.UiVersions.StylesBuilder;
import androidx.autofill.inline.common.ImageViewStyle;
import androidx.autofill.inline.common.TextViewStyle;
import androidx.autofill.inline.common.ViewStyle;
import androidx.autofill.inline.v1.InlineSuggestionUi;
import androidx.autofill.inline.v1.InlineSuggestionUi.Style;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -130,17 +143,58 @@ public class AutofillImeService extends InputMethodService {
@Override
public InlineSuggestionsRequest onCreateInlineSuggestionsRequest(Bundle uiExtras) {
Log.d(TAG, "onCreateInlineSuggestionsRequest() called");
StylesBuilder stylesBuilder = UiVersions.newStylesBuilder();
Style style = InlineSuggestionUi.newStyleBuilder()
.setSingleIconChipStyle(
new ViewStyle.Builder()
.setBackground(
Icon.createWithResource(this, R.drawable.chip_background))
.setPadding(0, 0, 0, 0)
.build())
.setChipStyle(
new ViewStyle.Builder()
.setBackground(
Icon.createWithResource(this, R.drawable.chip_background))
.setPadding(toPixel(5 + 8), 0, toPixel(5 + 8), 0)
.build())
.setStartIconStyle(new ImageViewStyle.Builder().setLayoutMargin(0, 0, 0, 0).build())
.setTitleStyle(
new TextViewStyle.Builder()
.setLayoutMargin(toPixel(4), 0, toPixel(4), 0)
.setTextColor(Color.parseColor("#FF202124"))
.setTextSize(16)
.build())
.setSubtitleStyle(
new TextViewStyle.Builder()
.setLayoutMargin(0, 0, toPixel(4), 0)
.setTextColor(Color.parseColor("#99202124")) // 60% opacity
.setTextSize(14)
.build())
.setEndIconStyle(new ImageViewStyle.Builder().setLayoutMargin(0, 0, 0, 0).build())
.build();
stylesBuilder.addStyle(style);
Bundle stylesBundle = stylesBuilder.build();
final ArrayList<InlinePresentationSpec> presentationSpecs = new ArrayList<>();
presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100),
new Size(400, 100)).build());
presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, 100),
new Size(400, 100)).build());
presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, getHeight()),
new Size(740, getHeight())).setStyle(stylesBundle).build());
presentationSpecs.add(new InlinePresentationSpec.Builder(new Size(100, getHeight()),
new Size(740, getHeight())).setStyle(stylesBundle).build());
return new InlineSuggestionsRequest.Builder(presentationSpecs)
.setMaxSuggestionCount(6)
.build();
}
private int toPixel(int dp) {
return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dp,
getResources().getDisplayMetrics());
}
private int getHeight() {
return getResources().getDimensionPixelSize(R.dimen.keyboard_header_height);
}
@Override
public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
Log.d(TAG, "onInlineSuggestionsResponse() called");
@@ -224,13 +278,12 @@ public class AutofillImeService extends InputMethodService {
for (int i = 0; i < totalSuggestionsCount; i++) {
final int index = i;
final InlineSuggestion inlineSuggestion = inlineSuggestions.get(i);
final Size size = inlineSuggestion.getInfo().getInlinePresentationSpec().getMaxSize();
final Size size = new Size(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
inlineSuggestion.inflate(this, size, executor, suggestionView -> {
Log.d(TAG, "new inline suggestion view ready");
if(suggestionView != null) {
suggestionView.setLayoutParams(new ViewGroup.LayoutParams(
size.getWidth(), size.getHeight()));
suggestionView.setOnClickListener((v) -> {
Log.d(TAG, "Received click on the suggestion");
});

View File

@@ -23,5 +23,9 @@
android:name=".SettingsActivity"
android:label="Autofill Settings"
android:exported="true"/>
<activity android:name=".AttributionDialogActivity"
android:label="Autofill Attribution"
android:theme="@android:style/Theme.Material.Light.Dialog.NoActionBar"/>
</application>
</manifest>

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foo.bar.inline;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
/**
* The activity which will be open when the inline suggestion is long pressed. It shows a dialog
* that describes the source of the suggestion.
*/
public class AttributionDialogActivity extends Activity {
static final String KEY_MSG = "AttributionDialogActivity:msg";
static final String DEFAULT_MSG = "Hello";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
Intent intent = getIntent();
String msg = DEFAULT_MSG;
if (intent != null) {
msg = intent.getStringExtra(KEY_MSG);
}
Dialog dialog = createDialog(msg);
dialog.setOnDismissListener(dialog1 -> finish());
dialog.show();
}
private Dialog createDialog(String msg) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder
.setMessage("The suggestions are generated by the InlineFillService. " + msg)
.setNegativeButton(
"Settings",
(dialog, id) -> {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
})
.setPositiveButton(
"Got it",
(dialog, id) -> {
// User cancelled the dialog
});
return builder.create();
}
}

View File

@@ -34,6 +34,8 @@ import android.view.inputmethod.InlineSuggestionsRequest;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Optional;
/**
* Activity used for autofill authentication, it simply sets the dataste upon tapping OK.
*/
@@ -75,7 +77,7 @@ public class AuthActivity extends Activity {
}
FillResponse response =
InlineFillService.createResponse(this, fields, 1, authenticateDatasets,
inlineRequest);
Optional.ofNullable(inlineRequest));
replyIntent.putExtra(EXTRA_AUTHENTICATION_RESULT, response);
}
setResult(RESULT_OK, replyIntent);
@@ -94,7 +96,7 @@ public class AuthActivity extends Activity {
public static IntentSender newIntentSenderForResponse(@NonNull Context context,
@NonNull String[] hints, @NonNull AutofillId[] ids, boolean authenticateDatasets,
@NonNull InlineSuggestionsRequest inlineRequest) {
@Nullable InlineSuggestionsRequest inlineRequest) {
return newIntentSender(context, null, hints, ids, authenticateDatasets, inlineRequest);
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foo.bar.inline;
import static foo.bar.inline.InlineFillService.TAG;
import android.app.assist.AssistStructure;
import android.content.Context;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.util.ArrayMap;
import android.util.Log;
import android.view.View;
import android.view.autofill.AutofillId;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Map;
final class Helper {
/**
* Displays a toast with the given message.
*/
static void showMessage(@NonNull Context context, @NonNull CharSequence message) {
Log.i(TAG, message.toString());
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
/**
* Extracts the autofillable fields from the request through assist structure.
*/
static ArrayMap<String, AutofillId> getAutofillableFields(@NonNull FillRequest request) {
AssistStructure structure = getLatestAssistStructure(request);
return getAutofillableFields(structure);
}
/**
* Helper method to get the {@link AssistStructure} associated with the latest request
* in an autofill context.
*/
@NonNull
private static AssistStructure getLatestAssistStructure(@NonNull FillRequest request) {
List<FillContext> fillContexts = request.getFillContexts();
return fillContexts.get(fillContexts.size() - 1).getStructure();
}
/**
* Parses the {@link AssistStructure} representing the activity being autofilled, and returns a
* map of autofillable fields (represented by their autofill ids) mapped by the hint associate
* with them.
*
* <p>An autofillable field is a {@link AssistStructure.ViewNode} whose getHint(ViewNode)
* method.
*/
@NonNull
private static ArrayMap<String, AutofillId> getAutofillableFields(
@NonNull AssistStructure structure) {
ArrayMap<String, AutofillId> fields = new ArrayMap<>();
int nodes = structure.getWindowNodeCount();
for (int i = 0; i < nodes; i++) {
AssistStructure.ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
addAutofillableFields(fields, node);
}
ArrayMap<String, AutofillId> result = new ArrayMap<>();
int filedCount = fields.size();
for (int i = 0; i < filedCount; i++) {
String key = fields.keyAt(i);
AutofillId value = fields.valueAt(i);
// For fields with no hint we just use Field
if (key.equals(value.toString())) {
result.put("Field:" + i + "-", fields.valueAt(i));
} else {
result.put(key, fields.valueAt(i));
}
}
return result;
}
/**
* Adds any autofillable view from the {@link AssistStructure.ViewNode} and its descendants to
* the map.
*/
private static void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
@NonNull AssistStructure.ViewNode node) {
if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
if (!fields.containsValue(node.getAutofillId())) {
final String key;
if (node.getHint() != null) {
key = node.getHint().toLowerCase();
} else if (node.getAutofillHints() != null) {
key = node.getAutofillHints()[0].toLowerCase();
} else {
key = node.getAutofillId().toString();
}
fields.put(key, node.getAutofillId());
}
}
int childrenSize = node.getChildCount();
for (int i = 0; i < childrenSize; i++) {
addAutofillableFields(fields, node.getChildAt(i));
}
}
}

View File

@@ -15,20 +15,11 @@
*/
package foo.bar.inline;
import android.app.PendingIntent;
import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
import android.app.slice.Slice;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.graphics.drawable.Icon;
import android.os.CancellationSignal;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
import android.service.autofill.FillCallback;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.InlinePresentation;
@@ -37,23 +28,14 @@ import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.inline.InlinePresentationSpec;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.autofill.InlinePresentationBuilder;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
/**
* A basic {@link AutofillService} implementation that only shows dynamic-generated datasets
@@ -61,7 +43,7 @@ import java.util.Map.Entry;
*/
public class InlineFillService extends AutofillService {
private static final String TAG = "InlineFillService";
static final String TAG = "InlineFillService";
/**
* Number of datasets sent on each request - we're simple, that value is hardcoded in our DNA!
@@ -76,22 +58,21 @@ public class InlineFillService extends AutofillService {
FillCallback callback) {
Log.d(TAG, "onFillRequest()");
final Context context = getApplicationContext();
// Find autofillable fields
AssistStructure structure = getLatestAssistStructure(request);
ArrayMap<String, AutofillId> fields = getAutofillableFields(structure);
ArrayMap<String, AutofillId> fields = Helper.getAutofillableFields(request);
Log.d(TAG, "autofillable fields:" + fields);
if (fields.isEmpty()) {
showMessage("Service could not figure out how to autofill this screen");
Helper.showMessage(context,
"InlineFillService could not figure out how to autofill this screen");
callback.onSuccess(null);
return;
}
final InlineSuggestionsRequest inlineRequest = request.getInlineSuggestionsRequest();
final int maxSuggestionsCount = inlineRequest == null
? NUMBER_DATASETS
: Math.min(inlineRequest.getMaxSuggestionCount(), NUMBER_DATASETS);
final Optional<InlineSuggestionsRequest> inlineRequest =
InlineRequestHelper.getInlineSuggestionsRequest(request);
final int maxSuggestionsCount = InlineRequestHelper.getMaxSuggestionCount(inlineRequest,
NUMBER_DATASETS);
// Create the base response
final FillResponse response;
@@ -103,31 +84,20 @@ public class InlineFillService extends AutofillService {
hints[i] = fields.keyAt(i);
ids[i] = fields.valueAt(i);
}
IntentSender authentication = AuthActivity.newIntentSenderForResponse(this, hints,
ids, mAuthenticateDatasets, inlineRequest);
RemoteViews presentation = newDatasetPresentation(getPackageName(),
ids, mAuthenticateDatasets, inlineRequest.orElse(null));
RemoteViews presentation = ResponseHelper.newDatasetPresentation(getPackageName(),
"Tap to auth response");
final InlinePresentation inlinePresentation;
if (inlineRequest != null) {
final Slice authSlice = new InlinePresentationBuilder("Tap to auth respones")
.build();
final List<InlinePresentationSpec> specs = inlineRequest.getInlinePresentationSpecs();
final int specsSize = specs.size();
final InlinePresentationSpec currentSpec = specsSize > 0 ? specs.get(0) : null;
inlinePresentation = new InlinePresentation(authSlice, currentSpec,
/* pined= */ false);
} else {
inlinePresentation = null;
}
InlinePresentation inlinePresentation =
InlineRequestHelper.maybeCreateInlineAuthenticationResponse(context,
inlineRequest);
response = new FillResponse.Builder()
.setAuthentication(ids, authentication, presentation, inlinePresentation)
.build();
} else {
response = createResponse(this, fields, maxSuggestionsCount, mAuthenticateDatasets,
request.getInlineSuggestionsRequest());
inlineRequest);
}
callback.onSuccess(response);
@@ -135,63 +105,30 @@ public class InlineFillService extends AutofillService {
static FillResponse createResponse(@NonNull Context context,
@NonNull ArrayMap<String, AutofillId> fields, int numDatasets,
boolean authenticateDatasets, @Nullable InlineSuggestionsRequest inlineRequest) {
boolean authenticateDatasets,
@NonNull Optional<InlineSuggestionsRequest> inlineRequest) {
String packageName = context.getPackageName();
FillResponse.Builder response = new FillResponse.Builder();
// 1.Add the dynamic datasets
for (int i = 1; i <= numDatasets; i++) {
Dataset unlockedDataset = newUnlockedDataset(context, fields, packageName, i,
inlineRequest);
for (int i = 0; i < numDatasets; i++) {
if (authenticateDatasets) {
Dataset.Builder lockedDataset = new Dataset.Builder();
for (Entry<String, AutofillId> field : fields.entrySet()) {
String hint = field.getKey();
AutofillId id = field.getValue();
String value = i + "-" + hint;
IntentSender authentication =
AuthActivity.newIntentSenderForDataset(context, unlockedDataset);
RemoteViews presentation = newDatasetPresentation(packageName,
"Tap to auth " + value);
final InlinePresentation inlinePresentation;
if (inlineRequest != null) {
final Slice authSlice = new InlinePresentationBuilder(
"Tap to auth " + value).build();
final List<InlinePresentationSpec> specs
= inlineRequest.getInlinePresentationSpecs();
final int specsSize = specs.size();
final InlinePresentationSpec currentSpec =
specsSize > 0 ? specs.get(0) : null;
inlinePresentation = new InlinePresentation(authSlice, currentSpec,
/* pined= */ false);
lockedDataset.setValue(id, null, presentation, inlinePresentation)
.setAuthentication(authentication);
} else {
lockedDataset.setValue(id, null, presentation)
.setAuthentication(authentication);
}
}
response.addDataset(lockedDataset.build());
response.addDataset(ResponseHelper.newLockedDataset(context, fields, packageName, i,
inlineRequest));
} else {
response.addDataset(unlockedDataset);
response.addDataset(ResponseHelper.newUnlockedDataset(context, fields,
packageName, i, inlineRequest));
}
}
if (inlineRequest != null) {
// Reuse the first spec's height for the inline action size, as there isn't dedicated
// value from the request for this.
final int height = inlineRequest.getInlinePresentationSpecs().get(0)
.getMinSize().getHeight();
final Size actionIconSize = new Size(height, height);
response.addDataset(
newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
fields));
response.addDataset(
newInlineActionDataset(context, actionIconSize, R.drawable.ic_settings,
fields));
// 2. Add some inline actions
if (inlineRequest.isPresent()) {
response.addDataset(InlineRequestHelper.createInlineActionDataset(context, fields,
inlineRequest.get(), R.drawable.ic_settings));
response.addDataset(InlineRequestHelper.createInlineActionDataset(context, fields,
inlineRequest.get(), R.drawable.ic_settings));
}
// 2.Add save info
// 3.Add save info
Collection<AutofillId> ids = fields.values();
AutofillId[] requiredIds = new AutofillId[ids.size()];
ids.toArray(requiredIds);
@@ -199,162 +136,14 @@ public class InlineFillService extends AutofillService {
// We're simple, so we're generic
new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build());
// 3.Profit!
// 4.Profit!
return response.build();
}
static Dataset newInlineActionDataset(@NonNull Context context,
@NonNull Size size, int drawable, ArrayMap<String, AutofillId> fields) {
Intent intent = new Intent().setComponent(
new ComponentName(context.getPackageName(), SettingsActivity.class.getName()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
final Slice suggestionSlice = new InlinePresentationBuilder()
.setStartIcon(Icon.createWithResource(context, drawable))
.setAttribution(pendingIntent)
.build();
final InlinePresentationSpec currentSpec = new InlinePresentationSpec.Builder(size,
size).build();
Dataset.Builder builder = new Dataset.Builder()
.setInlinePresentation(
new InlinePresentation(suggestionSlice, currentSpec, /** pined= */true))
.setAuthentication(pendingIntent.getIntentSender());
for (AutofillId fieldId : fields.values()) {
builder.setValue(fieldId, null);
}
return builder.build();
}
static Dataset newUnlockedDataset(@NonNull Context context,
@NonNull Map<String, AutofillId> fields, @NonNull String packageName, int i,
@Nullable InlineSuggestionsRequest inlineRequest) {
Dataset.Builder dataset = new Dataset.Builder();
for (Entry<String, AutofillId> field : fields.entrySet()) {
final String hint = field.getKey();
final AutofillId id = field.getValue();
final String value = hint + i;
// We're simple - our dataset values are hardcoded as "hintN" (for example,
// "username1", "username2") and they're displayed as such, except if they're a
// password
final String displayValue = hint.contains("password") ? "password for #" + i : value;
final RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
// Add Inline Suggestion required info.
if (inlineRequest != null) {
Log.d(TAG, "Found InlineSuggestionsRequest in FillRequest: " + inlineRequest);
Intent intent = new Intent().setComponent(
new ComponentName(context.getPackageName(), SettingsActivity.class.getName()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
final Slice suggestionSlice = new InlinePresentationBuilder(value)
.setAttribution(pendingIntent).build();
final List<InlinePresentationSpec> specs = inlineRequest.getInlinePresentationSpecs();
final int specsSize = specs.size();
final InlinePresentationSpec currentSpec = i - 1 < specsSize
? specs.get(i - 1)
: specs.get(specsSize - 1);
final InlinePresentation inlinePresentation =
new InlinePresentation(suggestionSlice, currentSpec, /** pined= */false);
dataset.setValue(id, AutofillValue.forText(value), presentation,
inlinePresentation);
} else {
dataset.setValue(id, AutofillValue.forText(value), presentation);
}
}
return dataset.build();
}
@Override
public void onSaveRequest(SaveRequest request, SaveCallback callback) {
Log.d(TAG, "onSaveRequest()");
showMessage("Save not supported");
Helper.showMessage(getApplicationContext(), "InlineFillService doesn't support Save");
callback.onSuccess();
}
/**
* Parses the {@link AssistStructure} representing the activity being autofilled, and returns a
* map of autofillable fields (represented by their autofill ids) mapped by the hint associate
* with them.
*
* <p>An autofillable field is a {@link ViewNode} whose getHint(ViewNode) method.
*/
@NonNull
private ArrayMap<String, AutofillId> getAutofillableFields(@NonNull AssistStructure structure) {
ArrayMap<String, AutofillId> fields = new ArrayMap<>();
int nodes = structure.getWindowNodeCount();
for (int i = 0; i < nodes; i++) {
ViewNode node = structure.getWindowNodeAt(i).getRootViewNode();
addAutofillableFields(fields, node);
}
ArrayMap<String, AutofillId> result = new ArrayMap<>();
int filedCount = fields.size();
for (int i = 0; i < filedCount; i++) {
String key = fields.keyAt(i);
AutofillId value = fields.valueAt(i);
// For fields with no hint we just use Field
if (key.equals(value.toString())) {
result.put("Field:" + i + "-", fields.valueAt(i));
} else {
result.put(key, fields.valueAt(i));
}
}
return result;
}
/**
* Adds any autofillable view from the {@link ViewNode} and its descendants to the map.
*/
private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
@NonNull ViewNode node) {
if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
if (!fields.containsValue(node.getAutofillId())) {
final String key;
if (node.getHint() != null) {
key = node.getHint().toLowerCase();
} else {
key = node.getAutofillId().toString();
}
fields.put(key, node.getAutofillId());
}
}
int childrenSize = node.getChildCount();
for (int i = 0; i < childrenSize; i++) {
addAutofillableFields(fields, node.getChildAt(i));
}
}
/**
* Helper method to get the {@link AssistStructure} associated with the latest request
* in an autofill context.
*/
@NonNull
private static AssistStructure getLatestAssistStructure(@NonNull FillRequest request) {
List<FillContext> fillContexts = request.getFillContexts();
return fillContexts.get(fillContexts.size() - 1).getStructure();
}
/**
* Helper method to create a dataset presentation with the given text.
*/
@NonNull
private static RemoteViews newDatasetPresentation(@NonNull String packageName,
@NonNull CharSequence text) {
RemoteViews presentation =
new RemoteViews(packageName, R.layout.list_item);
presentation.setTextViewText(R.id.text, text);
return presentation;
}
/**
* Displays a toast with the given message.
*/
private void showMessage(@NonNull CharSequence message) {
Log.i(TAG, message.toString());
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foo.bar.inline;
import android.app.PendingIntent;
import android.app.slice.Slice;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.service.autofill.Dataset;
import android.service.autofill.FillRequest;
import android.service.autofill.InlinePresentation;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.view.autofill.AutofillId;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.widget.inline.InlinePresentationSpec;
import androidx.autofill.inline.v1.InlineSuggestionUi;
import androidx.autofill.inline.v1.InlineSuggestionUi.Content;
import java.util.Optional;
public class InlineRequestHelper {
static Optional<InlineSuggestionsRequest> getInlineSuggestionsRequest(FillRequest request) {
final InlineSuggestionsRequest inlineRequest = request.getInlineSuggestionsRequest();
if (inlineRequest != null && inlineRequest.getMaxSuggestionCount() > 0
&& !inlineRequest.getInlinePresentationSpecs().isEmpty()) {
return Optional.of(inlineRequest);
}
return Optional.empty();
}
static int getMaxSuggestionCount(Optional<InlineSuggestionsRequest> inlineRequest, int max) {
if (inlineRequest.isPresent()) {
return Math.min(max, inlineRequest.get().getMaxSuggestionCount());
}
return max;
}
static InlinePresentation maybeCreateInlineAuthenticationResponse(
Context context, Optional<InlineSuggestionsRequest> inlineRequest) {
if (!inlineRequest.isPresent()) {
return null;
}
final PendingIntent attribution = createAttribution(context,
"Please tap on the chip to authenticate the Autofill response.");
final Slice slice = createSlice("Tap to auth response", null, null, null, attribution);
final InlinePresentationSpec spec = inlineRequest.get().getInlinePresentationSpecs().get(0);
return new InlinePresentation(slice, spec, false);
}
static InlinePresentation createInlineDataset(Context context,
InlineSuggestionsRequest inlineRequest, String value, int index) {
final PendingIntent attribution = createAttribution(context,
"Please tap on the chip to autofill the value:" + value);
final Slice slice = createSlice(value, null, null, null, attribution);
index = Math.min(inlineRequest.getInlinePresentationSpecs().size() - 1, index);
final InlinePresentationSpec spec = inlineRequest.getInlinePresentationSpecs().get(index);
return new InlinePresentation(slice, spec, false);
}
static Dataset createInlineActionDataset(Context context,
ArrayMap<String, AutofillId> fields,
InlineSuggestionsRequest inlineRequest, int drawable) {
PendingIntent pendingIntent =
PendingIntent.getActivity(context, 0, new Intent(context, SettingsActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT);
Dataset.Builder builder =
new Dataset.Builder()
.setInlinePresentation(createInlineAction(context, inlineRequest, drawable))
.setAuthentication(pendingIntent.getIntentSender());
for (AutofillId fieldId : fields.values()) {
builder.setValue(fieldId, null);
}
return builder.build();
}
private static InlinePresentation createInlineAction(Context context,
InlineSuggestionsRequest inlineRequest, int drawable) {
final PendingIntent attribution = createAttribution(context,
"Please tap on the chip to launch the action.");
final Icon icon = Icon.createWithResource(context, drawable);
final Slice slice = createSlice(null, null, icon, null, attribution);
// Reuse the first spec's height for the inline action size, as there isn't dedicated
// value from the request for this.
final InlinePresentationSpec spec = inlineRequest.getInlinePresentationSpecs().get(0);
return new InlinePresentation(slice, spec, true);
}
private static Slice createSlice(
String title, String subtitle, Icon startIcon, Icon endIcon,
PendingIntent attribution) {
Content.Builder builder = InlineSuggestionUi.newContentBuilder();
if (attribution != null) {
builder.setAttribution(attribution);
}
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title);
}
if (!TextUtils.isEmpty(subtitle)) {
builder.setSubtitle(subtitle);
}
if (startIcon != null) {
builder.setStartIcon(startIcon);
}
if (endIcon != null) {
builder.setEndIcon(endIcon);
}
return builder.build().getSlice();
}
private static PendingIntent createAttribution(Context context, String msg) {
Intent intent = new Intent(context, AttributionDialogActivity.class);
intent.putExtra(AttributionDialogActivity.KEY_MSG, msg);
// Should use different request code to avoid the new intent overriding the old one.
PendingIntent pendingIntent =
PendingIntent.getActivity(
context, msg.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
return pendingIntent;
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package foo.bar.inline;
import static foo.bar.inline.InlineFillService.TAG;
import android.content.Context;
import android.content.IntentSender;
import android.service.autofill.Dataset;
import android.service.autofill.InlinePresentation;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.InlineSuggestionsRequest;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import java.util.Map;
import java.util.Optional;
class ResponseHelper {
static Dataset newUnlockedDataset(@NonNull Context context,
@NonNull Map<String, AutofillId> fields, @NonNull String packageName, int index,
@NonNull Optional<InlineSuggestionsRequest> inlineRequest) {
Dataset.Builder dataset = new Dataset.Builder();
for (Map.Entry<String, AutofillId> field : fields.entrySet()) {
final String hint = field.getKey();
final AutofillId id = field.getValue();
final String value = hint + (index + 1);
// We're simple - our dataset values are hardcoded as "hintN" (for example,
// "username1", "username2") and they're displayed as such, except if they're a
// password
Log.d(TAG, "hint: " + hint);
final String displayValue = hint.contains("password") ? "password for #" + (index + 1)
: value;
final RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
// Add Inline Suggestion required info.
if (inlineRequest.isPresent()) {
Log.d(TAG, "Found InlineSuggestionsRequest in FillRequest: " + inlineRequest);
final InlinePresentation inlinePresentation =
InlineRequestHelper.createInlineDataset(context, inlineRequest.get(),
displayValue, index);
dataset.setValue(id, AutofillValue.forText(value), presentation,
inlinePresentation);
} else {
dataset.setValue(id, AutofillValue.forText(value), presentation);
}
}
return dataset.build();
}
static Dataset newLockedDataset(@NonNull Context context,
@NonNull Map<String, AutofillId> fields, @NonNull String packageName, int index,
@NonNull Optional<InlineSuggestionsRequest> inlineRequest) {
Dataset unlockedDataset = ResponseHelper.newUnlockedDataset(context, fields,
packageName, index, inlineRequest);
Dataset.Builder lockedDataset = new Dataset.Builder();
for (Map.Entry<String, AutofillId> field : fields.entrySet()) {
String hint = field.getKey();
AutofillId id = field.getValue();
String value = (index + 1) + "-" + hint;
String displayValue = "Tap to auth " + value;
IntentSender authentication =
AuthActivity.newIntentSenderForDataset(context, unlockedDataset);
RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
if (inlineRequest.isPresent()) {
final InlinePresentation inlinePresentation =
InlineRequestHelper.createInlineDataset(context, inlineRequest.get(),
displayValue, index);
lockedDataset.setValue(id, null, presentation, inlinePresentation)
.setAuthentication(authentication);
} else {
lockedDataset.setValue(id, null, presentation)
.setAuthentication(authentication);
}
}
return lockedDataset.build();
}
/**
* Helper method to create a dataset presentation with the givean text.
*/
@NonNull
static RemoteViews newDatasetPresentation(@NonNull String packageName,
@NonNull CharSequence text) {
RemoteViews presentation =
new RemoteViews(packageName, R.layout.list_item);
presentation.setTextViewText(R.id.text, text);
return presentation;
}
}