aboutsummaryrefslogtreecommitdiff
path: root/input/autofill/AutofillFramework
diff options
context:
space:
mode:
authorJisha Abubaker <jishaa@google.com>2018-09-14 14:48:20 -0700
committerJisha Abubaker <jishaa@google.com>2018-09-14 14:48:20 -0700
commit91deb6d993b53450135965994ffb303cec3b18f7 (patch)
tree90cf3019e8b36ccc9ad491acea0547734e1203b0 /input/autofill/AutofillFramework
parent6ba36307f5ad8549ad6807c3c7c648549f7dd710 (diff)
downloadandroid-91deb6d993b53450135965994ffb303cec3b18f7.tar.gz
Squashed commit of the following:
commit 6e3647ed64670ac29a0ba06b3d3a38190f180a10 Author: Felipe Leme <felipeal@google.com> Date: Fri Aug 17 15:19:28 2018 -0700 Created a virtual view that uses accessibility events instead of autofill... ...so it can be used to test Autofill Compatibility mode. Test: manual verification Bug: 112690889 commit d2da7ab50cb33b54fbdc72b2fab6c0b2a4bad178 Author: Felipe Leme <felipeal@google.com> Date: Tue May 8 10:06:53 2018 -0700 Added compat mode support to BasicHeuristicsService. Bug: 75285224 Test: manual verification with Chrome
Diffstat (limited to 'input/autofill/AutofillFramework')
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml1
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java120
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java433
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java363
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java139
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/res/layout/fragment_edge_cases.xml10
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml92
-rw-r--r--input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml6
-rw-r--r--input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml4
-rw-r--r--input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicHeuristicsService.java4
-rw-r--r--input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java22
-rw-r--r--input/autofill/AutofillFramework/afservice/src/main/res/xml/basic_heuristics_service.xml66
12 files changed, 905 insertions, 355 deletions
diff --git a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml
index a8371289..4079e2e2 100644
--- a/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml
+++ b/input/autofill/AutofillFramework/Application/src/main/AndroidManifest.xml
@@ -37,6 +37,7 @@
<activity android:name="com.example.android.autofill.app.commoncases.StandardSignInActivity" />
<activity android:name="com.example.android.autofill.app.commoncases.StandardAutoCompleteSignInActivity" />
<activity android:name="com.example.android.autofill.app.commoncases.VirtualSignInActivity" />
+ <activity android:name="com.example.android.autofill.app.edgecases.VirtualCompatModeSignInActivity" />
<activity android:name="com.example.android.autofill.app.WelcomeActivity" />
<activity android:name="com.example.android.autofill.app.edgecases.CreditCardActivity" />
<activity android:name="com.example.android.autofill.app.commoncases.CreditCardSpinnersActivity" />
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java
new file mode 100644
index 00000000..d55db82d
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/edgecases/VirtualCompatModeSignInActivity.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 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.edgecases;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.autofill.AutofillManager;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.WelcomeActivity;
+import com.example.android.autofill.app.commoncases.VirtualSignInActivity;
+import com.example.android.autofill.app.view.autofillable.CustomVirtualView;
+import com.example.android.autofill.app.view.autofillable.CustomVirtualViewCompatMode;
+
+/**
+ * Activity that uses a virtual views for Username/Password text fields but doesn't explicitly
+ * implement the Autofill APIs but Accessibility's.
+ *
+ * <p><b>Note:</b> this class is useful to test an Autofill service that supports Compatibility
+ * Mode; real applications with a virtual structure should explicitly support Autofill by
+ * implementing its APIs as {@link VirtualSignInActivity} does.
+
+ * <p>Useful to test an Autofill service that supports Compatibility Mode.
+ *
+ * <p><b>Note: </b>you must whitelist this app's package for compatibility mode. For exmaple, in
+ * a UNIX-like OS such as Linux, you can run:
+ *
+ * <pre>
+ * adb shell settings put global autofill_compat_mode_allowed_packages \
+ * `echo -n com.example.android.autofill.app[custom_virtual_login_header]:; \
+ * adb shell settings get global autofill_compat_mode_allowed_packages`
+ * </pre>
+ */
+public class VirtualCompatModeSignInActivity extends AppCompatActivity {
+
+ private CustomVirtualViewCompatMode mCustomVirtualView;
+ private AutofillManager mAutofillManager;
+ private CustomVirtualView.Line mUsernameLine;
+ private CustomVirtualView.Line mPasswordLine;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.virtual_compat_mode_login_activity);
+
+ mCustomVirtualView = findViewById(R.id.custom_view);
+
+ CustomVirtualView.Partition credentialsPartition =
+ mCustomVirtualView.addPartition(getString(R.string.partition_credentials));
+ mUsernameLine = credentialsPartition.addLine("username", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.username_label),
+ " ", false, View.AUTOFILL_HINT_USERNAME);
+ mPasswordLine = credentialsPartition.addLine("password", View.AUTOFILL_TYPE_TEXT,
+ getString(R.string.password_label),
+ " ", true, View.AUTOFILL_HINT_PASSWORD);
+
+ findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ login();
+ }
+ });
+ findViewById(R.id.clear).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ resetFields();
+ mAutofillManager.cancel();
+ }
+ });
+ mAutofillManager = getSystemService(AutofillManager.class);
+ }
+
+ private void resetFields() {
+ mUsernameLine.reset();
+ mPasswordLine.reset();
+ mCustomVirtualView.postInvalidate();
+ }
+
+ /**
+ * Emulates a login action.
+ */
+ private void login() {
+ String username = mUsernameLine.getText().toString();
+ String password = mPasswordLine.getText().toString();
+ boolean valid = isValidCredentials(username, password);
+ if (valid) {
+ Intent intent = WelcomeActivity
+ .getStartActivityIntent(VirtualCompatModeSignInActivity.this);
+ startActivity(intent);
+ finish();
+ } else {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Dummy implementation for demo purposes. A real service should use secure mechanisms to
+ * authenticate users.
+ */
+ public boolean isValidCredentials(String username, String password) {
+ return username != null && password != null && username.equals(password);
+ }
+}
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java
new file mode 100644
index 00000000..cb5df833
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/AbstractCustomVirtualView.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2018 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 android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.autofill.AutofillValue;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.android.autofill.app.R;
+import com.example.android.autofill.app.Util;
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Base class for a custom view that manages its own virtual structure, i.e., this is a leaf
+ * {@link View} in the activity's structure, and it draws its own child UI elements.
+ *
+ * <p>This class only draws the views and provides hooks to integrate them with Android APIs such
+ * as Autofill and Accessibility&mdash;its up to the subclass to implement these integration points.
+ */
+abstract class AbstractCustomVirtualView extends View {
+
+ protected static final boolean DEBUG = true;
+ protected static final boolean VERBOSE = false;
+
+ /**
+ * When set, it notifies AutofillManager of focus change as the view scrolls, so the
+ * autofill UI is continually drawn.
+ * <p>
+ * <p>This is janky and incompatible with the way the autofill UI works on native views, but
+ * it's a cool experiment!
+ */
+ private static final boolean DRAW_AUTOFILL_UI_AFTER_SCROLL = false;
+
+ private static final String TAG = "AbstractCustomVirtualView";
+ private static final int DEFAULT_TEXT_HEIGHT_DP = 34;
+ private static final int VERTICAL_GAP = 10;
+ private static final int UNFOCUSED_COLOR = Color.BLACK;
+ private static final int FOCUSED_COLOR = Color.RED;
+ private static int sNextId;
+ protected final ArrayList<Line> mVirtualViewGroups = new ArrayList<>();
+ protected final SparseArray<Item> mVirtualViews = new SparseArray<>();
+ private final ArrayMap<String, Partition> mPartitionsByName = new ArrayMap<>();
+ protected Line mFocusedLine;
+ protected int mTopMargin;
+ protected int mLeftMargin;
+ private Paint mTextPaint;
+ private int mTextHeight;
+ private int mLineLength;
+
+ protected AbstractCustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mTextPaint = new Paint();
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomVirtualView,
+ defStyleAttr, defStyleRes);
+ int defaultHeight =
+ (int) (DEFAULT_TEXT_HEIGHT_DP * getResources().getDisplayMetrics().density);
+ mTextHeight = typedArray.getDimensionPixelSize(
+ R.styleable.CustomVirtualView_internalTextSize, defaultHeight);
+ typedArray.recycle();
+ resetCoordinates();
+ }
+
+ protected Item getItem(int id) {
+ final Item item = mVirtualViews.get(id);
+ Preconditions.checkArgument(item != null, "No item for id %s: %s", id, mVirtualViews);
+ return item;
+ }
+
+ protected void resetCoordinates() {
+ mTextPaint.setStyle(Style.FILL);
+ mTextPaint.setTextSize(mTextHeight);
+ mTopMargin = getPaddingTop();
+ mLeftMargin = getPaddingStart();
+ mLineLength = mTextHeight + VERTICAL_GAP;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (VERBOSE) {
+ Log.v(TAG, "onDraw(): " + mVirtualViewGroups.size() + " lines; canvas:" + canvas);
+ }
+ float x;
+ float y = mTopMargin + mLineLength;
+ for (int i = 0; i < mVirtualViewGroups.size(); i++) {
+ Line line = mVirtualViewGroups.get(i);
+ x = mLeftMargin;
+ if (VERBOSE) Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
+ mTextPaint.setColor(line.mFieldTextItem.focused ? FOCUSED_COLOR : UNFOCUSED_COLOR);
+ String readOnlyText = line.mLabelItem.text + ": [";
+ String writeText = line.mFieldTextItem.text + "]";
+ // Paints the label first...
+ canvas.drawText(readOnlyText, x, y, mTextPaint);
+ // ...then paints the edit text and sets the proper boundary
+ float deltaX = mTextPaint.measureText(readOnlyText);
+ x += deltaX;
+ line.mBounds.set((int) x, (int) (y - mLineLength),
+ (int) (x + mTextPaint.measureText(writeText)), (int) y);
+ if (VERBOSE) Log.v(TAG, "setBounds(" + x + ", " + y + "): " + line.mBounds);
+ canvas.drawText(writeText, x, y, mTextPaint);
+ y += mLineLength;
+
+ if (DRAW_AUTOFILL_UI_AFTER_SCROLL) {
+ line.notifyFocusChanged();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int y = (int) event.getY();
+ onMotion(y);
+ return super.onTouchEvent(event);
+ }
+
+ /**
+ * Handles a motion event.
+ *
+ * @param y y coordinate.
+ */
+ protected void onMotion(int y) {
+ if (DEBUG) {
+ Log.d(TAG, "onMotion(): y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
+ }
+ int lowerY = mTopMargin;
+ int upperY = -1;
+ for (int i = 0; i < mVirtualViewGroups.size(); i++) {
+ Line line = mVirtualViewGroups.get(i);
+ upperY = lowerY + mLineLength;
+ if (DEBUG) Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY);
+ if (lowerY <= y && y <= upperY) {
+ if (mFocusedLine != null) {
+ Log.d(TAG, "Removing focus from " + mFocusedLine);
+ mFocusedLine.changeFocus(false);
+ }
+ Log.d(TAG, "Changing focus to " + line);
+ mFocusedLine = line;
+ mFocusedLine.changeFocus(true);
+ invalidate();
+ break;
+ }
+ lowerY += mLineLength;
+ }
+ }
+
+ /**
+ * Creates a new partition with the given name.
+ *
+ * @throws IllegalArgumentException if such partition already exists.
+ */
+ public Partition addPartition(String name) {
+ Preconditions.checkNotNull(name, "Name cannot be null.");
+ Preconditions.checkArgument(!mPartitionsByName.containsKey(name),
+ "Partition with such name already exists.");
+ Partition partition = new Partition(name);
+ mPartitionsByName.put(name, partition);
+ return partition;
+ }
+
+
+ protected abstract void notifyFocusGained(int virtualId, Rect bounds);
+
+ protected abstract void notifyFocusLost(int virtualId);
+
+ protected void onLineAdded(int id, Partition partition) {
+ if (VERBOSE) Log.v(TAG, "onLineAdded: id=" + id + ", partition=" + partition);
+ }
+
+ protected void showError(String message) {
+ showMessage(true, message);
+ }
+
+ protected void showMessage(String message) {
+ showMessage(false, message);
+ }
+
+ private void showMessage(boolean warning, String message) {
+ if (warning) {
+ Log.w(TAG, message);
+ } else {
+ Log.i(TAG, message);
+ }
+ Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ protected static final class Item {
+ public final int id;
+ public final String idEntry;
+ public final Line line;
+ public final boolean editable;
+ public final boolean sanitized;
+ public final String[] hints;
+ public final int type;
+ public CharSequence text;
+ public boolean focused = false;
+ public long date;
+ private TextWatcher mListener;
+
+ Item(Line line, int id, String idEntry, String[] hints, int type, CharSequence text,
+ boolean editable, boolean sanitized) {
+ this.line = line;
+ this.id = id;
+ this.idEntry = idEntry;
+ this.text = text;
+ this.editable = editable;
+ this.sanitized = sanitized;
+ this.hints = hints;
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return id + "/" + idEntry + ": "
+ + (type == AUTOFILL_TYPE_DATE ? date : text) // TODO: use DateFormat for date
+ + " (" + Util.getAutofillTypeAsString(type) + ")"
+ + (editable ? " (editable)" : " (read-only)"
+ + (sanitized ? " (sanitized)" : " (sensitive"))
+ + (hints == null ? " (no hints)" : " ( " + Arrays.toString(hints) + ")");
+ }
+
+ protected String getClassName() {
+ return editable ? EditText.class.getName() : TextView.class.getName();
+ }
+
+ protected AutofillValue getAutofillValue() {
+ switch (type) {
+ case AUTOFILL_TYPE_TEXT:
+ return (TextUtils.getTrimmedLength(text) > 0)
+ ? AutofillValue.forText(text)
+ : null;
+ case AUTOFILL_TYPE_DATE:
+ return AutofillValue.forDate(date);
+ default:
+ return null;
+ }
+ }
+
+ protected AccessibilityNodeInfo provideAccessibilityNodeInfo(View parent, Context context) {
+ final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+ node.setSource(parent, id);
+ node.setPackageName(context.getPackageName());
+ node.setClassName(getClassName());
+ node.setEditable(editable);
+ node.setViewIdResourceName(idEntry);
+ node.setVisibleToUser(true);
+ final Rect absBounds = line.getAbsCoordinates();
+ if (absBounds != null) {
+ node.setBoundsInScreen(absBounds);
+ }
+ if (TextUtils.getTrimmedLength(text) > 0) {
+ // TODO: Must checked trimmed length because input fields use 8 empty spaces to
+ // set width
+ node.setText(text);
+ }
+ return node;
+ }
+
+ protected void setText(CharSequence value) {
+ if (!editable) {
+ Log.w(TAG, "Item for id " + id + " is not editable: " + this);
+ return;
+ }
+ text = value;
+ if (mListener != null) {
+ Log.d(TAG, "Notify listener: " + text);
+ mListener.onTextChanged(text, 0, 0, 0);
+ }
+ }
+
+ }
+
+ /**
+ * A partition represents a logical group of items, such as credit card info.
+ */
+ public final class Partition {
+ protected final String mName;
+ protected final SparseArray<Line> mLines = new SparseArray<>();
+
+ private Partition(String name) {
+ mName = name;
+ }
+
+ /**
+ * Adds a new line (containining a label and an input field) to the view.
+ *
+ * @param idEntryPrefix id prefix used to identify the line - label node will be suffixed
+ * with {@code Label} and editable node with {@code Field}.
+ * @param autofillType {@link View#getAutofillType() autofill type} of the field.
+ * @param label text used in the label.
+ * @param text initial text used in the input field.
+ * @param sensitive whether the input is considered sensitive.
+ * @param autofillHints list of autofill hints.
+ * @return the new line.
+ */
+ public Line addLine(String idEntryPrefix, int autofillType, String label, String text,
+ boolean sensitive, String... autofillHints) {
+ Preconditions.checkArgument(autofillType == AUTOFILL_TYPE_TEXT
+ || autofillType == AUTOFILL_TYPE_DATE, "Unsupported type: " + autofillType);
+ Line line = new Line(idEntryPrefix, autofillType, label, autofillHints, text,
+ !sensitive);
+ mVirtualViewGroups.add(line);
+ int id = line.mFieldTextItem.id;
+ mLines.put(id, line);
+ mVirtualViews.put(line.mLabelItem.id, line.mLabelItem);
+ mVirtualViews.put(id, line.mFieldTextItem);
+ onLineAdded(id, this);
+
+ return line;
+ }
+
+ /**
+ * Resets the value of all items in the partition.
+ */
+ public void reset() {
+ for (int i = 0; i < mLines.size(); i++) {
+ mLines.valueAt(i).reset();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+ }
+
+ /**
+ * A line in the virtual view contains a label and an input field.
+ */
+ public final class Line {
+
+ protected final Item mFieldTextItem;
+ // Boundaries of the text field, relative to the CustomView
+ protected final Rect mBounds = new Rect();
+ protected final Item mLabelItem;
+ protected final int mAutofillType;
+
+ private Line(String idEntryPrefix, int autofillType, String label, String[] hints,
+ String text, boolean sanitized) {
+ this.mAutofillType = autofillType;
+ this.mLabelItem = new Item(this, ++sNextId, idEntryPrefix + "Label", null,
+ AUTOFILL_TYPE_NONE, label, false, true);
+ this.mFieldTextItem = new Item(this, ++sNextId, idEntryPrefix + "Field", hints,
+ autofillType, text, true, sanitized);
+ }
+
+ private void changeFocus(boolean focused) {
+ mFieldTextItem.focused = focused;
+ notifyFocusChanged();
+ }
+
+ void notifyFocusChanged() {
+ if (mFieldTextItem.focused) {
+ Rect absBounds = getAbsCoordinates();
+ if (DEBUG) {
+ Log.d(TAG, "focus gained on " + mFieldTextItem.id + "; absBounds=" + absBounds);
+ }
+ notifyFocusGained(mFieldTextItem.id, absBounds);
+ } else {
+ if (DEBUG) Log.d(TAG, "focus lost on " + mFieldTextItem.id);
+ notifyFocusLost(mFieldTextItem.id);
+ }
+ }
+
+ private Rect getAbsCoordinates() {
+ // Must offset the boundaries so they're relative to the CustomView.
+ int[] offset = new int[2];
+ getLocationOnScreen(offset);
+ Rect absBounds = new Rect(mBounds.left + offset[0],
+ mBounds.top + offset[1],
+ mBounds.right + offset[0], mBounds.bottom + offset[1]);
+ if (VERBOSE) {
+ Log.v(TAG, "getAbsCoordinates() for " + mFieldTextItem.id + ": bounds=" + mBounds
+ + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
+ }
+ return absBounds;
+ }
+
+ /**
+ * Gets the value of the input field text.
+ */
+ public CharSequence getText() {
+ return mFieldTextItem.text;
+ }
+
+ /**
+ * Resets the value of the input field text.
+ */
+ public void reset() {
+ mFieldTextItem.text = " ";
+ }
+
+ @Override
+ public String toString() {
+ return "Label: " + mLabelItem + " Text: " + mFieldTextItem
+ + " Focused: " + mFieldTextItem.focused + " Type: " + mAutofillType;
+ }
+ }
+}
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java
index 6c6e1254..67c86617 100644
--- a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualView.java
@@ -13,76 +13,37 @@
* 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.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Paint.Style;
import android.graphics.Rect;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewStructure;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
-import android.widget.EditText;
-import android.widget.TextView;
-import android.widget.Toast;
import com.example.android.autofill.app.R;
import com.example.android.autofill.app.Util;
-import com.google.common.base.Preconditions;
import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Date;
-import static com.example.android.autofill.app.Util.bundleToString;
-
/**
- * A custom View with a virtual structure for fields supporting {@link View#getAutofillHints()}
+ * A custom View with a virtual structure that implements the Autofill APIs.
*/
-public class CustomVirtualView extends View {
-
- protected static final boolean DEBUG = true;
- protected static final boolean VERBOSE = false;
-
- /**
- * When set, it notifies AutofillManager of focus change as the view scrolls, so the
- * autofill UI is continually drawn.
- * <p>
- * <p>This is janky and incompatible with the way the autofill UI works on native views, but
- * it's a cool experiment!
- */
- private static final boolean DRAW_AUTOFILL_UI_AFTER_SCROLL = false;
+public class CustomVirtualView extends AbstractCustomVirtualView {
private static final String TAG = "CustomView";
- private static final int DEFAULT_TEXT_HEIGHT_DP = 34;
- private static final int VERTICAL_GAP = 10;
- private static final int UNFOCUSED_COLOR = Color.BLACK;
- private static final int FOCUSED_COLOR = Color.RED;
- private static int sNextId;
+
protected final AutofillManager mAutofillManager;
- private final ArrayList<Line> mVirtualViewGroups = new ArrayList<>();
- private final SparseArray<Item> mVirtualViews = new SparseArray<>();
private final SparseArray<Partition> mPartitionsByAutofillId = new SparseArray<>();
- private final ArrayMap<String, Partition> mPartitionsByName = new ArrayMap<>();
- protected Line mFocusedLine;
- protected int mTopMargin;
- protected int mLeftMargin;
- private Paint mTextPaint;
- private int mTextHeight;
- private int mLineLength;
public CustomVirtualView(Context context) {
this(context, null);
@@ -92,31 +53,14 @@ public class CustomVirtualView extends View {
this(context, attrs, 0);
}
- public CustomVirtualView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ public CustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
- public CustomVirtualView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ public CustomVirtualView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mAutofillManager = context.getSystemService(AutofillManager.class);
- mTextPaint = new Paint();
- TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomVirtualView,
- defStyleAttr, defStyleRes);
- int defaultHeight =
- (int) (DEFAULT_TEXT_HEIGHT_DP * getResources().getDisplayMetrics().density);
- mTextHeight = typedArray.getDimensionPixelSize(
- R.styleable.CustomVirtualView_internalTextSize, defaultHeight);
- typedArray.recycle();
- resetCoordinates();
- }
-
- protected void resetCoordinates() {
- mTextPaint.setStyle(Style.FILL);
- mTextPaint.setTextSize(mTextHeight);
- mTopMargin = getPaddingTop();
- mLeftMargin = getPaddingStart();
- mLineLength = mTextHeight + VERTICAL_GAP;
}
@Override
@@ -128,7 +72,9 @@ public class CustomVirtualView extends View {
// 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);
+ if (DEBUG) {
+ Log.d(TAG, "autofill(): " + values);
+ }
// First get the name of all partitions in the values
ArraySet<String> partitions = new ArraySet<>();
@@ -205,7 +151,9 @@ public class CustomVirtualView extends View {
// 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);
+ 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);
@@ -225,284 +173,17 @@ public class CustomVirtualView extends View {
}
@Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- if (VERBOSE) {
- Log.v(TAG, "onDraw(): " + mVirtualViewGroups.size() + " lines; canvas:" + canvas);
- }
- float x;
- float y = mTopMargin + mLineLength;
- for (int i = 0; i < mVirtualViewGroups.size(); i++) {
- Line line = mVirtualViewGroups.get(i);
- x = mLeftMargin;
- if (VERBOSE) Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
- mTextPaint.setColor(line.mFieldTextItem.focused ? FOCUSED_COLOR : UNFOCUSED_COLOR);
- String readOnlyText = line.mLabelItem.text + ": [";
- String writeText = line.mFieldTextItem.text + "]";
- // Paints the label first...
- canvas.drawText(readOnlyText, x, y, mTextPaint);
- // ...then paints the edit text and sets the proper boundary
- float deltaX = mTextPaint.measureText(readOnlyText);
- x += deltaX;
- line.mBounds.set((int) x, (int) (y - mLineLength),
- (int) (x + mTextPaint.measureText(writeText)), (int) y);
- if (VERBOSE) Log.v(TAG, "setBounds(" + x + ", " + y + "): " + line.mBounds);
- canvas.drawText(writeText, x, y, mTextPaint);
- y += mLineLength;
-
- if (DRAW_AUTOFILL_UI_AFTER_SCROLL) {
- line.notifyFocusChanged();
- }
- }
+ protected void notifyFocusGained(int virtualId, Rect bounds) {
+ mAutofillManager.notifyViewEntered(this, virtualId, bounds);
}
@Override
- public boolean onTouchEvent(MotionEvent event) {
- int y = (int) event.getY();
- onMotion(y);
- return super.onTouchEvent(event);
+ protected void notifyFocusLost(int virtualId) {
+ mAutofillManager.notifyViewExited(this, virtualId);
}
- /**
- * Handles a motion event.
- *
- * @param y y coordinate.
- */
- protected void onMotion(int y) {
- if (DEBUG) {
- Log.d(TAG, "onMotion(): y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
- }
- int lowerY = mTopMargin;
- int upperY = -1;
- for (int i = 0; i < mVirtualViewGroups.size(); i++) {
- Line line = mVirtualViewGroups.get(i);
- upperY = lowerY + mLineLength;
- if (DEBUG) Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY);
- if (lowerY <= y && y <= upperY) {
- if (mFocusedLine != null) {
- Log.d(TAG, "Removing focus from " + mFocusedLine);
- mFocusedLine.changeFocus(false);
- }
- Log.d(TAG, "Changing focus to " + line);
- mFocusedLine = line;
- mFocusedLine.changeFocus(true);
- invalidate();
- break;
- }
- lowerY += mLineLength;
- }
- }
-
- /**
- * Creates a new partition with the given name.
- *
- * @throws IllegalArgumentException if such partition already exists.
- */
- public Partition addPartition(String name) {
- Preconditions.checkNotNull(name, "Name cannot be null.");
- Preconditions.checkArgument(!mPartitionsByName.containsKey(name),
- "Partition with such name already exists.");
- Partition partition = new Partition(name);
- mPartitionsByName.put(name, partition);
- return partition;
- }
-
- private void showError(String message) {
- showMessage(true, message);
- }
-
- private void showMessage(String message) {
- showMessage(false, message);
- }
-
- private void showMessage(boolean warning, String message) {
- if (warning) {
- Log.w(TAG, message);
- } else {
- Log.i(TAG, message);
- }
- Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show();
- }
-
-
- protected static final class Item {
- protected final int id;
- private final String idEntry;
- private final Line line;
- private final boolean editable;
- private final boolean sanitized;
- private final String[] hints;
- private final int type;
- private CharSequence text;
- private boolean focused = false;
- private long date;
-
- Item(Line line, int id, String idEntry, String[] hints, int type, CharSequence text,
- boolean editable, boolean sanitized) {
- this.line = line;
- this.id = id;
- this.idEntry = idEntry;
- this.text = text;
- this.editable = editable;
- this.sanitized = sanitized;
- this.hints = hints;
- this.type = type;
- }
-
- @Override
- public String toString() {
- return id + "/" + idEntry + ": "
- + (type == AUTOFILL_TYPE_DATE ? date : text) // TODO: use DateFormat for date
- + " (" + Util.getAutofillTypeAsString(type) + ")"
- + (editable ? " (editable)" : " (read-only)"
- + (sanitized ? " (sanitized)" : " (sensitive"))
- + (hints == null ? " (no hints)" : " ( " + Arrays.toString(hints) + ")");
- }
-
- public String getClassName() {
- return editable ? EditText.class.getName() : TextView.class.getName();
- }
-
- public AutofillValue getAutofillValue() {
- switch (type) {
- case AUTOFILL_TYPE_TEXT:
- return (TextUtils.getTrimmedLength(text) > 0)
- ? AutofillValue.forText(text)
- : null;
- case AUTOFILL_TYPE_DATE:
- return AutofillValue.forDate(date);
- default:
- return null;
- }
- }
- }
-
- /**
- * A partition represents a logical group of items, such as credit card info.
- */
- public final class Partition {
- private final String mName;
- private final SparseArray<Line> mLines = new SparseArray<>();
-
- private Partition(String name) {
- mName = name;
- }
-
- /**
- * Adds a new line (containining a label and an input field) to the view.
- *
- * @param idEntryPrefix id prefix used to identify the line - label node will be suffixed
- * with {@code Label} and editable node with {@code Field}.
- * @param autofillType {@link View#getAutofillType() autofill type} of the field.
- * @param label text used in the label.
- * @param text initial text used in the input field.
- * @param sensitive whether the input is considered sensitive.
- * @param autofillHints list of autofill hints.
- * @return the new line.
- */
- public Line addLine(String idEntryPrefix, int autofillType, String label, String text,
- boolean sensitive, String... autofillHints) {
- Preconditions.checkArgument(autofillType == AUTOFILL_TYPE_TEXT ||
- autofillType == AUTOFILL_TYPE_DATE, "Unsupported type: " + autofillType);
- Line line = new Line(idEntryPrefix, autofillType, label, autofillHints, text,
- !sensitive);
- mVirtualViewGroups.add(line);
- int id = line.mFieldTextItem.id;
- mLines.put(id, line);
- mVirtualViews.put(line.mLabelItem.id, line.mLabelItem);
- mVirtualViews.put(id, line.mFieldTextItem);
- mPartitionsByAutofillId.put(id, this);
-
- return line;
- }
-
- /**
- * Resets the value of all items in the partition.
- */
- public void reset() {
- for (int i = 0; i < mLines.size(); i++) {
- mLines.valueAt(i).reset();
- }
- }
-
- @Override
- public String toString() {
- return mName;
- }
- }
-
- /**
- * A line in the virtual view contains a label and an input field.
- */
- public final class Line {
-
- protected final Item mFieldTextItem;
- // Boundaries of the text field, relative to the CustomView
- private final Rect mBounds = new Rect();
- private final Item mLabelItem;
- private final int mAutofillType;
-
- private Line(String idEntryPrefix, int autofillType, String label, String[] hints,
- String text, boolean sanitized) {
- this.mAutofillType = autofillType;
- this.mLabelItem = new Item(this, ++sNextId, idEntryPrefix + "Label", null,
- AUTOFILL_TYPE_NONE, label, false, true);
- this.mFieldTextItem = new Item(this, ++sNextId, idEntryPrefix + "Field", hints,
- autofillType, text, true, sanitized);
- }
-
- private void changeFocus(boolean focused) {
- mFieldTextItem.focused = focused;
- notifyFocusChanged();
- }
-
- void notifyFocusChanged() {
- if (mFieldTextItem.focused) {
- Rect absBounds = getAbsCoordinates();
- if (DEBUG) {
- Log.d(TAG, "focus gained on " + mFieldTextItem.id + "; absBounds=" + absBounds);
- }
- mAutofillManager.notifyViewEntered(CustomVirtualView.this, mFieldTextItem.id,
- absBounds);
- } else {
- if (DEBUG) Log.d(TAG, "focus lost on " + mFieldTextItem.id);
- mAutofillManager.notifyViewExited(CustomVirtualView.this, mFieldTextItem.id);
- }
- }
-
- private Rect getAbsCoordinates() {
- // Must offset the boundaries so they're relative to the CustomView.
- int offset[] = new int[2];
- getLocationOnScreen(offset);
- Rect absBounds = new Rect(mBounds.left + offset[0],
- mBounds.top + offset[1],
- mBounds.right + offset[0], mBounds.bottom + offset[1]);
- if (VERBOSE) {
- Log.v(TAG, "getAbsCoordinates() for " + mFieldTextItem.id + ": bounds=" + mBounds
- + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
- }
- return absBounds;
- }
-
- /**
- * Gets the value of the input field text.
- */
- public CharSequence getText() {
- return mFieldTextItem.text;
- }
-
- /**
- * Resets the value of the input field text.
- */
- public void reset() {
- mFieldTextItem.text = " ";
- }
-
- @Override
- public String toString() {
- return "Label: " + mLabelItem + " Text: " + mFieldTextItem + " Focused: " +
- mFieldTextItem.focused + " Type: " + mAutofillType;
- }
+ @Override
+ protected void onLineAdded(int id, Partition partition) {
+ mPartitionsByAutofillId.put(id, partition);
}
-} \ No newline at end of file
+}
diff --git a/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java
new file mode 100644
index 00000000..7dfb6af6
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/java/com/example/android/autofill/app/view/autofillable/CustomVirtualViewCompatMode.java
@@ -0,0 +1,139 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+
+/**
+ * A custom View with a virtual structure that implements the Accessibility APIs.
+ *
+ * <p><b>Note:</b> this class is useful to test an Autofill service that supports Compatibility
+ * Mode; real applications with a virtual structure should explicitly support Autofill by
+ * implementing its APIs as {@link CustomVirtualView} does.
+ */
+public class CustomVirtualViewCompatMode extends AbstractCustomVirtualView {
+
+ private static final String TAG = "CustomVirtualViewCompatMode";
+
+ private final AccessibilityDelegate mAccessibilityDelegate;
+ private final AccessibilityNodeProvider mAccessibilityNodeProvider;
+
+ public CustomVirtualViewCompatMode(Context context) {
+ this(context, null);
+ }
+
+ public CustomVirtualViewCompatMode(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CustomVirtualViewCompatMode(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CustomVirtualViewCompatMode(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ mAccessibilityNodeProvider = new AccessibilityNodeProvider() {
+ @Override
+ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+ if (DEBUG) {
+ Log.d(TAG, "createAccessibilityNodeInfo(): id=" + virtualViewId);
+ }
+ switch (virtualViewId) {
+ case AccessibilityNodeProvider.HOST_VIEW_ID:
+ return onProvideAutofillCompatModeAccessibilityNodeInfo();
+ default:
+ final Item item = getItem(virtualViewId);
+ return item.provideAccessibilityNodeInfo(CustomVirtualViewCompatMode.this,
+ getContext());
+ }
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
+ final CharSequence text = arguments.getCharSequence(
+ AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
+ final Item item = getItem(virtualViewId);
+ item.setText(text);
+ invalidate();
+ return true;
+ }
+
+ return false;
+ }
+ };
+ mAccessibilityDelegate = new AccessibilityDelegate() {
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
+ return mAccessibilityNodeProvider;
+ }
+ };
+ setAccessibilityDelegate(mAccessibilityDelegate);
+ }
+
+ @Override
+ protected void notifyFocusGained(int virtualId, Rect bounds) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, virtualId);
+ }
+
+ @Override
+ protected void notifyFocusLost(int virtualId) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, virtualId);
+ }
+
+ private void sendAccessibilityEvent(int eventType, int virtualId) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(eventType);
+ event.setSource(this, virtualId);
+ event.setEnabled(true);
+ event.setPackageName(getContext().getPackageName());
+ if (VERBOSE) {
+ Log.v(TAG, "sendAccessibilityEvent(" + eventType + ", " + virtualId + "): " + event);
+ }
+ getContext().getSystemService(AccessibilityManager.class).sendAccessibilityEvent(event);
+ }
+
+ private AccessibilityNodeInfo onProvideAutofillCompatModeAccessibilityNodeInfo() {
+ final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+
+ final String packageName = getContext().getPackageName();
+ node.setPackageName(packageName);
+ node.setClassName(getClass().getName());
+
+ final int childrenSize = mVirtualViews.size();
+ for (int i = 0; i < childrenSize; i++) {
+ final Item item = mVirtualViews.valueAt(i);
+ if (DEBUG) {
+ Log.d(TAG, "Adding new A11Y child with id " + item.id + ": " + item);
+ }
+ node.addChild(this, item.id);
+ }
+ return node;
+ }
+}
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 ab7c32aa..c2d105e5 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
@@ -38,6 +38,16 @@
app:destinationActivityName="com.example.android.autofill.app.commoncases.StandardAutoCompleteSignInActivity"/>
<com.example.android.autofill.app.view.widget.NavigationItem
+ android:id="@+id/virtualViewSignInButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:imageColor="@android:color/holo_green_dark"
+ app:infoText="@string/custom_virtual_compat_mode_login_info"
+ app:itemLogo="@drawable/ic_custom_virtual_logo_24dp"
+ app:labelText="@string/navigation_button_custom_virtual_view_compat_mode_login_label"
+ app:destinationActivityName="com.example.android.autofill.app.edgecases.VirtualCompatModeSignInActivity" />
+
+ <com.example.android.autofill.app.view.widget.NavigationItem
android:id="@+id/multiplePartitionsButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/input/autofill/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml b/input/autofill/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml
new file mode 100644
index 00000000..aeeac4cd
--- /dev/null
+++ b/input/autofill/AutofillFramework/Application/src/main/res/layout/virtual_compat_mode_login_activity.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin">
+
+ <TextView
+ android:id="@+id/custom_virtual_compat_mode_login_header"
+ style="@style/TextAppearance.AppCompat.Large"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:gravity="center"
+ android:text="@string/navigation_button_custom_virtual_view_compat_mode_login_label"
+ app:layout_constraintEnd_toStartOf="@+id/imageButton"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.example.android.autofill.app.view.widget.InfoButton
+ android:id="@+id/imageButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_info_black_24dp"
+ app:dialogText="@string/custom_virtual_login_info"
+ app:layout_constraintBottom_toBottomOf="@+id/custom_virtual_compat_mode_login_header"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/custom_virtual_login_header"
+ app:layout_constraintTop_toTopOf="@+id/custom_virtual_login_header" />
+
+ <com.example.android.autofill.app.view.autofillable.CustomVirtualViewCompatMode
+ android:id="@+id/custom_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/custom_view_height"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:paddingEnd="@dimen/spacing_large"
+ android:paddingStart="@dimen/spacing_large"
+ android:paddingTop="@dimen/spacing_large"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/custom_virtual_compat_mode_login_header" />
+
+ <TextView
+ android:id="@+id/clear"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:layout_marginTop="@dimen/spacing_normal"
+ android:text="@string/clear_label"
+ android:textColor="@android:color/holo_blue_dark"
+ app:layout_constraintEnd_toStartOf="@+id/login"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/custom_view" />
+
+ <TextView
+ android:id="@+id/login"
+ style="@style/Widget.AppCompat.Button.Borderless"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/spacing_normal"
+ android:layout_marginStart="@dimen/spacing_normal"
+ android:text="@string/login_label"
+ android:textColor="@android:color/holo_blue_dark"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/clear"
+ app:layout_constraintTop_toTopOf="@+id/clear" />
+</android.support.constraint.ConstraintLayout>
diff --git a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml
index 2502ab7f..a8441ba4 100644
--- a/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml
+++ b/input/autofill/AutofillFramework/Application/src/main/res/values/strings.xml
@@ -19,6 +19,7 @@
<string name="edge_cases_page_title">Edge Cases</string>
<string name="common_cases_page_title">Common Cases</string>
<string name="navigation_button_custom_virtual_view_login_label">Sample Login Using a Custom Virtual View</string>
+ <string name="navigation_button_custom_virtual_view_compat_mode_login_label">Sample Login Using a Custom Virtual View and Compatibility Mode</string>
<string name="navigation_button_credit_card_label">Sample Credit Card Check Out Using EditTexts</string>
<string name="navigation_button_spinners_credit_card_label">Sample Credit Card Check Out Using Spinners</string>
<string name="navigation_button_edittext_login_label">Sample Login Using EditTexts</string>
@@ -69,6 +70,11 @@
virtual children out of the box, it is necessary implement certain Autofill-specific methods
and interface directly with AutofillManager.
</string>
+ <string name="custom_virtual_compat_mode_login_info">This is a sample login page that uses a
+ custom View with virtual children. This view does not implement the Autofill APIs, but
+ it generates Accessibility events, which can be used to implement Autofill through a
+ 'compatibility mode' feature introduced on Android Pie.
+ </string>
<string name="credit_card_info">This is a sample credit card checkout page that uses
EditTexts to input data into the form.
</string>
diff --git a/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml b/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml
index b02a591d..6ece581e 100644
--- a/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml
+++ b/input/autofill/AutofillFramework/afservice/src/main/AndroidManifest.xml
@@ -39,7 +39,9 @@
android:name=".simple.BasicHeuristicsService"
android:label="Basic Heuristics Autofill Service"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
-
+ <meta-data
+ android:name="android.autofill"
+ android:resource="@xml/basic_heuristics_service"/>
<intent-filter>
<action android:name="android.service.autofill.AutofillService" />
</intent-filter>
diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicHeuristicsService.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicHeuristicsService.java
index 843440c9..81aac2b6 100644
--- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicHeuristicsService.java
+++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicHeuristicsService.java
@@ -92,6 +92,10 @@ public class BasicHeuristicsService extends BasicService {
if (string == null) return null;
string = string.toLowerCase();
+ if (string.contains("label")) {
+ Log.v(TAG, "Ignroing 'label' hint: " + string);
+ return null;
+ }
if (string.contains("password")) return View.AUTOFILL_HINT_PASSWORD;
if (string.contains("username")
|| (string.contains("login") && string.contains("id")))
diff --git a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java
index 92d6436f..73210beb 100644
--- a/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java
+++ b/input/autofill/AutofillFramework/afservice/src/main/java/com/example/android/autofill/service/simple/BasicService.java
@@ -31,7 +31,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.util.Log;
-import android.view.View;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.widget.RemoteViews;
@@ -143,18 +142,15 @@ public class BasicService extends AutofillService {
private void addAutofillableFields(@NonNull Map<String, AutofillId> fields,
@NonNull ViewNode node) {
int type = node.getAutofillType();
- // We're simple, we just autofill text fields.
- if (type == View.AUTOFILL_TYPE_TEXT) {
- String hint = getHint(node);
- if (hint != null) {
- AutofillId id = node.getAutofillId();
- if (!fields.containsKey(hint)) {
- Log.v(TAG, "Setting hint " + hint + " on " + id);
- fields.put(hint, id);
- } else {
- Log.v(TAG, "Ignoring hint " + hint + " on " + id
- + " because it was already set");
- }
+ String hint = getHint(node);
+ if (hint != null) {
+ AutofillId id = node.getAutofillId();
+ if (!fields.containsKey(hint)) {
+ Log.v(TAG, "Setting hint " + hint + " on " + id);
+ fields.put(hint, id);
+ } else {
+ Log.v(TAG, "Ignoring hint " + hint + " on " + id
+ + " because it was already set");
}
}
int childrenSize = node.getChildCount();
diff --git a/input/autofill/AutofillFramework/afservice/src/main/res/xml/basic_heuristics_service.xml b/input/autofill/AutofillFramework/afservice/src/main/res/xml/basic_heuristics_service.xml
new file mode 100644
index 00000000..cfaabb03
--- /dev/null
+++ b/input/autofill/AutofillFramework/afservice/src/main/res/xml/basic_heuristics_service.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 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.
+-->
+
+<autofill-service xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- sample app -->
+ <compatibility-package
+ android:name="com.example.android.autofill.app"
+ android:maxLongVersionCode="10000000000"/>
+
+ <!-- well-known browswers -->
+ <compatibility-package
+ android:name="com.android.chrome"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.chrome.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.chrome.dev"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.chrome.canary"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.microsoft.emmx"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.browser"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.browser.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.mini.native"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.opera.mini.native.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.sec.android.app.sbrowser"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="com.sec.android.app.sbrowser.beta"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.mozilla.fennec_aurora"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.mozilla.firefox"
+ android:maxLongVersionCode="10000000000"/>
+ <compatibility-package
+ android:name="org.mozilla.firefox_beta"
+ android:maxLongVersionCode="10000000000"/>
+</autofill-service>