1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.service.autofill;
18
19import static android.view.autofill.Helper.sDebug;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.app.assist.AssistStructure;
24import android.app.assist.AssistStructure.ViewNode;
25import android.os.Bundle;
26import android.os.CancellationSignal;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.util.ArrayMap;
30import android.util.SparseIntArray;
31import android.view.autofill.AutofillId;
32
33import java.util.LinkedList;
34
35/**
36 * This class represents a context for each fill request made via {@link
37 * AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
38 * It contains a snapshot of the UI state, the view ids that were returned by
39 * the {@link AutofillService autofill service} as both required to trigger a save
40 * and optional that can be saved, and the id of the corresponding {@link
41 * FillRequest}.
42 * <p>
43 * This context allows you to inspect the values for the interesting views
44 * in the context they appeared. Also a reference to the corresponding fill
45 * request is useful to store meta-data in the client state bundle passed
46 * to {@link FillResponse.Builder#setClientState(Bundle)} to avoid interpreting
47 * the UI state again while saving.
48 */
49public final class FillContext implements Parcelable {
50    private final int mRequestId;
51    private final @NonNull AssistStructure mStructure;
52
53    /**
54     * Lookup table AutofillId->ViewNode to speed up {@link #findViewNodesByAutofillIds}
55     * This is purely a cache and can be deleted at any time
56     */
57    @Nullable private ArrayMap<AutofillId, AssistStructure.ViewNode> mViewNodeLookupTable;
58
59
60    /** @hide */
61    public FillContext(int requestId, @NonNull AssistStructure structure) {
62        mRequestId = requestId;
63        mStructure = structure;
64    }
65
66    private FillContext(Parcel parcel) {
67        this(parcel.readInt(), parcel.readParcelable(null));
68    }
69
70    /**
71     * Gets the id of the {@link FillRequest fill request} this context
72     * corresponds to. This is useful to associate your custom client
73     * state with every request to avoid reinterpreting the UI when saving
74     * user data.
75     *
76     * @return The request id.
77     */
78    public int getRequestId() {
79        return mRequestId;
80    }
81
82    /**
83     * @return The screen content.
84     */
85    public AssistStructure getStructure() {
86        return mStructure;
87    }
88
89    @Override
90    public String toString() {
91        if (!sDebug)  return super.toString();
92
93        return "FillContext [reqId=" + mRequestId + "]";
94    }
95
96    @Override
97    public int describeContents() {
98        return 0;
99    }
100
101    @Override
102    public void writeToParcel(Parcel parcel, int flags) {
103        parcel.writeInt(mRequestId);
104        parcel.writeParcelable(mStructure, flags);
105    }
106
107    /**
108     * Finds {@link ViewNode ViewNodes} that have the requested ids.
109     *
110     * @param ids The ids of the node to find.
111     *
112     * @return The nodes indexed in the same way as the ids.
113     *
114     * @hide
115     */
116    @NonNull public ViewNode[] findViewNodesByAutofillIds(@NonNull AutofillId[] ids) {
117        final LinkedList<ViewNode> nodesToProcess = new LinkedList<>();
118        final ViewNode[] foundNodes = new AssistStructure.ViewNode[ids.length];
119
120        // Indexes of foundNodes that are not found yet
121        final SparseIntArray missingNodeIndexes = new SparseIntArray(ids.length);
122
123        for (int i = 0; i < ids.length; i++) {
124            if (mViewNodeLookupTable != null) {
125                int lookupTableIndex = mViewNodeLookupTable.indexOfKey(ids[i]);
126
127                if (lookupTableIndex >= 0) {
128                    foundNodes[i] = mViewNodeLookupTable.valueAt(lookupTableIndex);
129                } else {
130                    missingNodeIndexes.put(i, /* ignored */ 0);
131                }
132            } else {
133                missingNodeIndexes.put(i, /* ignored */ 0);
134            }
135        }
136
137        final int numWindowNodes = mStructure.getWindowNodeCount();
138        for (int i = 0; i < numWindowNodes; i++) {
139            nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode());
140        }
141
142        while (missingNodeIndexes.size() > 0 && !nodesToProcess.isEmpty()) {
143            final ViewNode node = nodesToProcess.removeFirst();
144
145            for (int i = 0; i < missingNodeIndexes.size(); i++) {
146                final int index = missingNodeIndexes.keyAt(i);
147                final AutofillId id = ids[index];
148
149                if (id.equals(node.getAutofillId())) {
150                    foundNodes[index] = node;
151
152                    if (mViewNodeLookupTable == null) {
153                        mViewNodeLookupTable = new ArrayMap<>(ids.length);
154                    }
155
156                    mViewNodeLookupTable.put(id, node);
157
158                    missingNodeIndexes.removeAt(i);
159                    break;
160                }
161            }
162
163            for (int i = 0; i < node.getChildCount(); i++) {
164                nodesToProcess.addLast(node.getChildAt(i));
165            }
166        }
167
168        // Remember which ids could not be resolved to not search for them again the next time
169        for (int i = 0; i < missingNodeIndexes.size(); i++) {
170            if (mViewNodeLookupTable == null) {
171                mViewNodeLookupTable = new ArrayMap<>(missingNodeIndexes.size());
172            }
173
174            mViewNodeLookupTable.put(ids[missingNodeIndexes.keyAt(i)], null);
175        }
176
177        return foundNodes;
178    }
179
180    /**
181     * Finds the {@link ViewNode} that has the requested {@code id}, if any.
182     *
183     * @hide
184     */
185    @Nullable public ViewNode findViewNodeByAutofillId(@NonNull AutofillId id) {
186        final LinkedList<ViewNode> nodesToProcess = new LinkedList<>();
187        final int numWindowNodes = mStructure.getWindowNodeCount();
188        for (int i = 0; i < numWindowNodes; i++) {
189            nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode());
190        }
191        while (!nodesToProcess.isEmpty()) {
192            final ViewNode node = nodesToProcess.removeFirst();
193            if (id.equals(node.getAutofillId())) {
194                return node;
195            }
196            for (int i = 0; i < node.getChildCount(); i++) {
197                nodesToProcess.addLast(node.getChildAt(i));
198            }
199        }
200
201        return null;
202    }
203
204    public static final Parcelable.Creator<FillContext> CREATOR =
205            new Parcelable.Creator<FillContext>() {
206        @Override
207        public FillContext createFromParcel(Parcel parcel) {
208            return new FillContext(parcel);
209        }
210
211        @Override
212        public FillContext[] newArray(int size) {
213            return new FillContext[size];
214        }
215    };
216}
217