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 
17 package android.autofillservice.cts;
18 
19 import static android.autofillservice.cts.Helper.FILL_TIMEOUT_MS;
20 
21 import static com.google.common.truth.Truth.assertWithMessage;
22 
23 import android.app.assist.AssistStructure.ViewNode;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.graphics.Paint.Style;
29 import android.graphics.Rect;
30 import android.text.Editable;
31 import android.text.TextUtils;
32 import android.text.TextWatcher;
33 import android.util.AttributeSet;
34 import android.util.Log;
35 import android.util.Pair;
36 import android.util.SparseArray;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.ViewStructure;
40 import android.view.ViewStructure.HtmlInfo;
41 import android.view.autofill.AutofillManager;
42 import android.view.autofill.AutofillValue;
43 
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.concurrent.CountDownLatch;
47 import java.util.concurrent.TimeUnit;
48 
49 class VirtualContainerView extends View {
50 
51     private static final String TAG = "VirtualContainerView";
52 
53     static final String LABEL_CLASS = "my.readonly.view";
54     static final String TEXT_CLASS = "my.editable.view";
55 
56 
57     private final ArrayList<Line> mLines = new ArrayList<>();
58     private final SparseArray<Item> mItems = new SparseArray<>();
59     private final AutofillManager mAfm;
60 
61     private Line mFocusedLine;
62 
63     private Paint mTextPaint;
64     private int mTextHeight;
65     private int mTopMargin;
66     private int mLeftMargin;
67     private int mVerticalGap;
68     private int mLineLength;
69     private int mFocusedColor;
70     private int mUnfocusedColor;
71     private boolean mSync = true;
72     private boolean mOverrideDispatchProvideAutofillStructure = false;
73 
VirtualContainerView(Context context, AttributeSet attrs)74     public VirtualContainerView(Context context, AttributeSet attrs) {
75         super(context, attrs);
76 
77         mAfm = context.getSystemService(AutofillManager.class);
78 
79         mTextPaint = new Paint();
80 
81         mUnfocusedColor = Color.BLACK;
82         mFocusedColor = Color.RED;
83         mTextPaint.setStyle(Style.FILL);
84         mTopMargin = 100;
85         mLeftMargin = 100;
86         mTextHeight = 90;
87         mVerticalGap = 10;
88 
89         mLineLength = mTextHeight + mVerticalGap;
90         mTextPaint.setTextSize(mTextHeight);
91         Log.d(TAG, "Text height: " + mTextHeight);
92     }
93 
94     @Override
autofill(SparseArray<AutofillValue> values)95     public void autofill(SparseArray<AutofillValue> values) {
96         Log.d(TAG, "autofill: " + values);
97         for (int i = 0; i < values.size(); i++) {
98             final int id = values.keyAt(i);
99             final AutofillValue value = values.valueAt(i);
100             final Item item = mItems.get(id);
101             if (item == null) {
102                 Log.w(TAG, "No item for id " + id);
103                 return;
104             }
105             if (!item.editable) {
106                 Log.w(TAG, "Item for id " + id + " is not editable: " + item);
107                 return;
108             }
109             item.text = value.getTextValue();
110             if (item.listener != null) {
111                 Log.d(TAG, "Notify listener: " + item.text);
112                 item.listener.onTextChanged(item.text, 0, 0, 0);
113             }
114         }
115         postInvalidate();
116     }
117 
118     @Override
onDraw(Canvas canvas)119     protected void onDraw(Canvas canvas) {
120         super.onDraw(canvas);
121 
122         Log.d(TAG, "onDraw: " + mLines.size() + " lines; canvas:" + canvas);
123         float x;
124         float y = mTopMargin + mLineLength;
125         for (int i = 0; i < mLines.size(); i++) {
126             x = mLeftMargin;
127             final Line line = mLines.get(i);
128             Log.v(TAG, "Drawing '" + line + "' at " + x + "x" + y);
129             mTextPaint.setColor(line.focused ? mFocusedColor : mUnfocusedColor);
130             final String readOnlyText = line.label.text + ":  [";
131             final String writeText = line.text.text + "]";
132             // Paints the label first...
133             canvas.drawText(readOnlyText, x, y, mTextPaint);
134             // ...then paints the edit text and sets the proper boundary
135             final float deltaX = mTextPaint.measureText(readOnlyText);
136             x += deltaX;
137             line.bounds.set((int) x, (int) (y - mLineLength),
138                     (int) (x + mTextPaint.measureText(writeText)), (int) y);
139             Log.d(TAG, "setBounds(" + x + ", " + y + "): " + line.bounds);
140             canvas.drawText(writeText, x, y, mTextPaint);
141             y += mLineLength;
142         }
143     }
144 
145     @Override
onTouchEvent(MotionEvent event)146     public boolean onTouchEvent(MotionEvent event) {
147         final int y = (int) event.getY();
148         Log.d(TAG, "You can touch this: y=" + y + ", range=" + mLineLength + ", top=" + mTopMargin);
149         int lowerY = mTopMargin;
150         int upperY = -1;
151         for (int i = 0; i < mLines.size(); i++) {
152             upperY = lowerY + mLineLength;
153             final Line line = mLines.get(i);
154             Log.d(TAG, "Line " + i + " ranges from " + lowerY + " to " + upperY);
155             if (lowerY <= y && y <= upperY) {
156                 if (mFocusedLine != null) {
157                     Log.d(TAG, "Removing focus from " + mFocusedLine);
158                     mFocusedLine.changeFocus(false);
159                 }
160                 Log.d(TAG, "Changing focus to " + line);
161                 mFocusedLine = line;
162                 mFocusedLine.changeFocus(true);
163                 invalidate();
164                 break;
165             }
166             lowerY += mLineLength;
167         }
168         return super.onTouchEvent(event);
169     }
170 
171     @Override
dispatchProvideAutofillStructure(ViewStructure structure, int flags)172     public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
173         if (mOverrideDispatchProvideAutofillStructure) {
174             Log.d(TAG, "Overriding dispatchProvideAutofillStructure()");
175             structure.setAutofillId(getAutofillId());
176             onProvideAutofillVirtualStructure(structure, flags);
177         } else {
178             super.dispatchProvideAutofillStructure(structure, flags);
179         }
180     }
181 
182     @Override
onProvideAutofillVirtualStructure(ViewStructure structure, int flags)183     public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
184         Log.d(TAG, "onProvideAutofillVirtualStructure(): flags = " + flags);
185         super.onProvideAutofillVirtualStructure(structure, flags);
186 
187         final String packageName = getContext().getPackageName();
188         structure.setClassName(getClass().getName());
189         final int childrenSize = mItems.size();
190         int index = structure.addChildCount(childrenSize);
191         final String syncMsg = mSync ? "" : " (async)";
192         for (int i = 0; i < childrenSize; i++) {
193             final Item item = mItems.valueAt(i);
194             Log.d(TAG, "Adding new child" + syncMsg + " at index " + index + ": " + item);
195             final ViewStructure child = mSync
196                     ? structure.newChild(index)
197                     : structure.asyncNewChild(index);
198             child.setAutofillId(structure.getAutofillId(), item.id);
199             child.setDataIsSensitive(item.sensitive);
200             index++;
201             final String className = item.editable ? TEXT_CLASS : LABEL_CLASS;
202             child.setClassName(className);
203             // Must set "fake" idEntry because that's what the test cases use to find nodes.
204             child.setId(1000 + index, packageName, "id", item.resourceId);
205             child.setText(item.text);
206             if (TextUtils.getTrimmedLength(item.text) > 0) {
207                 child.setAutofillValue(AutofillValue.forText(item.text));
208             }
209             child.setFocused(item.line.focused);
210             child.setHtmlInfo(child.newHtmlInfoBuilder("TAGGY")
211                     .addAttribute("a1", "v1")
212                     .addAttribute("a2", "v2")
213                     .addAttribute("a1", "v2")
214                     .build());
215             child.setAutofillHints(new String[] {"c", "a", "a", "b", "a", "a"});
216 
217             if (!mSync) {
218                 Log.d(TAG, "Commiting virtual child");
219                 child.asyncCommit();
220             }
221         }
222     }
223 
assertHtmlInfo(ViewNode node)224     static void assertHtmlInfo(ViewNode node) {
225         final String name = node.getText().toString();
226         final HtmlInfo info = node.getHtmlInfo();
227         assertWithMessage("no HTML info on %s", name).that(info).isNotNull();
228         assertWithMessage("wrong HTML tag on %s", name).that(info.getTag()).isEqualTo("TAGGY");
229         assertWithMessage("wrong attributes on %s", name).that(info.getAttributes())
230                 .containsExactly(
231                         new Pair<>("a1", "v1"),
232                         new Pair<>("a2", "v2"),
233                         new Pair<>("a1", "v2"));
234     }
235 
addLine(String labelId, String label, String textId, String text)236     Line addLine(String labelId, String label, String textId, String text) {
237         final Line line = new Line(labelId, label, textId, text);
238         Log.d(TAG, "addLine: " + line);
239         mLines.add(line);
240         mItems.put(line.label.id, line.label);
241         mItems.put(line.text.id, line.text);
242         return line;
243     }
244 
setSync(boolean sync)245     void setSync(boolean sync) {
246         mSync = sync;
247     }
248 
setOverrideDispatchProvideAutofillStructure(boolean flag)249     void setOverrideDispatchProvideAutofillStructure(boolean flag) {
250         mOverrideDispatchProvideAutofillStructure = flag;
251     }
252 
253     private static int nextId;
254 
255     final class Line {
256 
257         final Item label;
258         final Item text;
259         // Boundaries of the text field, relative to the CustomView
260         final Rect bounds = new Rect();
261 
262         private boolean focused;
263 
Line(String labelId, String label, String textId, String text)264         private Line(String labelId, String label, String textId, String text) {
265             this.label = new Item(this, ++nextId, labelId, label, false, false);
266             this.text = new Item(this, ++nextId, textId, text, true, true);
267         }
268 
changeFocus(boolean focused)269         void changeFocus(boolean focused) {
270             this.focused = focused;
271             if (focused) {
272                 final Rect absBounds = getAbsCoordinates();
273                 Log.d(TAG, "focus gained on " + text.id + "; absBounds=" + absBounds);
274                 mAfm.notifyViewEntered(VirtualContainerView.this, text.id, absBounds);
275             } else {
276                 Log.d(TAG, "focus lost on " + text.id);
277                 mAfm.notifyViewExited(VirtualContainerView.this, text.id);
278             }
279         }
280 
getAbsCoordinates()281         Rect getAbsCoordinates() {
282             // Must offset the boundaries so they're relative to the CustomView.
283             final int offset[] = new int[2];
284             getLocationOnScreen(offset);
285             final Rect absBounds = new Rect(bounds.left + offset[0],
286                     bounds.top + offset[1],
287                     bounds.right + offset[0], bounds.bottom + offset[1]);
288             Log.v(TAG, "getAbsCoordinates() for " + text.id + ": bounds=" + bounds
289                     + " offset: " + Arrays.toString(offset) + " absBounds: " + absBounds);
290             return absBounds;
291         }
292 
setTextChangedListener(TextWatcher listener)293         void setTextChangedListener(TextWatcher listener) {
294             text.listener = listener;
295         }
296 
297         @Override
toString()298         public String toString() {
299             return "Label: " + label + " Text: " + text + " Focused: " + focused;
300         }
301 
302         final class OneTimeLineWatcher implements TextWatcher {
303             private final CountDownLatch latch;
304             private final CharSequence expected;
305 
OneTimeLineWatcher(CharSequence expectedValue)306             OneTimeLineWatcher(CharSequence expectedValue) {
307                 this.expected = expectedValue;
308                 this.latch = new CountDownLatch(1);
309             }
310 
311             @Override
beforeTextChanged(CharSequence s, int start, int count, int after)312             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
313             }
314 
315             @Override
onTextChanged(CharSequence s, int start, int before, int count)316             public void onTextChanged(CharSequence s, int start, int before, int count) {
317                 latch.countDown();
318             }
319 
320             @Override
afterTextChanged(Editable s)321             public void afterTextChanged(Editable s) {
322             }
323 
assertAutoFilled()324             void assertAutoFilled() throws Exception {
325                 final boolean set = latch.await(FILL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
326                 assertWithMessage("Timeout (%s ms) on Line %s", FILL_TIMEOUT_MS, label)
327                         .that(set).isTrue();
328                 final String actual = text.text.toString();
329                 assertWithMessage("Wrong auto-fill value on Line %s", label)
330                         .that(actual).isEqualTo(expected.toString());
331             }
332         }
333     }
334 
335     static final class Item {
336         private final Line line;
337         final int id;
338         private final String resourceId;
339         private CharSequence text;
340         private final boolean editable;
341         private final boolean sensitive;
342         private TextWatcher listener;
343 
Item(Line line, int id, String resourceId, CharSequence text, boolean editable, boolean sensitive)344         Item(Line line, int id, String resourceId, CharSequence text, boolean editable,
345                 boolean sensitive) {
346             this.line = line;
347             this.id = id;
348             this.resourceId = resourceId;
349             this.text = text;
350             this.editable = editable;
351             this.sensitive = sensitive;
352         }
353 
354         @Override
toString()355         public String toString() {
356             return id + "/" + resourceId + ": " + text + (editable ? " (editable)" : " (read-only)"
357                     + (sensitive ? " (sensitive)" : " (sanitized"));
358         }
359     }
360 }
361