Inline Fill Service for demo.

Bug: 137800469
Test: manual verification
Change-Id: I44464649dc64f26823461b2bc54dd64440471d61
diff --git a/samples/InlineFillService/Android.bp b/samples/InlineFillService/Android.bp
new file mode 100644
index 0000000..00fa237
--- /dev/null
+++ b/samples/InlineFillService/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2019 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.
+
+android_app {
+    name: "InlineFillService",
+    srcs: [
+        "src/**/*.java",
+    ],
+    resource_dirs: ["res"],
+    sdk_version: "system_current",
+    static_libs: [
+        "androidx.annotation_annotation",
+    ],
+}
\ No newline at end of file
diff --git a/samples/InlineFillService/AndroidManifest.xml b/samples/InlineFillService/AndroidManifest.xml
new file mode 100644
index 0000000..c08ca85
--- /dev/null
+++ b/samples/InlineFillService/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="foo.bar.fill" >
+    <application>
+        <service
+            android:name=".InlineFillService"
+            android:label="Inline Fill Service"
+            android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.autofill.AutofillService" />
+            </intent-filter>
+            <meta-data
+                android:name="android.autofill"
+                android:resource="@xml/autofill_service_config">
+            </meta-data>
+        </service>
+    </application>
+</manifest>
+
diff --git a/samples/InlineFillService/res/drawable-hdpi/ic_launcher.png b/samples/InlineFillService/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a301d57
--- /dev/null
+++ b/samples/InlineFillService/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/samples/InlineFillService/res/drawable-ldpi/ic_launcher.png b/samples/InlineFillService/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..2c2a58b
--- /dev/null
+++ b/samples/InlineFillService/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/samples/InlineFillService/res/drawable-mdpi/ic_launcher.png b/samples/InlineFillService/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..f91f736
--- /dev/null
+++ b/samples/InlineFillService/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/samples/InlineFillService/res/drawable-xhdpi/ic_launcher.png b/samples/InlineFillService/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..96095ec
--- /dev/null
+++ b/samples/InlineFillService/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/samples/InlineFillService/res/layout/list_item.xml b/samples/InlineFillService/res/layout/list_item.xml
new file mode 100644
index 0000000..abefbdd
--- /dev/null
+++ b/samples/InlineFillService/res/layout/list_item.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:background="#ffffffff">
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceListItemSmall"
+        android:gravity="center_vertical"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        android:minHeight="?android:attr/listPreferredItemHeightSmall">
+    </TextView>
+</LinearLayout>
diff --git a/samples/InlineFillService/res/xml/autofill_service_config.xml b/samples/InlineFillService/res/xml/autofill_service_config.xml
new file mode 100644
index 0000000..a2954d2
--- /dev/null
+++ b/samples/InlineFillService/res/xml/autofill_service_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 Google Inc.
+
+     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.
+-->
+
+<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:supportsInlineSuggestions="true">
+    <compatibility-package android:name="com.android.chrome" android:maxLongVersionCode="1000000000" />
+    <compatibility-package android:name="com.chrome.beta" android:maxLongVersionCode="1000000000" />
+</autofill-service>
diff --git a/samples/InlineFillService/src/foo/bar/fill/InlineFillService.java b/samples/InlineFillService/src/foo/bar/fill/InlineFillService.java
new file mode 100644
index 0000000..0a5705c
--- /dev/null
+++ b/samples/InlineFillService/src/foo/bar/fill/InlineFillService.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 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.fill;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.slice.Slice;
+import android.app.slice.SliceSpec;
+import android.content.Context;
+import android.net.Uri;
+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.SaveCallback;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.SaveRequest;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.view.inline.InlinePresentationSpec;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import foo.bar.fill.R;
+
+/**
+ * A basic {@link AutofillService} implementation that only shows dynamic-generated datasets
+ * and supports inline suggestions.
+ */
+public class InlineFillService extends AutofillService {
+
+    private static final String TAG = "InlineFillService";
+
+    /**
+     * Number of datasets sent on each request - we're simple, that value is hardcoded in our DNA!
+     */
+    static final int NUMBER_DATASETS = 4;
+
+    @Override
+    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+            FillCallback callback) {
+        Log.d(TAG, "onFillRequest()");
+
+        // Find autofillable fields
+        AssistStructure structure = getLatestAssistStructure(request);
+
+        ArrayMap<String, AutofillId> fields = getAutofillableFields(structure, request.getFlags());
+        Log.d(TAG, "autofillable fields:" + fields);
+
+        if (fields.isEmpty()) {
+            showMessage("Service could not figure out how to autofill this screen");
+            callback.onSuccess(null);
+            return;
+        }
+
+        // Create the base response
+        FillResponse response = createResponse(this, fields, NUMBER_DATASETS,
+                request.getInlineSuggestionsRequest());
+        callback.onSuccess(response);
+    }
+
+    static FillResponse createResponse(@NonNull Context context,
+            @NonNull ArrayMap<String, AutofillId> fields, int numDatasets,
+            @Nullable 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(fields, packageName, i);
+            response.addDataset(unlockedDataset);
+        }
+
+        if (inlineRequest != null) {
+            Log.d(TAG, "Found InlineSuggestionsRequest in FillRequest: " + inlineRequest);
+            final int maxSuggestionsCount = Math.min(inlineRequest.getMaxSuggestionCount(),
+                    NUMBER_DATASETS);
+            final List<InlinePresentationSpec> presentationSpecs =
+                    inlineRequest.getPresentationSpecs();
+            final int specsSize = presentationSpecs.size();
+
+            InlinePresentationSpec currentSpecs = presentationSpecs.get(0);
+            for (int i = 1; i <= maxSuggestionsCount; i++) {
+                if (currentSpecs == null) {
+                    break;
+                }
+
+                if (i < specsSize) {
+                    currentSpecs = presentationSpecs.get(i);
+                }
+
+                final Uri uri = new Uri.Builder().appendPath("BasicService-" + i).build();
+                final ArrayList<String> autofillHints = new ArrayList<>();
+                autofillHints.add(fields.keyAt(0));
+                final Slice suggestionSlice = new Slice.Builder(uri,
+                        new SliceSpec("InlineSuggestion", 1))
+                        .addInt(currentSpecs.getMinSize().getWidth(), "SUBTYPE_MIN_WIDTH",
+                                Collections.EMPTY_LIST)
+                        .addInt(currentSpecs.getMaxSize().getWidth(), "SUBTYPE_MAX_WIDTH",
+                                Collections.EMPTY_LIST)
+                        .addInt(currentSpecs.getMinSize().getHeight(), "SUBTYPE_MIN_HEIGHT",
+                                Collections.EMPTY_LIST)
+                        .addInt(currentSpecs.getMaxSize().getHeight(), "SUBTYPE_MAX_HEIGHT",
+                                Collections.EMPTY_LIST)
+                        .addHints(autofillHints)
+                        .build();
+                response.addInlineSuggestionSlice(suggestionSlice);
+            }
+        }
+
+        // 2.Add save info
+        Collection<AutofillId> ids = fields.values();
+        AutofillId[] requiredIds = new AutofillId[ids.size()];
+        ids.toArray(requiredIds);
+        response.setSaveInfo(
+                // We're simple, so we're generic
+                new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build());
+
+        // 3.Profit!
+        return response.build();
+    }
+
+    static Dataset newUnlockedDataset(@NonNull Map<String, AutofillId> fields,
+            @NonNull String packageName, int i) {
+        Dataset.Builder dataset = new Dataset.Builder();
+        for (Entry<String, AutofillId> field : fields.entrySet()) {
+            String hint = field.getKey();
+            AutofillId id = field.getValue();
+            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
+            String displayValue = hint.contains("password") ? "password for #" + i : value;
+            RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
+            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");
+        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,
+            int flags) {
+        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, flags);
+        }
+        return fields;
+    }
+
+    /**
+     * 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, int flags) {
+        int type = node.getAutofillType();
+        String hint = getHint(node, flags);
+        if (hint != null) {
+            AutofillId id = node.getAutofillId();
+            if (!fields.containsKey(hint)) {
+                Log.v(TAG, "Setting hint " + hint + " on " + id);
+                fields.put(hint, id);
+            } else {
+                Log.v(TAG, "Ignoring hint " + hint + " on " + id
+                        + " because it was already set");
+            }
+        }
+        int childrenSize = node.getChildCount();
+        for (int i = 0; i < childrenSize; i++) {
+            addAutofillableFields(fields, node.getChildAt(i), flags);
+        }
+    }
+
+    /**
+     * Gets the autofill hint associated with the given node.
+     *
+     * <p>By default it just return the first entry on the node's
+     * {@link ViewNode#getAutofillHints() autofillHints} (when available), but subclasses could
+     * extend it to use heuristics when the app developer didn't explicitly provide these hints.
+     *
+     */
+    @Nullable
+    protected String getHint(@NonNull ViewNode node, int flags) {
+        String[] hints = node.getAutofillHints();
+        if (hints == null) return null;
+
+        // We're simple, we only care about the first hint
+        String hint = hints[0].toLowerCase();
+        return hint;
+    }
+
+    /**
+     * 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();
+    }
+}