chromium/android_webview/tools/captured_sites_tests/javatests/src/org/chromium/webview_ui_test/test/TestAutofillService.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.webview_ui_test.test;

import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.ViewNode;
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.SaveRequest;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;

import androidx.annotation.NonNull;

import org.chromium.webview_ui_test.test.util.AutofillProfile;

import java.util.ArrayList;
import java.util.List;

/** An {@link AutofillService} implementation which provides static responses, useful for testing. */
public class TestAutofillService extends AutofillService {
    private static final long EPOCH_TIME = 946684800000L; // Jan 1, 2000.
    private static final String TAG = "TestAutofillService";
    private static final String PACKAGE_NAME = "org.chromium.webview_ui_test.test";
    private static final String DEFAULT_FILL = "default";

    // Called by Autofill architecture to fill autofillable fields.
    @Override
    public void onFillRequest(
            FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)
            throws Error {
        try {
            handleRequest(request, callback);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            raiseError("onFillRequest() interrupted", e, callback);
        } catch (Exception e) {
            raiseError("exception on onFillRequest()", e, callback);
        }
    }

    // Not useful for our testing purposes.
    @Override
    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
        callback.onFailure(
                "onSaveRequest() should not have been called."
                        + "This service only supplies onFillRequest()");
    }

    // Collects all Autofill nodes in a UI ViewNode.
    private List<ViewNode> collectAutofillNodes(ViewNode node) {
        List<ViewNode> nodes = new ArrayList<>();
        if (node.getAutofillType() != View.AUTOFILL_TYPE_NONE) {
            nodes.add(node);
        }
        int childrenSize = node.getChildCount();
        for (int i = 0; i < childrenSize; i++) {
            nodes.addAll(collectAutofillNodes(node.getChildAt(i)));
        }
        return nodes;
    }

    /*
     * Returns a response based upon the values stored in the AutofillProfile provided.
     * For Autofill Types TOGGLE, and DATE, we ignore the field but otherwise fill in the form.
     */
    private void handleRequest(FillRequest request, FillCallback callback) throws Exception {
        // Grab all ViewNodes.
        List<FillContext> contexts = request.getFillContexts();
        if (contexts.isEmpty()) {
            Log.w(TAG, "Request has no contexts");
            callback.onSuccess(null);
            return; // We can't fulfill a request without the current context.
        }
        List<ViewNode> nodes = new ArrayList<>();
        FillContext context = contexts.get(contexts.size() - 1); // Get current context
        AssistStructure struct = context.getStructure();

        if (struct == null || struct.getWindowNodeCount() == 0) {
            Log.w(TAG, "Request has no ViewNodes");
            callback.onSuccess(null);
            return; // Nothing to autofill
        }

        for (int i = 0; i < struct.getWindowNodeCount(); i++) {
            ViewNode rootNode = struct.getWindowNodeAt(i).getRootViewNode();
            nodes.addAll(collectAutofillNodes(rootNode));
        }
        // Build response
        Dataset.Builder dataset = new Dataset.Builder(createRemoteViews("dataset"));
        for (ViewNode node : nodes) {
            AutofillId autofillId = node.getAutofillId();
            AutofillValue value = null;

            String fill = DEFAULT_FILL;
            switch (node.getAutofillType()) {
                case View.AUTOFILL_TYPE_TEXT:
                    fill = getFillFromProfile(node);
                    value = AutofillValue.forText(fill);
                    dataset.setValue(autofillId, value, createRemoteViews(fill));
                    break;
                case View.AUTOFILL_TYPE_LIST:
                    fill = getFillFromProfile(node);
                    value = AutofillValue.forList(findFillIndex(node, fill));
                    dataset.setValue(autofillId, value, createRemoteViews(fill));
                    break;
                case View.AUTOFILL_TYPE_TOGGLE:
                    Log.d(TAG, "Ignoring ViewNode: " + autofillId + "because it has type toggle.");
                    break;
                case View.AUTOFILL_TYPE_DATE:
                    Log.d(TAG, "Ignoring ViewNode: " + autofillId + "because it has type date.");
                    break;
                default:
                    throw new Exception(
                            "TestAutofillService should not fill node with AutofillType NONE");
            }
        }
        if (nodes.size() > 0) {
            FillResponse.Builder fillResponse = new FillResponse.Builder();
            FillResponse response = fillResponse.addDataset(dataset.build()).build();
            Log.w(TAG, "Success: returning FillResponse: " + response.toString());
            callback.onSuccess(response);
        } else {
            Log.w(TAG, "Dataset contains zero ViewNodes");
            callback.onSuccess(null);
        }
    }

    // Takes the AutofillHint from the given ViewNode and returns the matching value from profile.
    private String getFillFromProfile(ViewNode node) throws Exception {
        // (crbug/1473156) Move this to initialization of service once 'uber profile' made.
        AutofillProfile profile = new AutofillProfile("us.profile");
        List<Pair<String, String>> attributes = node.getHtmlInfo().getAttributes();
        String hint = DEFAULT_FILL;
        for (Pair<String, String> attribute : attributes) {
            if (attribute.first.equals("ua-autofill-hints")) {
                hint = attribute.second;
            }
        }
        if (hint.equals(DEFAULT_FILL)) {
            throw new NullPointerException("No ua-autofill-hint for element");
        }
        if (profile.profileMap.containsKey(hint)) {
            return profile.profileMap.get(hint);
        } else {
            throw new NullPointerException("Profile has no type with hint " + hint);
        }
    }

    // Finds the index to select in dropdown list.
    private int findFillIndex(ViewNode node, String fillString) throws Exception {
        CharSequence[] options = node.getAutofillOptions();
        for (int i = 0; i < options.length; i++) {
            if (options[i].toString().equals(fillString)) {
                return i;
            }
        }
        throw new Exception("No dropdown found with fill string " + fillString);
    }

    private void raiseError(
            @NonNull String msg, @NonNull Exception e, @NonNull FillCallback callback) {
        Log.e(TAG, msg, e);
        callback.onFailure(msg);
    }

    @NonNull
    private static RemoteViews createRemoteViews(@NonNull CharSequence text) {
        RemoteViews presentation =
                new RemoteViews(PACKAGE_NAME, R.layout.autofill_dataset_picker_text_only);
        presentation.setTextViewText(R.id.text, text);
        return presentation;
    }
}