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