1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.example.android.autofillframework.app; 17 18 import android.content.Context; 19 import android.graphics.Canvas; 20 import android.graphics.Color; 21 import android.graphics.Paint; 22 import android.graphics.Paint.Style; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.util.SparseArray; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewStructure; 30 import android.view.autofill.AutofillManager; 31 import android.view.autofill.AutofillValue; 32 import android.widget.EditText; 33 import android.widget.TextView; 34 35 import com.example.android.autofillframework.R; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 40 import static com.example.android.autofillframework.CommonUtil.bundleToString; 41 42 43 /** 44 * Custom View with virtual child views for Username/Password text fields. 45 */ 46 public class CustomVirtualView extends View { 47 48 private static final String TAG = "CustomView"; 49 50 private static final int TOP_MARGIN = 100; 51 private static final int LEFT_MARGIN = 100; 52 private static final int TEXT_HEIGHT = 90; 53 private static final int VERTICAL_GAP = 10; 54 private static final int LINE_HEIGHT = TEXT_HEIGHT + VERTICAL_GAP; 55 private static final int UNFOCUSED_COLOR = Color.BLACK; 56 private static final int FOCUSED_COLOR = Color.RED; 57 private static int sNextId; 58 59 private final ArrayList<Line> mVirtualViewGroups = new ArrayList<>(); 60 private final SparseArray<Item> mVirtualViews = new SparseArray<>(); 61 private final AutofillManager mAutofillManager; 62 63 private Line mFocusedLine; 64 private Paint mTextPaint; 65 66 private Line mUsernameLine; 67 private Line mPasswordLine; 68 69 public CustomVirtualView(Context context, AttributeSet attrs) { 70 super(context, attrs); 71 mAutofillManager = context.getSystemService(AutofillManager.class); 72 mTextPaint = new Paint(); 73 mTextPaint.setStyle(Style.FILL); 74 mTextPaint.setTextSize(TEXT_HEIGHT); 75 mUsernameLine = addLine("usernameField", context.getString(R.string.username_label), 76 new String[]{View.AUTOFILL_HINT_USERNAME}, " ", true); 77 mPasswordLine = addLine("passwordField", context.getString(R.string.password_label), 78 new String[]{View.AUTOFILL_HINT_PASSWORD}, " ", false); 79 } 80 81 @Override 82 public void autofill(SparseArray<AutofillValue> values) { 83 // User has just selected a Dataset from the list of autofill suggestions. 84 // The Dataset is comprised of a list of AutofillValues, with each AutofillValue meant 85 // to fill a specific autofillable view. Now we have to update the UI based on the 86 // AutofillValues in the list. 87 Log.d(TAG, "autoFill(): " + values); 88 for (int i = 0; i < values.size(); i++) { 89 int id = values.keyAt(i); 90 AutofillValue value = values.valueAt(i); 91 Item item = mVirtualViews.get(id); 92 if (item != null && item.editable) { 93 // Set the item's text to the text wrapped in the AutofillValue. 94 item.text = value.getTextValue(); 95 } else if (item == null) { 96 Log.w(TAG, "No item for id " + id); 97 } else { 98 Log.w(TAG, "Item for id " + id + " is not editable: " + item); 99 } 100 } 101 postInvalidate(); 102 } 103 104 105 @Override 106 public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { 107 // Build a ViewStructure that will get passed to the AutofillService by the framework 108 // when it is time to find autofill suggestions. 109 structure.setClassName(getClass().getName()); 110 int childrenSize = mVirtualViews.size(); 111 Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags + ", items = " 112 + childrenSize + ", extras: " + bundleToString(structure.getExtras())); 113 int index = structure.addChildCount(childrenSize); 114 // Traverse through the view hierarchy, including virtual child views. For each view, we 115 // need to set the relevant autofill metadata and add it to the ViewStructure. 116 for (int i = 0; i < childrenSize; i++) { 117 Item item = mVirtualViews.valueAt(i); 118 Log.d(TAG, "Adding new child at index " + index + ": " + item); 119 ViewStructure child = structure.newChild(index); 120 child.setAutofillId(structure.getAutofillId(), item.id); 121 child.setAutofillHints(item.hints); 122 child.setAutofillType(item.type); 123 child.setDataIsSensitive(!item.sanitized); 124 child.setText(item.text); 125 child.setAutofillValue(AutofillValue.forText(item.text)); 126 child.setFocused(item.focused); 127 child.setId(item.id, getContext().getPackageName(), null, item.line.idEntry); 128 child.setClassName(item.getClassName()); 129 index++; 130 } 131 } 132 133 @Override 134 protected void onDraw(Canvas canvas) { 135 super.onDraw(canvas); 136 137 Log.d(TAG, "onDraw: " + mVirtualViewGroups.size() + " lines; canvas:" + canvas); 138 float x; 139 float y = TOP_MARGIN + LINE_HEIGHT; 140 for (int i = 0; i < mVirtualViewGroups.size(); i++) { 141 x = LEFT_MARGIN; 142 Line line = mVirtualViewGroups.get(i); 143 Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y); 144 mTextPaint.setColor(line.fieldTextItem.focused ? FOCUSED_COLOR : UNFOCUSED_COLOR); 145 String readOnlyText = line.labelItem.text + ": ["; 146 String writeText = line.fieldTextItem.text + "]"; 147 // Paints the label first... 148 canvas.drawText(readOnlyText, x, y, mTextPaint); 149 // ...then paints the edit text and sets the proper boundary 150 float deltaX = mTextPaint.measureText(readOnlyText); 151 x += deltaX; 152 line.bounds.set((int) x, (int) (y - LINE_HEIGHT), 153 (int) (x + mTextPaint.measureText(writeText)), (int) y); 154 Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds); 155 canvas.drawText(writeText, x, y, mTextPaint); 156 y += LINE_HEIGHT; 157 } 158 } 159 160 @Override 161 public boolean onTouchEvent(MotionEvent event) { 162 int y = (int) event.getY(); 163 Log.d(TAG, "Touched: y=" + y + ", range=" + LINE_HEIGHT + ", top=" + TOP_MARGIN); 164 int lowerY = TOP_MARGIN; 165 int upperY = -1; 166 for (int i = 0; i < mVirtualViewGroups.size(); i++) { 167 upperY = lowerY + LINE_HEIGHT; 168 Line line = mVirtualViewGroups.get(i); 169 Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY); 170 if (lowerY <= y && y <= upperY) { 171 if (mFocusedLine != null) { 172 Log.d(TAG, "Removing focus from " + mFocusedLine); 173 mFocusedLine.changeFocus(false); 174 } 175 Log.d(TAG, "Changing focus to " + line); 176 mFocusedLine = line; 177 mFocusedLine.changeFocus(true); 178 invalidate(); 179 break; 180 } 181 lowerY += LINE_HEIGHT; 182 } 183 return super.onTouchEvent(event); 184 } 185 186 public CharSequence getUsernameText() { 187 return mUsernameLine.fieldTextItem.text; 188 } 189 190 public CharSequence getPasswordText() { 191 return mPasswordLine.fieldTextItem.text; 192 } 193 194 public void resetFields() { 195 mUsernameLine.reset(); 196 mPasswordLine.reset(); 197 postInvalidate(); 198 } 199 200 private Line addLine(String idEntry, String label, String[] hints, String text, 201 boolean sanitized) { 202 Line line = new Line(idEntry, label, hints, text, sanitized); 203 mVirtualViewGroups.add(line); 204 mVirtualViews.put(line.labelItem.id, line.labelItem); 205 mVirtualViews.put(line.fieldTextItem.id, line.fieldTextItem); 206 return line; 207 } 208 209 private static final class Item { 210 private final Line line; 211 private final int id; 212 private final boolean editable; 213 private final boolean sanitized; 214 private final String[] hints; 215 private final int type; 216 private CharSequence text; 217 private boolean focused = false; 218 219 Item(Line line, int id, String[] hints, int type, CharSequence text, boolean editable, 220 boolean sanitized) { 221 this.line = line; 222 this.id = id; 223 this.text = text; 224 this.editable = editable; 225 this.sanitized = sanitized; 226 this.hints = hints; 227 this.type = type; 228 } 229 230 @Override 231 public String toString() { 232 return id + ": " + text + (editable ? " (editable)" : " (read-only)" 233 + (sanitized ? " (sanitized)" : " (sensitive")); 234 } 235 236 public String getClassName() { 237 return editable ? EditText.class.getName() : TextView.class.getName(); 238 } 239 } 240 241 private final class Line { 242 243 // Boundaries of the text field, relative to the CustomView 244 final Rect bounds = new Rect(); 245 private Item labelItem; 246 private Item fieldTextItem; 247 private String idEntry; 248 249 private Line(String idEntry, String label, String[] hints, String text, boolean sanitized) { 250 this.idEntry = idEntry; 251 this.labelItem = new Item(this, ++sNextId, null, AUTOFILL_TYPE_NONE, label, 252 false, true); 253 this.fieldTextItem = new Item(this, ++sNextId, hints, AUTOFILL_TYPE_TEXT, text, 254 true, sanitized); 255 } 256 257 void changeFocus(boolean focused) { 258 fieldTextItem.focused = focused; 259 if (focused) { 260 Rect absBounds = getAbsCoordinates(); 261 Log.d(TAG, "focus gained on " + fieldTextItem.id + "; absBounds=" + absBounds); 262 mAutofillManager.notifyViewEntered(CustomVirtualView.this, fieldTextItem.id, 263 absBounds); 264 } else { 265 Log.d(TAG, "focus lost on " + fieldTextItem.id); 266 mAutofillManager.notifyViewExited(CustomVirtualView.this, fieldTextItem.id); 267 } 268 } 269 270 private Rect getAbsCoordinates() { 271 // Must offset the boundaries so they're relative to the CustomView. 272 int offset[] = new int[2]; 273 getLocationOnScreen(offset); 274 Rect absBounds = new Rect(bounds.left + offset[0], 275 bounds.top + offset[1], 276 bounds.right + offset[0], bounds.bottom + offset[1]); 277 Log.v(TAG, "getAbsCoordinates() for " + fieldTextItem.id + ": bounds=" + bounds 278 + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds); 279 return absBounds; 280 } 281 282 public void reset() { 283 fieldTextItem.text = " "; 284 } 285 286 @Override 287 public String toString() { 288 return "Label: " + labelItem + " Text: " + fieldTextItem + " Focused: " + 289 fieldTextItem.focused; 290 } 291 } 292 }