From cec0c701a6c2c00a66bd1c7ec6dd26d13d95d4fd Mon Sep 17 00:00:00 2001 From: Douglas Sigelbaum Date: Tue, 28 Nov 2017 16:18:42 -0800 Subject: Save ClientViewMetadata between pages. Bug: 69771916 Test: Manual Change-Id: I4ec1d73e727c6e98d007af234b9174e46c51a652 --- .../app/edgecases/MultiplePartitionsActivity.java | 7 - .../src/main/res/layout/fragment_edge_cases.xml | 2 - .../res/layout/multiple_partitions_activity.xml | 3 +- .../AutofillFramework/afservice/build.gradle | 4 +- .../android/autofill/service/AuthActivity.java | 4 +- .../autofill/service/MyAutofillService.java | 11 +- .../android/autofill/service/StructureParser.java | 1 - .../autofill/service/data/ClientViewMetadata.java | 171 ++++++++------------- .../service/data/ClientViewMetadataBuilder.java | 79 ++++++++++ .../service/data/adapter/ResponseAdapter.java | 145 ++++++++--------- 10 files changed, 231 insertions(+), 196 deletions(-) create mode 100644 input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java (limited to 'input/autofill/AutofillFramework') diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java index d7395c40..9e09b45b 100644 --- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java +++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/MultiplePartitionsActivity.java @@ -47,19 +47,14 @@ public class MultiplePartitionsActivity extends AppCompatActivity { private ScrollableCustomVirtualView mCustomVirtualView; private AutofillManager mAutofillManager; - private CustomVirtualView.Partition mCredentialsPartition; private CustomVirtualView.Partition mCcPartition; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.multiple_partitions_activity); - mCustomVirtualView = findViewById(R.id.custom_view); - - mCredentialsPartition = mCustomVirtualView.addPartition(getString(R.string.partition_credentials)); mCredentialsPartition.addLine("username", View.AUTOFILL_TYPE_TEXT, @@ -68,7 +63,6 @@ public class MultiplePartitionsActivity extends AppCompatActivity { mCredentialsPartition.addLine("password", View.AUTOFILL_TYPE_TEXT, getString(R.string.password_label), " ", true, View.AUTOFILL_HINT_PASSWORD); - int ccExpirationType = View.AUTOFILL_TYPE_DATE; // TODO: add a checkbox to switch between text / date instead Intent intent = getIntent(); @@ -82,7 +76,6 @@ public class MultiplePartitionsActivity extends AppCompatActivity { Toast.makeText(getApplicationContext(), typeMessage, Toast.LENGTH_LONG).show(); } } - mCcPartition = mCustomVirtualView.addPartition(getString(R.string.partition_credit_card)); mCcPartition.addLine("ccNumber", View.AUTOFILL_TYPE_TEXT, getString(R.string.credit_card_number_label), diff --git a/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml index cca506fa..ab7c32aa 100644 --- a/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml +++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml @@ -72,7 +72,6 @@ android:id="@+id/multistepSignInButton" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" app:imageColor="@android:color/holo_green_light" app:infoText="@string/multi_step_signin_info" app:itemLogo="@drawable/ic_person_black_24dp" @@ -84,7 +83,6 @@ android:id="@+id/multistepCreditCardButton" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" app:imageColor="@android:color/holo_purple" app:infoText="@string/multi_step_cc_info" app:itemLogo="@drawable/ic_spinners_logo_24dp" diff --git a/input/autofill/AutofillFramework/Application/src/main/res/layout/multiple_partitions_activity.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/multiple_partitions_activity.xml index 8b99e6a4..e0fae460 100644 --- a/input/autofill/AutofillFramework/Application/src/main/res/layout/multiple_partitions_activity.xml +++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/multiple_partitions_activity.xml @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. --> - - * Note: this class is not thread safe. + * In this simple implementation, the only view data we collect from the client are autofill hints + * of the views in the view hierarchy, the corresponding autofill IDs, and the {@link SaveInfo} + * based on the hints. */ -public class ClientViewMetadata { - private final StructureParser mStructureParser; - - private List mCachedAllHints; - private Integer mCachedSaveType; - private SaveInfo mCachedSaveInfo; - private List mCachedAutofillIds; - - public ClientViewMetadata(StructureParser parser) { - mStructureParser = parser; - } +public class ClientViewMetadata implements Parcelable { - public List getAllHints() { - if (mCachedAllHints == null) { - parseHints(); + public static final Creator CREATOR = new Creator() { + @Override + public ClientViewMetadata createFromParcel(Parcel parcel) { + return new ClientViewMetadata(parcel); } - return mCachedAllHints; - } - - private List getAutofillIds() { - if (mCachedAutofillIds == null) { - AutofillSaveType autofillSaveType = new AutofillSaveType(); - List autofillIds = new ArrayList<>(); - mStructureParser.parse((node) -> parseSaveTypeAndIds(node, autofillSaveType, autofillIds)); - mCachedSaveType = autofillSaveType.saveType; - mCachedAutofillIds = autofillIds; - } - return mCachedAutofillIds; - } - public AutofillId[] getAutofillIdsArray() { - List autofillIds = getAutofillIds(); - if (autofillIds == null || autofillIds.isEmpty()) { - return null; + @Override + public ClientViewMetadata[] newArray(int size) { + return new ClientViewMetadata[size]; } - return autofillIds.toArray(new AutofillId[autofillIds.size()]); + }; + + private final List mAllHints; + private final int mSaveType; + private final AutofillId[] mAutofillIds; + private final String mWebDomain; + + public ClientViewMetadata(List allHints, int saveType, AutofillId[] autofillIds, + String webDomain) { + mAllHints = allHints; + mSaveType = saveType; + mAutofillIds = autofillIds; + mWebDomain = webDomain; } - public SaveInfo getSaveInfo() { - if (mCachedSaveInfo == null) { - int saveType = getSaveType(); - AutofillId[] autofillIdsArray = getAutofillIdsArray(); - if (autofillIdsArray == null || autofillIdsArray.length == 0) { - return null; - } - // TODO: on MR1, creates a new SaveType without required ids - mCachedSaveInfo = new SaveInfo.Builder(saveType, autofillIdsArray).build(); + private ClientViewMetadata(Parcel parcel) { + mAllHints = new ArrayList<>(); + parcel.readList(mAllHints, String.class.getClassLoader()); + mSaveType = parcel.readInt(); + Parcelable[] ids = parcel.readParcelableArray(AutofillId.class.getClassLoader()); + if (ids != null && ids.length > 0) { + mAutofillIds = Arrays.copyOf(ids, ids.length, AutofillId[].class); + } else { + mAutofillIds = null; } - return mCachedSaveInfo; + mWebDomain = parcel.readString(); } - public int getSaveType() { - if (mCachedSaveType == null) { - AutofillSaveType autofillSaveType = new AutofillSaveType(); - List autofillIds = new ArrayList<>(); - mStructureParser.parse((node) -> parseSaveTypeAndIds(node, autofillSaveType, autofillIds)); - mCachedSaveType = autofillSaveType.saveType; - mCachedAutofillIds = autofillIds; - } - return mCachedSaveType; + public List getAllHints() { + return mAllHints; } - public String buildWebDomain() { - StringBuilder webDomainBuilder = new StringBuilder(); - mStructureParser.parse((node) -> parseWebDomain(node, webDomainBuilder)); - return webDomainBuilder.toString(); + public AutofillId[] getAutofillIds() { + return mAutofillIds; } - private void parseWebDomain(AssistStructure.ViewNode viewNode, StringBuilder validWebDomain) { - String webDomain = viewNode.getWebDomain(); - if (webDomain != null) { - logd("child web domain: %s", webDomain); - if (validWebDomain.length() > 0) { - if (!webDomain.equals(validWebDomain.toString())) { - throw new SecurityException("Found multiple web domains: valid= " - + validWebDomain + ", child=" + webDomain); - } - } else { - validWebDomain.append(webDomain); - } - } - } - - private void parseSaveTypeAndIds(AssistStructure.ViewNode root, - AutofillSaveType autofillSaveType, List autofillIds) { - String[] hints = root.getAutofillHints(); - if (hints != null) { - for (String hint : hints) { - if (AutofillHints.isValidHint(hint)) { - autofillSaveType.saveType |= AutofillHints.getSaveTypeForHint(hint); - autofillIds.add(root.getAutofillId()); - } - } - } + public int getSaveType() { + return mSaveType; } - private void parseHints() { - List allHints = new ArrayList<>(); - mStructureParser.parse((node) -> getHints(node, allHints)); - mCachedAllHints = allHints; + public String getWebDomain() { + return mWebDomain; } - private void getHints(AssistStructure.ViewNode node, List allHints) { - if (node.getAutofillHints() != null) { - String[] hints = node.getAutofillHints(); - Collections.addAll(allHints, hints); - } + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeList(mAllHints); + parcel.writeInt(mSaveType); + parcel.writeParcelableArray(mAutofillIds, 0); + parcel.writeString(mWebDomain); } - public void clearCache() { - mCachedAllHints = null; - mCachedSaveType = null; - mCachedSaveInfo = null; - mCachedAutofillIds = null; + @Override + public int describeContents() { + return 0; } - private class AutofillSaveType { - int saveType; + @Override + public String toString() { + return "ClientViewMetadata{" + + "mAllHints=" + mAllHints + + ", mSaveType=" + mSaveType + + ", mAutofillIds=" + Arrays.toString(mAutofillIds) + + ", mWebDomain='" + mWebDomain + '\'' + + '}'; } } diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java new file mode 100644 index 00000000..394cd72d --- /dev/null +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/ClientViewMetadataBuilder.java @@ -0,0 +1,79 @@ +/* + * 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 com.example.android.autofill.service.data; + + +import android.app.assist.AssistStructure; +import android.util.MutableInt; +import android.view.autofill.AutofillId; + +import com.example.android.autofill.service.AutofillHints; +import com.example.android.autofill.service.StructureParser; + +import java.util.ArrayList; +import java.util.List; + +import static com.example.android.autofill.service.util.Util.logd; + +public class ClientViewMetadataBuilder { + private StructureParser mStructureParser; + + public ClientViewMetadataBuilder(StructureParser parser) { + mStructureParser = parser; + } + + public ClientViewMetadata buildClientViewMetadata() { + List allHints = new ArrayList<>(); + MutableInt saveType = new MutableInt(0); + List autofillIds = new ArrayList<>(); + StringBuilder webDomainBuilder = new StringBuilder(); + mStructureParser.parse((node) -> parseNode(node, allHints, saveType, autofillIds)); + mStructureParser.parse((node) -> parseWebDomain(node, webDomainBuilder)); + String webDomain = webDomainBuilder.toString(); + AutofillId[] autofillIdsArray = autofillIds.toArray(new AutofillId[autofillIds.size()]); + return new ClientViewMetadata(allHints, saveType.value, autofillIdsArray, webDomain); + } + + private void parseWebDomain(AssistStructure.ViewNode viewNode, StringBuilder validWebDomain) { + String webDomain = viewNode.getWebDomain(); + if (webDomain != null) { + logd("child web domain: %s", webDomain); + if (validWebDomain.length() > 0) { + if (!webDomain.equals(validWebDomain.toString())) { + throw new SecurityException("Found multiple web domains: valid= " + + validWebDomain + ", child=" + webDomain); + } + } else { + validWebDomain.append(webDomain); + } + } + } + + private void parseNode(AssistStructure.ViewNode root, List allHints, + MutableInt autofillSaveType, List autofillIds) { + String[] hints = root.getAutofillHints(); + if (hints != null) { + for (String hint : hints) { + if (AutofillHints.isValidHint(hint)) { + allHints.add(hint); + autofillSaveType.value |= AutofillHints.getSaveTypeForHint(hint); + autofillIds.add(root.getAutofillId()); + } + } + } + } +} diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java index 39ebf702..974288d2 100644 --- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java +++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/data/adapter/ResponseAdapter.java @@ -22,7 +22,6 @@ import android.os.Bundle; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.service.autofill.SaveInfo; -import android.view.View; import android.view.autofill.AutofillId; import android.widget.RemoteViews; @@ -31,24 +30,21 @@ import com.example.android.autofill.service.RemoteViewsHelper; import com.example.android.autofill.service.data.ClientViewMetadata; import com.example.android.autofill.service.model.DatasetWithFilledAutofillFields; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; -import static com.example.android.autofill.service.util.Util.bundleToString; -import static com.example.android.autofill.service.util.Util.getSaveTypeAsString; -import static com.example.android.autofill.service.util.Util.logDebugEnabled; import static com.example.android.autofill.service.util.Util.logd; public class ResponseAdapter { public static final String CLIENT_STATE_PARTIAL_ID_TEMPLATE = "partial-%s"; // TODO: move to settings activity and document it private static final boolean SUPPORT_MULTIPLE_STEPS = true; - + static int pageno = 0; private final Context mContext; private final DatasetAdapter mDatasetAdapter; private final String mPackageName; private final ClientViewMetadata mClientViewMetadata; - private final Bundle mPreviousClientState; + private final List mPreviousClientViewMetadatas; public ResponseAdapter(Context context, ClientViewMetadata clientViewMetadata, String packageName, DatasetAdapter datasetAdapter, Bundle clientState) { @@ -56,7 +52,15 @@ public class ResponseAdapter { mClientViewMetadata = clientViewMetadata; mDatasetAdapter = datasetAdapter; mPackageName = packageName; - mPreviousClientState = clientState; + mPreviousClientViewMetadatas = new ArrayList<>(); + if (clientState != null) { + clientState.setClassLoader(getClass().getClassLoader()); + for (int i = pageno - 1; i >= 0; i--) { + ClientViewMetadata previousPage = clientState.getParcelable("client-" + (pageno - 1)); + mPreviousClientViewMetadatas.add(previousPage); + } + logd("previous client state == " + mPreviousClientViewMetadatas); + } } /** @@ -91,74 +95,77 @@ public class ResponseAdapter { } } } - + Bundle clientState = new Bundle(); + clientState.putParcelable("client-" + (pageno++), mClientViewMetadata); int saveType = mClientViewMetadata.getSaveType(); - AutofillId[] autofillIds = mClientViewMetadata.getAutofillIdsArray(); - List allHints = mClientViewMetadata.getAllHints(); - if (logDebugEnabled()) { - logd("setPartialSaveInfo() for type %s: allHints=%s, ids=%s, clientState=%s", - getSaveTypeAsString(saveType), allHints, Arrays.toString(autofillIds), - bundleToString(mPreviousClientState)); - } - // TODO: this should be more generic, but for now it's hardcode to support just activities - // that have an username and a password in separate steps (like MultipleStepsSigninActivity) - if ((saveType != SaveInfo.SAVE_DATA_TYPE_USERNAME - && saveType != SaveInfo.SAVE_DATA_TYPE_PASSWORD) - || autofillIds.length != 1 || allHints.size() != 1) { - logd("Unsupported activity for partial info; returning full"); - SaveInfo saveInfo = mClientViewMetadata.getSaveInfo(); - if (saveInfo != null) { - responseBuilder.setSaveInfo(mClientViewMetadata.getSaveInfo()); - return responseBuilder.build(); - } else { - return null; - } - } - int previousSaveType; - String previousHint; - if (saveType == SaveInfo.SAVE_DATA_TYPE_PASSWORD) { - previousHint = View.AUTOFILL_HINT_USERNAME; - previousSaveType = SaveInfo.SAVE_DATA_TYPE_USERNAME; - } else { - previousHint = View.AUTOFILL_HINT_PASSWORD; - previousSaveType = SaveInfo.SAVE_DATA_TYPE_PASSWORD; - } - String previousKey = String.format(CLIENT_STATE_PARTIAL_ID_TEMPLATE, previousHint); - - AutofillId previousValue = mPreviousClientState == null ? null : mPreviousClientState - .getParcelable(previousKey); - logd("previous: %s=%s", previousKey, previousValue); - - Bundle newClientState = new Bundle(); - String key = String.format(CLIENT_STATE_PARTIAL_ID_TEMPLATE, allHints.get(0)); - AutofillId value = autofillIds[0]; - logd("New client state: %s = %s", key, value); - newClientState.putParcelable(key, value); - - if (previousValue != null) { - AutofillId[] newIds = new AutofillId[]{previousValue, value}; - int newSaveType = saveType | previousSaveType; - logd("new values: type=%s, ids=%s", - getSaveTypeAsString(newSaveType), Arrays.toString(newIds)); - newClientState.putAll(mPreviousClientState); - responseBuilder.setSaveInfo(new SaveInfo.Builder(newSaveType, newIds) - .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) - .build()) - .setClientState(newClientState); - - return responseBuilder.build(); - } - responseBuilder.setClientState(newClientState); - responseBuilder.setSaveInfo(mClientViewMetadata.getSaveInfo()); + AutofillId[] autofillIds = mClientViewMetadata.getAutofillIds(); + SaveInfo saveInfo = new SaveInfo.Builder(saveType, autofillIds).build(); + responseBuilder.setSaveInfo(saveInfo); + responseBuilder.setClientState(clientState); return responseBuilder.build(); +// int saveType = mClientViewMetadata.getSaveType(); +// AutofillId[] autofillIds = mClientViewMetadata.getAutofillIds(); +// List allHints = mClientViewMetadata.getAllHints(); +// if (logDebugEnabled()) { +// logd("setPartialSaveInfo() for type %s: allHints=%s, ids=%s, clientState=%s", +// getSaveTypeAsString(saveType), allHints, Arrays.toString(autofillIds), +// bundleToString(mPreviousClientState)); +// } +// // TODO: this should be more generic, but for now it's hardcode to support just activities +// // that have an username and a password in separate steps (like MultipleStepsSigninActivity) +// if ((saveType != SaveInfo.SAVE_DATA_TYPE_USERNAME +// && saveType != SaveInfo.SAVE_DATA_TYPE_PASSWORD) +// || autofillIds.length != 1 || allHints.size() != 1) { +// logd("Unsupported activity for partial info; returning full"); +// responseBuilder.setSaveInfo(mClientViewMetadata.getSaveInfo()); +// return responseBuilder.build(); +// } +// int previousSaveType; +// String previousHint; +// if (saveType == SaveInfo.SAVE_DATA_TYPE_PASSWORD) { +// previousHint = View.AUTOFILL_HINT_USERNAME; +// previousSaveType = SaveInfo.SAVE_DATA_TYPE_USERNAME; +// } else { +// previousHint = View.AUTOFILL_HINT_PASSWORD; +// previousSaveType = SaveInfo.SAVE_DATA_TYPE_PASSWORD; +// } +// String previousKey = String.format(CLIENT_STATE_PARTIAL_ID_TEMPLATE, previousHint); +// +// AutofillId previousValue = mPreviousClientState == null ? null : mPreviousClientState +// .getParcelable(previousKey); +// logd("previous: %s=%s", previousKey, previousValue); +// +// Bundle newClientState = new Bundle(); +// String key = String.format(CLIENT_STATE_PARTIAL_ID_TEMPLATE, allHints.get(0)); +// AutofillId value = autofillIds[0]; +// logd("New client state: %s = %s", key, value); +// newClientState.putParcelable(key, value); +// +// if (previousValue != null) { +// AutofillId[] newIds = new AutofillId[]{previousValue, value}; +// int newSaveType = saveType | previousSaveType; +// logd("new values: type=%s, ids=%s", +// getSaveTypeAsString(newSaveType), Arrays.toString(newIds)); +// newClientState.putAll(mPreviousClientState); +// responseBuilder.setSaveInfo(new SaveInfo.Builder(newSaveType, newIds) +// .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) +// .build()) +// .setClientState(newClientState); +// +// return responseBuilder.build(); +// } +// responseBuilder.setClientState(newClientState); +// responseBuilder.setSaveInfo(mClientViewMetadata.getSaveInfo()); +// return responseBuilder.build(); } public FillResponse buildResponse(IntentSender sender, RemoteViews remoteViews) { FillResponse.Builder responseBuilder = new FillResponse.Builder(); - AutofillId[] autofillIds = mClientViewMetadata.getAutofillIdsArray(); - SaveInfo saveInfo = mClientViewMetadata.getSaveInfo(); - responseBuilder.setAuthentication(autofillIds, sender, remoteViews); + int saveType = mClientViewMetadata.getSaveType(); + AutofillId[] autofillIds = mClientViewMetadata.getAutofillIds(); + SaveInfo saveInfo = new SaveInfo.Builder(saveType, autofillIds).build(); responseBuilder.setSaveInfo(saveInfo); + responseBuilder.setAuthentication(autofillIds, sender, remoteViews); return responseBuilder.build(); } } -- cgit v1.2.3