From 0c501136ca8fe1684e0fff1188a29682493dd810 Mon Sep 17 00:00:00 2001 From: Feng Cao Date: Mon, 27 Apr 2020 16:30:59 -0700 Subject: [PATCH] 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 --- .../res/drawable-hdpi/chip_background.xml | 41 +++ .../autofillkeyboard/AutofillImeService.java | 67 ++++- samples/InlineFillService/AndroidManifest.xml | 4 + .../bar/inline/AttributionDialogActivity.java | 65 ++++ .../src/foo/bar/inline/AuthActivity.java | 6 +- .../src/foo/bar/inline/Helper.java | 120 ++++++++ .../src/foo/bar/inline/InlineFillService.java | 277 +++--------------- .../foo/bar/inline/InlineRequestHelper.java | 138 +++++++++ .../src/foo/bar/inline/ResponseHelper.java | 112 +++++++ 9 files changed, 577 insertions(+), 253 deletions(-) create mode 100644 samples/AutofillKeyboard/res/drawable-hdpi/chip_background.xml create mode 100644 samples/InlineFillService/src/foo/bar/inline/AttributionDialogActivity.java create mode 100644 samples/InlineFillService/src/foo/bar/inline/Helper.java create mode 100644 samples/InlineFillService/src/foo/bar/inline/InlineRequestHelper.java create mode 100644 samples/InlineFillService/src/foo/bar/inline/ResponseHelper.java diff --git a/samples/AutofillKeyboard/res/drawable-hdpi/chip_background.xml b/samples/AutofillKeyboard/res/drawable-hdpi/chip_background.xml new file mode 100644 index 000000000..c69ebf587 --- /dev/null +++ b/samples/AutofillKeyboard/res/drawable-hdpi/chip_background.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java b/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java index e6164824f..73e5a92ea 100644 --- a/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java +++ b/samples/AutofillKeyboard/src/com/example/android/autofillkeyboard/AutofillImeService.java @@ -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 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"); }); diff --git a/samples/InlineFillService/AndroidManifest.xml b/samples/InlineFillService/AndroidManifest.xml index 70f8409f4..6ce859b8f 100644 --- a/samples/InlineFillService/AndroidManifest.xml +++ b/samples/InlineFillService/AndroidManifest.xml @@ -23,5 +23,9 @@ android:name=".SettingsActivity" android:label="Autofill Settings" android:exported="true"/> + + diff --git a/samples/InlineFillService/src/foo/bar/inline/AttributionDialogActivity.java b/samples/InlineFillService/src/foo/bar/inline/AttributionDialogActivity.java new file mode 100644 index 000000000..9fc76cc2a --- /dev/null +++ b/samples/InlineFillService/src/foo/bar/inline/AttributionDialogActivity.java @@ -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(); + } +} diff --git a/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java b/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java index 9af2494f8..d96fb4b45 100644 --- a/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java +++ b/samples/InlineFillService/src/foo/bar/inline/AuthActivity.java @@ -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); } diff --git a/samples/InlineFillService/src/foo/bar/inline/Helper.java b/samples/InlineFillService/src/foo/bar/inline/Helper.java new file mode 100644 index 000000000..f7710f81d --- /dev/null +++ b/samples/InlineFillService/src/foo/bar/inline/Helper.java @@ -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 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 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. + * + *

An autofillable field is a {@link AssistStructure.ViewNode} whose getHint(ViewNode) + * method. + */ + @NonNull + private static ArrayMap getAutofillableFields( + @NonNull AssistStructure structure) { + ArrayMap 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 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 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)); + } + } +} diff --git a/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java b/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java index 59c9f73a3..d0c88c552 100644 --- a/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java +++ b/samples/InlineFillService/src/foo/bar/inline/InlineFillService.java @@ -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 fields = getAutofillableFields(structure); + ArrayMap 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 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 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 fields, int numDatasets, - boolean authenticateDatasets, @Nullable InlineSuggestionsRequest inlineRequest) { + boolean authenticateDatasets, + @NonNull Optional 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 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 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 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 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 fields, @NonNull String packageName, int i, - @Nullable InlineSuggestionsRequest inlineRequest) { - - Dataset.Builder dataset = new Dataset.Builder(); - for (Entry 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 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. - * - *

An autofillable field is a {@link ViewNode} whose getHint(ViewNode) method. - */ - @NonNull - private ArrayMap getAutofillableFields(@NonNull AssistStructure structure) { - ArrayMap fields = new ArrayMap<>(); - int nodes = structure.getWindowNodeCount(); - for (int i = 0; i < nodes; i++) { - ViewNode node = structure.getWindowNodeAt(i).getRootViewNode(); - addAutofillableFields(fields, node); - } - ArrayMap 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 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 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(); - } } diff --git a/samples/InlineFillService/src/foo/bar/inline/InlineRequestHelper.java b/samples/InlineFillService/src/foo/bar/inline/InlineRequestHelper.java new file mode 100644 index 000000000..92fb2a278 --- /dev/null +++ b/samples/InlineFillService/src/foo/bar/inline/InlineRequestHelper.java @@ -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 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 inlineRequest, int max) { + if (inlineRequest.isPresent()) { + return Math.min(max, inlineRequest.get().getMaxSuggestionCount()); + } + return max; + } + + static InlinePresentation maybeCreateInlineAuthenticationResponse( + Context context, Optional 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 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; + } + +} diff --git a/samples/InlineFillService/src/foo/bar/inline/ResponseHelper.java b/samples/InlineFillService/src/foo/bar/inline/ResponseHelper.java new file mode 100644 index 000000000..ad1349756 --- /dev/null +++ b/samples/InlineFillService/src/foo/bar/inline/ResponseHelper.java @@ -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 fields, @NonNull String packageName, int index, + @NonNull Optional inlineRequest) { + + Dataset.Builder dataset = new Dataset.Builder(); + for (Map.Entry 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 fields, @NonNull String packageName, int index, + @NonNull Optional inlineRequest) { + Dataset unlockedDataset = ResponseHelper.newUnlockedDataset(context, fields, + packageName, index, inlineRequest); + + Dataset.Builder lockedDataset = new Dataset.Builder(); + for (Map.Entry 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; + } +}