/* * 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.app.view.autofillable; import static com.example.android.autofill.app.Util.bundleToString; import android.content.Context; import android.graphics.Rect; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewStructure; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import com.example.android.autofill.app.R; import com.example.android.autofill.app.Util; import java.text.DateFormat; import java.util.Date; /** * A custom View with a virtual structure that implements the Autofill APIs. */ public class CustomVirtualView extends AbstractCustomVirtualView { private static final String TAG = "CustomView"; protected final AutofillManager mAutofillManager; private final SparseArray<Partition> mPartitionsByAutofillId = new SparseArray<>(); public CustomVirtualView(Context context) { this(context, null); } public CustomVirtualView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public CustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mAutofillManager = context.getSystemService(AutofillManager.class); } @Override public void autofill(SparseArray<AutofillValue> values) { Context context = getContext(); // User has just selected a Dataset from the list of autofill suggestions. // The Dataset is comprised of a list of AutofillValues, with each AutofillValue meant // to fill a specific autofillable view. Now we have to update the UI based on the // AutofillValues in the list, but first we make sure all autofilled values belong to the // same partition if (DEBUG) { Log.d(TAG, "autofill(): " + values); } // First get the name of all partitions in the values ArraySet<String> partitions = new ArraySet<>(); for (int i = 0; i < values.size(); i++) { int id = values.keyAt(i); Partition partition = mPartitionsByAutofillId.get(id); if (partition == null) { showError(context.getString(R.string.message_autofill_no_partitions, id, mPartitionsByAutofillId)); return; } partitions.add(partition.mName); } // Then make sure they follow the Highlander rule (There can be only one) if (partitions.size() != 1) { showError(context.getString(R.string.message_autofill_blocked, partitions)); return; } // Finally, autofill it. DateFormat df = android.text.format.DateFormat.getDateFormat(context); for (int i = 0; i < values.size(); i++) { int id = values.keyAt(i); AutofillValue value = values.valueAt(i); Item item = mVirtualViews.get(id); if (item == null) { Log.w(TAG, "No item for id " + id); continue; } if (!item.editable) { showError(context.getString(R.string.message_autofill_readonly, item.text)); continue; } // Check if the type was properly set by the autofill service if (DEBUG) { Log.d(TAG, "Validating " + i + ": expectedType=" + Util.getAutofillTypeAsString(item.type) + "(" + item.type + "), value=" + value); } boolean valid = false; if (value.isText() && item.type == AUTOFILL_TYPE_TEXT) { item.text = value.getTextValue(); valid = true; } else if (value.isDate() && item.type == AUTOFILL_TYPE_DATE) { item.text = df.format(new Date(value.getDateValue())); valid = true; } else { Log.w(TAG, "Unsupported type: " + value); } if (!valid) { item.text = context.getString(R.string.message_autofill_invalid); } } postInvalidate(); showMessage(context.getString(R.string.message_autofill_ok, partitions.valueAt(0))); } @Override public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { // Build a ViewStructure that will get passed to the AutofillService by the framework // when it is time to find autofill suggestions. structure.setClassName(getClass().getName()); int childrenSize = mVirtualViews.size(); if (DEBUG) { Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = " + childrenSize + ", extras: " + bundleToString(structure.getExtras())); } int index = structure.addChildCount(childrenSize); // Traverse through the view hierarchy, including virtual child views. For each view, we // need to set the relevant autofill metadata and add it to the ViewStructure. for (int i = 0; i < childrenSize; i++) { Item item = mVirtualViews.valueAt(i); if (DEBUG) { Log.d(TAG, "Adding new child at index " + index + ": " + item); } ViewStructure child = structure.newChild(index); child.setAutofillId(structure.getAutofillId(), item.id); child.setAutofillHints(item.hints); child.setAutofillType(item.type); child.setAutofillValue(item.getAutofillValue()); child.setDataIsSensitive(!item.sanitized); child.setFocused(item.focused); child.setVisibility(View.VISIBLE); child.setDimens(item.line.mBounds.left, item.line.mBounds.top, 0, 0, item.line.mBounds.width(), item.line.mBounds.height()); child.setId(item.id, getContext().getPackageName(), null, item.idEntry); child.setClassName(item.getClassName()); child.setDimens(item.line.mBounds.left, item.line.mBounds.top, 0, 0, item.line.mBounds.width(), item.line.mBounds.height()); index++; } } @Override protected void notifyFocusGained(int virtualId, Rect bounds) { mAutofillManager.notifyViewEntered(this, virtualId, bounds); } @Override protected void notifyFocusLost(int virtualId) { mAutofillManager.notifyViewExited(this, virtualId); } @Override protected void onLineAdded(int id, Partition partition) { mPartitionsByAutofillId.put(id, partition); } }