Home | History | Annotate | Download | only in app
      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 }