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 
17 package android.service.autofill;
18 
19 import static android.view.autofill.Helper.sDebug;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.assist.AssistStructure;
24 import android.app.assist.AssistStructure.ViewNode;
25 import android.os.Bundle;
26 import android.os.CancellationSignal;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.util.ArrayMap;
30 import android.util.SparseIntArray;
31 import android.view.autofill.AutofillId;
32 
33 import 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  */
49 public final class FillContext implements Parcelable {
50     private final int mRequestId;
51     private final @NonNull AssistStructure mStructure;
52     private final @NonNull AutofillId mFocusedId;
53 
54     /**
55      * Lookup table AutofillId->ViewNode to speed up {@link #findViewNodesByAutofillIds}
56      * This is purely a cache and can be deleted at any time
57      */
58     @Nullable private ArrayMap<AutofillId, AssistStructure.ViewNode> mViewNodeLookupTable;
59 
60 
61     /** @hide */
FillContext(int requestId, @NonNull AssistStructure structure, @NonNull AutofillId autofillId)62     public FillContext(int requestId, @NonNull AssistStructure structure,
63             @NonNull AutofillId autofillId) {
64         mRequestId = requestId;
65         mStructure = structure;
66         mFocusedId = autofillId;
67     }
68 
FillContext(Parcel parcel)69     private FillContext(Parcel parcel) {
70         this(parcel.readInt(), parcel.readParcelable(null), parcel.readParcelable(null));
71     }
72 
73     /**
74      * Gets the id of the {@link FillRequest fill request} this context
75      * corresponds to. This is useful to associate your custom client
76      * state with every request to avoid reinterpreting the UI when saving
77      * user data.
78      *
79      * @return The request id.
80      */
getRequestId()81     public int getRequestId() {
82         return mRequestId;
83     }
84 
85     /**
86      * @return The screen content.
87      */
88     @NonNull
getStructure()89     public AssistStructure getStructure() {
90         return mStructure;
91     }
92 
93     /**
94      * @return the AutofillId of the view that triggered autofill.
95      */
96     @NonNull
getFocusedId()97     public AutofillId getFocusedId() {
98         return mFocusedId;
99     }
100 
101     @Override
toString()102     public String toString() {
103         if (!sDebug)  return super.toString();
104 
105         return "FillContext [reqId=" + mRequestId + ", focusedId=" + mFocusedId + "]";
106     }
107 
108     @Override
describeContents()109     public int describeContents() {
110         return 0;
111     }
112 
113     @Override
writeToParcel(Parcel parcel, int flags)114     public void writeToParcel(Parcel parcel, int flags) {
115         parcel.writeInt(mRequestId);
116         parcel.writeParcelable(mStructure, flags);
117         parcel.writeParcelable(mFocusedId, flags);
118     }
119 
120     /**
121      * Finds {@link ViewNode ViewNodes} that have the requested ids.
122      *
123      * @param ids The ids of the node to find.
124      *
125      * @return The nodes indexed in the same way as the ids.
126      *
127      * @hide
128      */
findViewNodesByAutofillIds(@onNull AutofillId[] ids)129     @NonNull public ViewNode[] findViewNodesByAutofillIds(@NonNull AutofillId[] ids) {
130         final LinkedList<ViewNode> nodesToProcess = new LinkedList<>();
131         final ViewNode[] foundNodes = new AssistStructure.ViewNode[ids.length];
132 
133         // Indexes of foundNodes that are not found yet
134         final SparseIntArray missingNodeIndexes = new SparseIntArray(ids.length);
135 
136         for (int i = 0; i < ids.length; i++) {
137             if (mViewNodeLookupTable != null) {
138                 int lookupTableIndex = mViewNodeLookupTable.indexOfKey(ids[i]);
139 
140                 if (lookupTableIndex >= 0) {
141                     foundNodes[i] = mViewNodeLookupTable.valueAt(lookupTableIndex);
142                 } else {
143                     missingNodeIndexes.put(i, /* ignored */ 0);
144                 }
145             } else {
146                 missingNodeIndexes.put(i, /* ignored */ 0);
147             }
148         }
149 
150         final int numWindowNodes = mStructure.getWindowNodeCount();
151         for (int i = 0; i < numWindowNodes; i++) {
152             nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode());
153         }
154 
155         while (missingNodeIndexes.size() > 0 && !nodesToProcess.isEmpty()) {
156             final ViewNode node = nodesToProcess.removeFirst();
157 
158             for (int i = 0; i < missingNodeIndexes.size(); i++) {
159                 final int index = missingNodeIndexes.keyAt(i);
160                 final AutofillId id = ids[index];
161 
162                 if (id.equals(node.getAutofillId())) {
163                     foundNodes[index] = node;
164 
165                     if (mViewNodeLookupTable == null) {
166                         mViewNodeLookupTable = new ArrayMap<>(ids.length);
167                     }
168 
169                     mViewNodeLookupTable.put(id, node);
170 
171                     missingNodeIndexes.removeAt(i);
172                     break;
173                 }
174             }
175 
176             for (int i = 0; i < node.getChildCount(); i++) {
177                 nodesToProcess.addLast(node.getChildAt(i));
178             }
179         }
180 
181         // Remember which ids could not be resolved to not search for them again the next time
182         for (int i = 0; i < missingNodeIndexes.size(); i++) {
183             if (mViewNodeLookupTable == null) {
184                 mViewNodeLookupTable = new ArrayMap<>(missingNodeIndexes.size());
185             }
186 
187             mViewNodeLookupTable.put(ids[missingNodeIndexes.keyAt(i)], null);
188         }
189 
190         return foundNodes;
191     }
192 
193     public static final @android.annotation.NonNull Parcelable.Creator<FillContext> CREATOR =
194             new Parcelable.Creator<FillContext>() {
195         @Override
196         @NonNull
197         public FillContext createFromParcel(Parcel parcel) {
198             return new FillContext(parcel);
199         }
200 
201         @Override
202         @NonNull
203         public FillContext[] newArray(int size) {
204             return new FillContext[size];
205         }
206     };
207 }
208