blob: 085f827c2638619b7427b51edf835bb3011e2e74 [file] [log] [blame]
/*
* 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.autofillframework.app;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
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 com.example.android.autofillframework.R;
import java.util.ArrayList;
import java.util.Arrays;
import static com.example.android.autofillframework.CommonUtil.bundleToString;
/**
* Custom View with virtual child views for Username/Password text fields.
*/
public class CustomVirtualView extends View {
private static final String TAG = "CustomView";
private static int nextId;
private final ArrayList<Line> mLines = new ArrayList<>();
private final SparseArray<Item> mItems = new SparseArray<>();
private final AutofillManager mAfm;
private Line mFocusedLine;
private Paint mTextPaint;
private int mTextHeight;
private int mTopMargin;
private int mLeftMargin;
private int mVerticalGap;
private int mLineLength;
private int mFocusedColor;
private int mUnfocusedColor;
private Line mUsernameLine;
private Line mPasswordLine;
public CustomVirtualView(Context context, AttributeSet attrs) {
super(context, attrs);
mAfm = context.getSystemService(AutofillManager.class);
mTextPaint = new Paint();
mUnfocusedColor = Color.BLACK;
mFocusedColor = Color.RED;
mTextPaint.setStyle(Style.FILL);
mTopMargin = 100;
mLeftMargin = 100;
mTextHeight = 90;
mVerticalGap = 10;
mLineLength = mTextHeight + mVerticalGap;
mTextPaint.setTextSize(mTextHeight);
mUsernameLine = addLine("usernameField", context.getString(R.string.username_label),
new String[] {View.AUTOFILL_HINT_USERNAME}, " ", true);
mPasswordLine = addLine("passwordField", context.getString(R.string.password_label),
new String[] {View.AUTOFILL_HINT_PASSWORD}, " ", false);
Log.d(TAG, "Text height: " + mTextHeight);
}
@Override
public void autofill(SparseArray<AutofillValue> values) {
// User has just selected a Dataset from the list of Autofill suggestions and the Dataset's
// AutofillValue gets passed into this method.
Log.d(TAG, "autoFill(): " + values);
for (int i = 0; i < values.size(); i++) {
final int id = values.keyAt(i);
final AutofillValue value = values.valueAt(i);
final Item item = mItems.get(id);
if (item == null) {
Log.w(TAG, "No item for id " + id);
return;
}
if (!item.editable) {
Log.w(TAG, "Item for id " + id + " is not editable: " + item);
return;
}
// Set the item's text to the text wrapped in the AutofillValue.
item.text = value.getTextValue();
}
postInvalidate();
}
@Override
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
// Build a ViewStructure to pack in AutoFillService requests.
structure.setClassName(getClass().getName());
int childrenSize = mItems.size();
Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = "
+ childrenSize + ", extras: " + bundleToString(structure.getExtras()));
int index = structure.addChildCount(childrenSize);
for (int i = 0; i < childrenSize; i++) {
Item item = mItems.valueAt(i);
Log.d(TAG, "Adding new child at index " + index + ": " + item);
ViewStructure child = structure.newChild(index);
child.setAutofillId(structure, item.id);
child.setAutofillHints(item.hints);
child.setAutofillType(item.type);
child.setDataIsSensitive(!item.sanitized);
child.setText(item.text);
child.setAutofillValue(AutofillValue.forText(item.text));
child.setFocused(item.focused);
child.setId(item.id, getContext().getPackageName(), null, item.line.idEntry);
child.setClassName(item.getClassName());
index++;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "onDraw: " + mLines.size() + " lines; canvas:" + canvas);
float x;
float y = mTopMargin + mLineLength;
for (int i = 0; i < mLines.size(); i++) {
x = mLeftMargin;
Line line = mLines.get(i);
Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
mTextPaint.setColor(line.fieldTextItem.focused ? mFocusedColor : mUnfocusedColor);
String readOnlyText = line.labelItem.text + ": [";
String writeText = line.fieldTextItem.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.bounds.set((int) x, (int) (y - mLineLength),
(int) (x + mTextPaint.measureText(writeText)), (int) y);
Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds);
canvas.drawText(writeText, x, y, mTextPaint);
y += mLineLength;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
Log.d(TAG, "Touched: y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
int lowerY = mTopMargin;
int upperY = -1;
for (int i = 0; i < mLines.size(); i++) {
upperY = lowerY + mLineLength;
Line line = mLines.get(i);
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;
}
return super.onTouchEvent(event);
}
public CharSequence getUsernameText() {
return mUsernameLine.fieldTextItem.text;
}
public CharSequence getPasswordText() {
return mPasswordLine.fieldTextItem.text;
}
public void resetFields() {
mUsernameLine.reset();
mPasswordLine.reset();
postInvalidate();
}
private Line addLine(String idEntry, String label, String[] hints, String text, boolean sanitized) {
Line line = new Line(idEntry, label, hints, text, sanitized);
mLines.add(line);
mItems.put(line.labelItem.id, line.labelItem);
mItems.put(line.fieldTextItem.id, line.fieldTextItem);
return line;
}
private static final class Item {
private final Line line;
private final int id;
private final boolean editable;
private final boolean sanitized;
private final String[] hints;
private final int type;
private CharSequence text;
private boolean focused = false;
Item(Line line, int id, String[] hints, int type, CharSequence text, boolean editable, boolean sanitized) {
this.line = line;
this.id = id;
this.text = text;
this.editable = editable;
this.sanitized = sanitized;
this.hints = hints;
this.type = type;
}
@Override
public String toString() {
return id + ": " + text + (editable ? " (editable)" : " (read-only)"
+ (sanitized ? " (sanitized)" : " (sensitive"));
}
public String getClassName() {
return editable ? EditText.class.getName() : TextView.class.getName();
}
}
private final class Line {
// Boundaries of the text field, relative to the CustomView
final Rect bounds = new Rect();
private Item labelItem;
private Item fieldTextItem;
private String idEntry;
private Line(String idEntry, String label, String[] hints, String text, boolean sanitized) {
this.idEntry = idEntry;
this.labelItem = new Item(this, ++nextId, null, AUTOFILL_TYPE_NONE, label, false, true);
this.fieldTextItem = new Item(this, ++nextId, hints, AUTOFILL_TYPE_TEXT, text, true, sanitized);
}
void changeFocus(boolean focused) {
fieldTextItem.focused = focused;
if (focused) {
final Rect absBounds = getAbsCoordinates();
Log.d(TAG, "focus gained on " + fieldTextItem.id + "; absBounds=" + absBounds);
mAfm.notifyViewEntered(CustomVirtualView.this, fieldTextItem.id, absBounds);
} else {
Log.d(TAG, "focus lost on " + fieldTextItem.id);
mAfm.notifyViewExited(CustomVirtualView.this, fieldTextItem.id);
}
}
private Rect getAbsCoordinates() {
// Must offset the boundaries so they're relative to the CustomView.
final int offset[] = new int[2];
getLocationOnScreen(offset);
final Rect absBounds = new Rect(bounds.left + offset[0],
bounds.top + offset[1],
bounds.right + offset[0], bounds.bottom + offset[1]);
Log.v(TAG, "getAbsCoordinates() for " + fieldTextItem.id + ": bounds=" + bounds
+ " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
return absBounds;
}
public void reset() {
fieldTextItem.text = " ";
}
@Override
public String toString() {
return "Label: " + labelItem + " Text: " + fieldTextItem + " Focused: " +
fieldTextItem.focused;
}
}
}