diff options
author | Jisha Abubaker <jishaa@google.com> | 2018-09-14 14:48:20 -0700 |
---|---|---|
committer | Jisha Abubaker <jishaa@google.com> | 2018-09-14 14:48:20 -0700 |
commit | 91deb6d993b53450135965994ffb303cec3b18f7 (patch) | |
tree | 90cf3019e8b36ccc9ad491acea0547734e1203b0 /input | |
parent | 6ba36307f5ad8549ad6807c3c7c648549f7dd710 (diff) | |
download | android-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')
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—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> |