/* * Copyright (C) 2017 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 android.service.autofill; import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.util.SparseIntArray; import android.view.autofill.AutofillId; import java.util.LinkedList; /** * This class represents a context for each fill request made via {@link * AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}. * It contains a snapshot of the UI state, the view ids that were returned by * the {@link AutofillService autofill service} as both required to trigger a save * and optional that can be saved, and the id of the corresponding {@link * FillRequest}. *

* This context allows you to inspect the values for the interesting views * in the context they appeared. Also a reference to the corresponding fill * request is useful to store meta-data in the client state bundle passed * to {@link FillResponse.Builder#setClientState(Bundle)} to avoid interpreting * the UI state again while saving. */ public final class FillContext implements Parcelable { private final int mRequestId; private final @NonNull AssistStructure mStructure; /** * Lookup table AutofillId->ViewNode to speed up {@link #findViewNodesByAutofillIds} * This is purely a cache and can be deleted at any time */ @Nullable private ArrayMap mViewNodeLookupTable; /** @hide */ public FillContext(int requestId, @NonNull AssistStructure structure) { mRequestId = requestId; mStructure = structure; } private FillContext(Parcel parcel) { this(parcel.readInt(), parcel.readParcelable(null)); } /** * Gets the id of the {@link FillRequest fill request} this context * corresponds to. This is useful to associate your custom client * state with every request to avoid reinterpreting the UI when saving * user data. * * @return The request id. */ public int getRequestId() { return mRequestId; } /** * @return The screen content. */ public AssistStructure getStructure() { return mStructure; } @Override public String toString() { if (!sDebug) return super.toString(); return "FillContext [reqId=" + mRequestId + "]"; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(mRequestId); parcel.writeParcelable(mStructure, flags); } /** * Finds {@link ViewNode ViewNodes} that have the requested ids. * * @param ids The ids of the node to find. * * @return The nodes indexed in the same way as the ids. * * @hide */ @NonNull public ViewNode[] findViewNodesByAutofillIds(@NonNull AutofillId[] ids) { final LinkedList nodesToProcess = new LinkedList<>(); final ViewNode[] foundNodes = new AssistStructure.ViewNode[ids.length]; // Indexes of foundNodes that are not found yet final SparseIntArray missingNodeIndexes = new SparseIntArray(ids.length); for (int i = 0; i < ids.length; i++) { if (mViewNodeLookupTable != null) { int lookupTableIndex = mViewNodeLookupTable.indexOfKey(ids[i]); if (lookupTableIndex >= 0) { foundNodes[i] = mViewNodeLookupTable.valueAt(lookupTableIndex); } else { missingNodeIndexes.put(i, /* ignored */ 0); } } else { missingNodeIndexes.put(i, /* ignored */ 0); } } final int numWindowNodes = mStructure.getWindowNodeCount(); for (int i = 0; i < numWindowNodes; i++) { nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode()); } while (missingNodeIndexes.size() > 0 && !nodesToProcess.isEmpty()) { final ViewNode node = nodesToProcess.removeFirst(); for (int i = 0; i < missingNodeIndexes.size(); i++) { final int index = missingNodeIndexes.keyAt(i); final AutofillId id = ids[index]; if (id.equals(node.getAutofillId())) { foundNodes[index] = node; if (mViewNodeLookupTable == null) { mViewNodeLookupTable = new ArrayMap<>(ids.length); } mViewNodeLookupTable.put(id, node); missingNodeIndexes.removeAt(i); break; } } for (int i = 0; i < node.getChildCount(); i++) { nodesToProcess.addLast(node.getChildAt(i)); } } // Remember which ids could not be resolved to not search for them again the next time for (int i = 0; i < missingNodeIndexes.size(); i++) { if (mViewNodeLookupTable == null) { mViewNodeLookupTable = new ArrayMap<>(missingNodeIndexes.size()); } mViewNodeLookupTable.put(ids[missingNodeIndexes.keyAt(i)], null); } return foundNodes; } /** * Finds the {@link ViewNode} that has the requested {@code id}, if any. * * @hide */ @Nullable public ViewNode findViewNodeByAutofillId(@NonNull AutofillId id) { final LinkedList nodesToProcess = new LinkedList<>(); final int numWindowNodes = mStructure.getWindowNodeCount(); for (int i = 0; i < numWindowNodes; i++) { nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode()); } while (!nodesToProcess.isEmpty()) { final ViewNode node = nodesToProcess.removeFirst(); if (id.equals(node.getAutofillId())) { return node; } for (int i = 0; i < node.getChildCount(); i++) { nodesToProcess.addLast(node.getChildAt(i)); } } return null; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public FillContext createFromParcel(Parcel parcel) { return new FillContext(parcel); } @Override public FillContext[] newArray(int size) { return new FillContext[size]; } }; }