Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(840)

Side by Side Diff: chrome/android/java/src/org/chromium/chrome/browser/omnibox/UrlBar.java

Issue 2885973002: Refactor autocomplete-specific logic into a separate class (Closed)
Patch Set: fix junit Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.chrome.browser.omnibox; 5 package org.chromium.chrome.browser.omnibox;
6 6
7 import android.content.ClipData; 7 import android.content.ClipData;
8 import android.content.ClipboardManager; 8 import android.content.ClipboardManager;
9 import android.content.Context; 9 import android.content.Context;
10 import android.content.res.Resources; 10 import android.content.res.Resources;
11 import android.graphics.Canvas; 11 import android.graphics.Canvas;
12 import android.graphics.Paint; 12 import android.graphics.Paint;
13 import android.graphics.Rect; 13 import android.graphics.Rect;
14 import android.net.Uri; 14 import android.net.Uri;
15 import android.os.Build; 15 import android.os.Build;
16 import android.os.StrictMode; 16 import android.os.StrictMode;
17 import android.os.SystemClock; 17 import android.os.SystemClock;
18 import android.text.Editable; 18 import android.text.Editable;
19 import android.text.Layout; 19 import android.text.Layout;
20 import android.text.Selection; 20 import android.text.Selection;
21 import android.text.Spanned;
22 import android.text.TextUtils; 21 import android.text.TextUtils;
23 import android.text.style.ReplacementSpan; 22 import android.text.style.ReplacementSpan;
24 import android.util.AttributeSet; 23 import android.util.AttributeSet;
25 import android.util.Pair; 24 import android.util.Pair;
26 import android.view.GestureDetector; 25 import android.view.GestureDetector;
27 import android.view.KeyEvent; 26 import android.view.KeyEvent;
28 import android.view.MotionEvent; 27 import android.view.MotionEvent;
29 import android.view.View; 28 import android.view.View;
30 import android.view.accessibility.AccessibilityEvent;
31 import android.view.accessibility.AccessibilityManager;
32 import android.view.accessibility.AccessibilityNodeInfo; 29 import android.view.accessibility.AccessibilityNodeInfo;
33 import android.view.inputmethod.BaseInputConnection;
34 import android.view.inputmethod.EditorInfo;
35 import android.view.inputmethod.InputConnection;
36 import android.view.inputmethod.InputConnectionWrapper;
37 import android.widget.TextView; 30 import android.widget.TextView;
38 31
39 import org.chromium.base.ApiCompatibilityUtils; 32 import org.chromium.base.ApiCompatibilityUtils;
40 import org.chromium.base.Log; 33 import org.chromium.base.Log;
41 import org.chromium.base.SysUtils; 34 import org.chromium.base.SysUtils;
42 import org.chromium.base.VisibleForTesting; 35 import org.chromium.base.VisibleForTesting;
43 import org.chromium.chrome.R; 36 import org.chromium.chrome.R;
44 import org.chromium.chrome.browser.WindowDelegate; 37 import org.chromium.chrome.browser.WindowDelegate;
45 import org.chromium.chrome.browser.metrics.StartupMetrics; 38 import org.chromium.chrome.browser.metrics.StartupMetrics;
46 import org.chromium.chrome.browser.omnibox.LocationBarLayout.OmniboxLivenessList ener; 39 import org.chromium.chrome.browser.omnibox.LocationBarLayout.OmniboxLivenessList ener;
40 import org.chromium.chrome.browser.omnibox.view.AutocompleteEditText;
47 import org.chromium.chrome.browser.tab.Tab; 41 import org.chromium.chrome.browser.tab.Tab;
48 import org.chromium.chrome.browser.util.UrlUtilities; 42 import org.chromium.chrome.browser.util.UrlUtilities;
49 import org.chromium.chrome.browser.widget.VerticallyFixedEditText;
50 import org.chromium.content.browser.ContentViewCore; 43 import org.chromium.content.browser.ContentViewCore;
51 import org.chromium.ui.UiUtils; 44 import org.chromium.ui.UiUtils;
52 45
53 import java.net.MalformedURLException; 46 import java.net.MalformedURLException;
54 import java.net.URI; 47 import java.net.URI;
55 import java.net.URISyntaxException; 48 import java.net.URISyntaxException;
56 import java.net.URL; 49 import java.net.URL;
57 50
58 /** 51 /**
59 * The URL text entry view for the Omnibox. 52 * The URL text entry view for the Omnibox.
60 */ 53 */
61 public class UrlBar extends VerticallyFixedEditText { 54 public class UrlBar extends AutocompleteEditText {
62 private static final String TAG = "cr_UrlBar"; 55 private static final String TAG = "cr_UrlBar";
63 56
64 private static final boolean DEBUG = false; 57 private static final boolean DEBUG = false;
65 58
66 // TextView becomes very slow on long strings, so we limit maximum length 59 // TextView becomes very slow on long strings, so we limit maximum length
67 // of what is displayed to the user, see limitDisplayableLength(). 60 // of what is displayed to the user, see limitDisplayableLength().
68 private static final int MAX_DISPLAYABLE_LENGTH = 4000; 61 private static final int MAX_DISPLAYABLE_LENGTH = 4000;
69 private static final int MAX_DISPLAYABLE_LENGTH_LOW_END = 1000; 62 private static final int MAX_DISPLAYABLE_LENGTH_LOW_END = 1000;
70 63
71 // Unicode "Left-To-Right Mark" (LRM) character. 64 // Unicode "Left-To-Right Mark" (LRM) character.
(...skipping 10 matching lines...) Expand all
82 /** 75 /**
83 * The text direction of the URL or query: LAYOUT_DIRECTION_LOCALE, LAYOUT_D IRECTION_LTR, or 76 * The text direction of the URL or query: LAYOUT_DIRECTION_LOCALE, LAYOUT_D IRECTION_LTR, or
84 * LAYOUT_DIRECTION_RTL. 77 * LAYOUT_DIRECTION_RTL.
85 * */ 78 * */
86 private int mUrlDirection; 79 private int mUrlDirection;
87 80
88 private UrlBarDelegate mUrlBarDelegate; 81 private UrlBarDelegate mUrlBarDelegate;
89 82
90 private UrlDirectionListener mUrlDirectionListener; 83 private UrlDirectionListener mUrlDirectionListener;
91 84
92 private final AutocompleteSpan mAutocompleteSpan;
93
94 /** 85 /**
95 * The gesture detector is used to detect long presses. Long presses require special treatment 86 * The gesture detector is used to detect long presses. Long presses require special treatment
96 * because the URL bar has custom touch event handling. See: {@link #onTouch Event}. 87 * because the URL bar has custom touch event handling. See: {@link #onTouch Event}.
97 */ 88 */
98 private final GestureDetector mGestureDetector; 89 private final GestureDetector mGestureDetector;
99 90
100 private final KeyboardHideHelper mKeyboardHideHelper; 91 private final KeyboardHideHelper mKeyboardHideHelper;
101 92
102 private boolean mFocused; 93 private boolean mFocused;
103 private boolean mAllowFocus = true; 94 private boolean mAllowFocus = true;
104 95
105 private final int mDarkHintColor; 96 private final int mDarkHintColor;
106 private final int mDarkDefaultTextColor; 97 private final int mDarkDefaultTextColor;
107 private final int mDarkHighlightColor; 98 private final int mDarkHighlightColor;
108 99
109 private final int mLightHintColor; 100 private final int mLightHintColor;
110 private final int mLightDefaultTextColor; 101 private final int mLightDefaultTextColor;
111 private final int mLightHighlightColor; 102 private final int mLightHighlightColor;
112 103
113 private Boolean mUseDarkColors; 104 private Boolean mUseDarkColors;
114 105
115 private AccessibilityManager mAccessibilityManager;
116
117 /**
118 * Whether default TextView scrolling should be disabled because autocomplet e has been added.
119 * This allows the user entered text to be shown instead of the end of the a utocomplete.
120 */
121 private boolean mDisableTextScrollingFromAutocomplete;
122
123 private OmniboxLivenessListener mOmniboxLivenessListener; 106 private OmniboxLivenessListener mOmniboxLivenessListener;
124 107
125 private long mFirstFocusTimeMs; 108 private long mFirstFocusTimeMs;
126 109
127 private boolean mInBatchEditMode;
128 private int mBeforeBatchEditAutocompleteIndex = -1;
129 private String mBeforeBatchEditFullText;
130 private boolean mSelectionChangedInBatchMode;
131 private boolean mTextDeletedInBatchMode;
132
133 private boolean mIsPastedText;
134 // Used as a hint to indicate the text may contain an ellipsize span. This will be true if an 110 // Used as a hint to indicate the text may contain an ellipsize span. This will be true if an
135 // ellispize span was applied the last time the text changed. A true value here does not 111 // ellispize span was applied the last time the text changed. A true value here does not
136 // guarantee that the text does contain the span currently as newly set text may have cleared 112 // guarantee that the text does contain the span currently as newly set text may have cleared
137 // this (and it the value will only be recalculated after the text has been changed). 113 // this (and it the value will only be recalculated after the text has been changed).
138 private boolean mDidEllipsizeTextHint; 114 private boolean mDidEllipsizeTextHint;
139 115
140 // Set to true when the URL bar text is modified programmatically. Initially set
141 // to true until the old state has been loaded.
142 private boolean mIgnoreTextChangeFromAutocomplete = true;
143 private boolean mLastUrlEditWasDelete;
144
145 /** This tracks whether or not the last ACTION_DOWN event was when the url b ar had focus. */ 116 /** This tracks whether or not the last ACTION_DOWN event was when the url b ar had focus. */
146 boolean mDownEventHadFocus; 117 boolean mDownEventHadFocus;
147 118
148 /** 119 /**
149 * Implement this to get updates when the direction of the text in the URL b ar changes. 120 * Implement this to get updates when the direction of the text in the URL b ar changes.
150 * E.g. If the user is typing a URL, then erases it and starts typing a quer y in Arabic, 121 * E.g. If the user is typing a URL, then erases it and starts typing a quer y in Arabic,
151 * the direction will change from left-to-right to right-to-left. 122 * the direction will change from left-to-right to right-to-left.
152 */ 123 */
153 interface UrlDirectionListener { 124 interface UrlDirectionListener {
154 /** 125 /**
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
201 mLightDefaultTextColor = 172 mLightDefaultTextColor =
202 ApiCompatibilityUtils.getColor(resources, R.color.url_emphasis_l ight_default_text); 173 ApiCompatibilityUtils.getColor(resources, R.color.url_emphasis_l ight_default_text);
203 mLightHintColor = 174 mLightHintColor =
204 ApiCompatibilityUtils.getColor(resources, R.color.locationbar_li ght_hint_text); 175 ApiCompatibilityUtils.getColor(resources, R.color.locationbar_li ght_hint_text);
205 mLightHighlightColor = ApiCompatibilityUtils.getColor(resources, 176 mLightHighlightColor = ApiCompatibilityUtils.getColor(resources,
206 R.color.locationbar_light_selection_color); 177 R.color.locationbar_light_selection_color);
207 178
208 setUseDarkTextColors(true); 179 setUseDarkTextColors(true);
209 180
210 mUrlDirection = LAYOUT_DIRECTION_LOCALE; 181 mUrlDirection = LAYOUT_DIRECTION_LOCALE;
211 mAutocompleteSpan = new AutocompleteSpan();
212 182
213 // The URL Bar is derived from an text edit class, and as such is focusa ble by 183 // The URL Bar is derived from an text edit class, and as such is focusa ble by
214 // default. This means that if it is created before the first draw of th e UI it 184 // default. This means that if it is created before the first draw of th e UI it
215 // will (as the only focusable element of the UI) get focus on the first draw. 185 // will (as the only focusable element of the UI) get focus on the first draw.
216 // We react to this by greying out the tab area and bringing up the keyb oard, 186 // We react to this by greying out the tab area and bringing up the keyb oard,
217 // which we don't want to do at startup. Prevent this by disabling focus until 187 // which we don't want to do at startup. Prevent this by disabling focus until
218 // the first draw. 188 // the first draw.
219 setFocusable(false); 189 setFocusable(false);
220 setFocusableInTouchMode(false); 190 setFocusableInTouchMode(false);
221 191
(...skipping 10 matching lines...) Expand all
232 return true; 202 return true;
233 } 203 }
234 }); 204 });
235 mGestureDetector.setOnDoubleTapListener(null); 205 mGestureDetector.setOnDoubleTapListener(null);
236 mKeyboardHideHelper = new KeyboardHideHelper(this, new Runnable() { 206 mKeyboardHideHelper = new KeyboardHideHelper(this, new Runnable() {
237 @Override 207 @Override
238 public void run() { 208 public void run() {
239 if (mUrlBarDelegate != null) mUrlBarDelegate.backKeyPressed(); 209 if (mUrlBarDelegate != null) mUrlBarDelegate.backKeyPressed();
240 } 210 }
241 }); 211 });
242
243 mAccessibilityManager =
244 (AccessibilityManager) context.getSystemService(Context.ACCESSIB ILITY_SERVICE);
245 } 212 }
246 213
247 /** 214 /**
248 * Initialize the delegate that allows interaction with the Window. 215 * Initialize the delegate that allows interaction with the Window.
249 */ 216 */
250 public void setWindowDelegate(WindowDelegate windowDelegate) { 217 public void setWindowDelegate(WindowDelegate windowDelegate) {
251 mKeyboardHideHelper.setWindowDelegate(windowDelegate); 218 mKeyboardHideHelper.setWindowDelegate(windowDelegate);
252 } 219 }
253 220
254 /** 221 /**
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
296 } 263 }
297 264
298 @Override 265 @Override
299 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 266 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
300 if (KeyEvent.KEYCODE_BACK == keyCode && event.getAction() == KeyEvent.AC TION_UP) { 267 if (KeyEvent.KEYCODE_BACK == keyCode && event.getAction() == KeyEvent.AC TION_UP) {
301 mKeyboardHideHelper.monitorForKeyboardHidden(); 268 mKeyboardHideHelper.monitorForKeyboardHidden();
302 } 269 }
303 return super.onKeyPreIme(keyCode, event); 270 return super.onKeyPreIme(keyCode, event);
304 } 271 }
305 272
306 /**
307 * Sets whether text changes should trigger autocomplete.
308 * <p>
309 * {@link #setDelegate(UrlBarDelegate)} must be called with a non-null insta nce prior to
310 * enabling autocomplete.
311 *
312 * @param ignoreAutocomplete Whether text changes should be ignored and no a uto complete
313 * triggered.
314 */
315 public void setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete) {
316 assert mUrlBarDelegate != null;
Ted C 2017/05/17 18:29:32 should we keep this but call super.setIgnore...
Changwan Ryu 2017/05/17 20:18:56 Done
317
318 mIgnoreTextChangeFromAutocomplete = ignoreAutocomplete;
319 }
320
321 /**
322 * @return The search query text (non-null).
323 */
324 public String getQueryText() {
325 return getEditableText() != null ? getEditableText().toString() : "";
326 }
327
328 /**
329 * @return Whether the current cursor position is at the end of the user typ ed text (i.e.
330 * at the beginning of the inline autocomplete text if present other wise the very
331 * end of the current text).
332 */
333 private boolean isCursorAtEndOfTypedText() {
334 final int selectionStart = getSelectionStart();
335 final int selectionEnd = getSelectionEnd();
336
337 int expectedSelectionStart = getText().getSpanStart(mAutocompleteSpan);
338 int expectedSelectionEnd = getText().length();
339 if (expectedSelectionStart < 0) {
340 expectedSelectionStart = expectedSelectionEnd;
341 }
342
343 return selectionStart == expectedSelectionStart && selectionEnd == expec tedSelectionEnd;
344 }
345
346 /**
347 * @return Whether the URL is currently in batch edit mode triggered by an I ME. No external
348 * text changes should be triggered while this is true.
349 */
350 // isInBatchEditMode is a package protected method on TextView, so we intent ionally chose
351 // a different name.
352 private boolean isHandlingBatchInput() {
353 return mInBatchEditMode;
354 }
355
356 /**
357 * @return The user text without the autocomplete text.
358 */
359 public String getTextWithoutAutocomplete() {
360 int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan);
361 if (autoCompleteIndex < 0) {
362 return getQueryText();
363 } else {
364 return getQueryText().substring(0, autoCompleteIndex);
365 }
366 }
367
368 /** @return Whether any autocomplete information is specified on the current text. */
369 @VisibleForTesting
370 protected boolean hasAutocomplete() {
371 return getText().getSpanStart(mAutocompleteSpan) >= 0
372 || mAutocompleteSpan.mAutocompleteText != null
373 || mAutocompleteSpan.mUserText != null;
374 }
375
376 /**
377 * Whether we want to be showing inline autocomplete results. We don't want to show them as the
378 * user deletes input. Also if there is a composition (e.g. while using the Japanese IME),
379 * we must not autocomplete or we'll destroy the composition.
380 * @return Whether we want to be showing inline autocomplete results.
381 */
382 public boolean shouldAutocomplete() {
383 if (mLastUrlEditWasDelete) return false;
384 Editable text = getText();
385
386 return isCursorAtEndOfTypedText()
387 && !isPastedText()
388 && !isHandlingBatchInput()
389 && BaseInputConnection.getComposingSpanEnd(text)
390 == BaseInputConnection.getComposingSpanStart(text);
391 }
392
393 @Override
394 public void onBeginBatchEdit() {
395 if (DEBUG) Log.i(TAG, "onBeginBatchEdit");
396 mBeforeBatchEditAutocompleteIndex = getText().getSpanStart(mAutocomplete Span);
397 mBeforeBatchEditFullText = getText().toString();
398
399 super.onBeginBatchEdit();
400 mInBatchEditMode = true;
401 mTextDeletedInBatchMode = false;
402 }
403
404 @Override
405 public void onEndBatchEdit() {
406 if (DEBUG) Log.i(TAG, "onEndBatchEdit");
407 super.onEndBatchEdit();
408 mInBatchEditMode = false;
409 limitDisplayableLength();
410 if (mSelectionChangedInBatchMode) {
411 validateSelection(getSelectionStart(), getSelectionEnd());
412 mSelectionChangedInBatchMode = false;
413 }
414
415 String newText = getText().toString();
416 if (!TextUtils.equals(mBeforeBatchEditFullText, newText)
417 || getText().getSpanStart(mAutocompleteSpan) != mBeforeBatchEdit AutocompleteIndex) {
418 // If the text being typed is a single character that matches the ne xt character in the
419 // previously visible autocomplete text, we reapply the autocomplete text to prevent
420 // a visual flickering when the autocomplete text is cleared and the n quickly reapplied
421 // when the next round of suggestions is received.
422 if (shouldAutocomplete() && mBeforeBatchEditAutocompleteIndex != -1
423 && mBeforeBatchEditFullText != null
424 && mBeforeBatchEditFullText.startsWith(newText)
425 && !mTextDeletedInBatchMode
426 && newText.length() - mBeforeBatchEditAutocompleteIndex == 1 ) {
427 setAutocompleteText(newText, mBeforeBatchEditFullText.substring( newText.length()));
428 }
429 notifyAutocompleteTextStateChanged(mTextDeletedInBatchMode);
430 }
431
432 mTextDeletedInBatchMode = false;
433 mBeforeBatchEditAutocompleteIndex = -1;
434 mBeforeBatchEditFullText = null;
435 }
436
437 @Override
438 protected void onSelectionChanged(int selStart, int selEnd) {
439 if (DEBUG) Log.i(TAG, "onSelectionChanged -- selStart: %d, selEnd: %d", selStart, selEnd);
440 if (!mInBatchEditMode) {
441 int beforeTextLength = getText().length();
442 if (validateSelection(selStart, selEnd)) {
443 boolean textDeleted = getText().length() < beforeTextLength;
444 notifyAutocompleteTextStateChanged(textDeleted);
445 }
446 } else {
447 mSelectionChangedInBatchMode = true;
448 }
449 super.onSelectionChanged(selStart, selEnd);
450 }
451
452 /**
453 * Validates the selection and clears the autocomplete span if needed. The autocomplete text
454 * will be deleted if the selection occurs entirely before the autocomplete region.
455 *
456 * @param selStart The start of the selection.
457 * @param selEnd The end of the selection.
458 * @return Whether the autocomplete span was removed as a result of this val idation.
459 */
460 private boolean validateSelection(int selStart, int selEnd) {
461 int spanStart = getText().getSpanStart(mAutocompleteSpan);
462 int spanEnd = getText().getSpanEnd(mAutocompleteSpan);
463
464 if (DEBUG) {
465 Log.i(TAG, "validateSelection -- selStart: %d, selEnd: %d, spanStart : %d, spanEnd: %d",
466 selStart, selEnd, spanStart, spanEnd);
467 }
468
469 if (spanStart >= 0 && (spanStart != selStart || spanEnd != selEnd)) {
470 CharSequence previousAutocompleteText = mAutocompleteSpan.mAutocompl eteText;
471
472 // On selection changes, the autocomplete text has been accepted by the user or needs
473 // to be deleted below.
474 mAutocompleteSpan.clearSpan();
475
476 // The autocomplete text will be deleted any time the selection occu rs entirely before
477 // the start of the autocomplete text. This is required because cer tain keyboards will
478 // insert characters temporarily when starting a key entry gesture ( whether it be
479 // swyping a word or long pressing to get a special character). Whe n this temporary
480 // character appears, Chrome may decide to append some autocomplete, but the keyboard
481 // will then remove this temporary character only while leaving the autocomplete text
482 // alone. See crbug/273763 for more details.
483 if (selEnd <= spanStart && TextUtils.equals(previousAutocompleteText ,
484 getText().subSequence(spanStart, getText().length()))) {
485 getText().delete(spanStart, getText().length());
486 }
487 return true;
488 }
489 return false;
490 }
491
492 @Override 273 @Override
493 protected void onFocusChanged(boolean focused, int direction, Rect previousl yFocusedRect) { 274 protected void onFocusChanged(boolean focused, int direction, Rect previousl yFocusedRect) {
494 mFocused = focused; 275 mFocused = focused;
495 if (!focused) mAutocompleteSpan.clearSpan();
496 super.onFocusChanged(focused, direction, previouslyFocusedRect); 276 super.onFocusChanged(focused, direction, previouslyFocusedRect);
497 277
498 if (focused && mFirstFocusTimeMs == 0) { 278 if (focused && mFirstFocusTimeMs == 0) {
499 mFirstFocusTimeMs = SystemClock.elapsedRealtime(); 279 mFirstFocusTimeMs = SystemClock.elapsedRealtime();
500 if (mOmniboxLivenessListener != null) mOmniboxLivenessListener.onOmn iboxFocused(); 280 if (mOmniboxLivenessListener != null) mOmniboxLivenessListener.onOmn iboxFocused();
501 } 281 }
502 282
503 if (focused) StartupMetrics.getInstance().recordFocusedOmnibox(); 283 if (focused) StartupMetrics.getInstance().recordFocusedOmnibox();
504 284
505 fixupTextDirection(); 285 fixupTextDirection();
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
596 @Override 376 @Override
597 public boolean performLongClick(float x, float y) { 377 public boolean performLongClick(float x, float y) {
598 // If the touch event that triggered this was when the url bar was in a different focus 378 // If the touch event that triggered this was when the url bar was in a different focus
599 // state, ignore the event. 379 // state, ignore the event.
600 if (mDownEventHadFocus != mFocused) return true; 380 if (mDownEventHadFocus != mFocused) return true;
601 381
602 return super.performLongClick(x, y); 382 return super.performLongClick(x, y);
603 } 383 }
604 384
605 @Override 385 @Override
606 public boolean bringPointIntoView(int offset) {
607 if (mDisableTextScrollingFromAutocomplete) return false;
608 return super.bringPointIntoView(offset);
609 }
610
611 @Override
612 public boolean onPreDraw() {
613 boolean retVal = super.onPreDraw();
614 if (mDisableTextScrollingFromAutocomplete) {
615 // super.onPreDraw will put the selection at the end of the text sel ection, but
616 // in the case of autocomplete we want the last typed character to b e shown, which
617 // is the start of selection.
618 mDisableTextScrollingFromAutocomplete = false;
619 bringPointIntoView(getSelectionStart());
620 retVal = true;
621 }
622 return retVal;
623 }
624
625 @Override
626 public void onDraw(Canvas canvas) { 386 public void onDraw(Canvas canvas) {
627 super.onDraw(canvas); 387 super.onDraw(canvas);
628 388
629 if (!mFirstDrawComplete) { 389 if (!mFirstDrawComplete) {
630 mFirstDrawComplete = true; 390 mFirstDrawComplete = true;
631 391
632 // We have now avoided the first draw problem (see the comment in 392 // We have now avoided the first draw problem (see the comment in
633 // the constructor) so we want to make the URL bar focusable so that 393 // the constructor) so we want to make the URL bar focusable so that
634 // touches etc. activate it. 394 // touches etc. activate it.
635 setFocusable(mAllowFocus); 395 setFocusable(mAllowFocus);
(...skipping 100 matching lines...) Expand 10 before | Expand all | Expand 10 after
736 if (isFocused()) { 496 if (isFocused()) {
737 final int selStart = getSelectionStart(); 497 final int selStart = getSelectionStart();
738 final int selEnd = getSelectionEnd(); 498 final int selEnd = getSelectionEnd();
739 499
740 min = Math.max(0, Math.min(selStart, selEnd)); 500 min = Math.max(0, Math.min(selStart, selEnd));
741 max = Math.max(0, Math.max(selStart, selEnd)); 501 max = Math.max(0, Math.max(selStart, selEnd));
742 } 502 }
743 503
744 Selection.setSelection(getText(), max); 504 Selection.setSelection(getText(), max);
745 getText().replace(min, max, pasteString); 505 getText().replace(min, max, pasteString);
746 mIsPastedText = true; 506 onPaste();
747 return true; 507 return true;
748 } 508 }
749 } 509 }
750 510
751 if (mOriginalUrlLocation == null || mFormattedUrlLocation == null) { 511 if (mOriginalUrlLocation == null || mFormattedUrlLocation == null) {
752 return super.onTextContextMenuItem(id); 512 return super.onTextContextMenuItem(id);
753 } 513 }
754 514
755 int selectedStartIndex = getSelectionStart(); 515 int selectedStartIndex = getSelectionStart();
756 int selectedEndIndex = getSelectionEnd(); 516 int selectedEndIndex = getSelectionEnd();
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after
818 578
819 Editable previousText = getEditableText(); 579 Editable previousText = getEditableText();
820 setText(formattedUrl); 580 setText(formattedUrl);
821 581
822 if (!isFocused()) scrollToTLD(); 582 if (!isFocused()) scrollToTLD();
823 583
824 return !TextUtils.equals(previousText, getEditableText()); 584 return !TextUtils.equals(previousText, getEditableText());
825 } 585 }
826 586
827 /** 587 /**
828 * Autocompletes the text on the url bar and selects the text that was not e ntered by the
829 * user. Using append() instead of setText() to preserve the soft-keyboard l ayout.
830 * @param userText user The text entered by the user.
831 * @param inlineAutocompleteText The suggested autocompletion for the user's text.
832 */
833 public void setAutocompleteText(CharSequence userText, CharSequence inlineAu tocompleteText) {
834 if (DEBUG) {
835 Log.i(TAG, "setAutocompleteText -- userText: %s, inlineAutocompleteT ext: %s",
836 userText, inlineAutocompleteText);
837 }
838 boolean emptyAutocomplete = TextUtils.isEmpty(inlineAutocompleteText);
839
840 if (!emptyAutocomplete) mDisableTextScrollingFromAutocomplete = true;
841
842 int autocompleteIndex = userText.length();
843
844 String previousText = getQueryText();
845 CharSequence newText = TextUtils.concat(userText, inlineAutocompleteText );
846
847 setIgnoreTextChangesForAutocomplete(true);
848
849 if (!TextUtils.equals(previousText, newText)) {
850 // The previous text may also have included autocomplete text, so we only
851 // append the new autocomplete text that has changed.
852 if (TextUtils.indexOf(newText, previousText) == 0) {
853 append(newText.subSequence(previousText.length(), newText.length ()));
854 } else {
855 setUrl(newText.toString(), null);
856 }
857 }
858
859 if (getSelectionStart() != autocompleteIndex
860 || getSelectionEnd() != getText().length()) {
861 setSelection(autocompleteIndex, getText().length());
862
863 if (inlineAutocompleteText.length() != 0) {
864 // Sending a TYPE_VIEW_TEXT_SELECTION_CHANGED accessibility even t causes the
865 // previous TYPE_VIEW_TEXT_CHANGED event to be swallowed. As a r esult the user
866 // hears the autocomplete text but *not* the text they typed. In stead we send a
867 // TYPE_ANNOUNCEMENT event, which doesn't swallow the text-chang ed event.
868 announceForAccessibility(inlineAutocompleteText);
869 }
870 }
871
872 if (emptyAutocomplete) {
873 mAutocompleteSpan.clearSpan();
874 } else {
875 mAutocompleteSpan.setSpan(userText, inlineAutocompleteText);
876 }
877
878 setIgnoreTextChangesForAutocomplete(false);
879 }
880
881 /**
882 * Returns the length of the autocomplete text currently displayed, zero if none is
883 * currently displayed.
884 */
885 public int getAutocompleteLength() {
886 int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan);
887 if (autoCompleteIndex < 0) return 0;
888 return getText().length() - autoCompleteIndex;
889 }
890
891 /**
892 * Scroll to ensure the TLD is visible. 588 * Scroll to ensure the TLD is visible.
893 * @return Whether the TLD was discovered and successfully scrolled to. 589 * @return Whether the TLD was discovered and successfully scrolled to.
894 */ 590 */
895 public boolean scrollToTLD() { 591 public boolean scrollToTLD() {
896 Editable url = getText(); 592 Editable url = getText();
897 if (url == null || url.length() < 1) return false; 593 if (url == null || url.length() < 1) return false;
898 String urlString = url.toString(); 594 String urlString = url.toString();
899 Pair<String, String> urlComponents = 595 Pair<String, String> urlComponents =
900 LocationBarLayout.splitPathFromUrlDisplayText(urlString); 596 LocationBarLayout.splitPathFromUrlDisplayText(urlString);
901 597
902 if (TextUtils.isEmpty(urlComponents.first)) return false; 598 if (TextUtils.isEmpty(urlComponents.first)) return false;
903 599
904 // Do not scroll to the end of the host for URLs such as data:, javascri pt:, etc... 600 // Do not scroll to the end of the host for URLs such as data:, javascri pt:, etc...
905 if (urlComponents.second == null) { 601 if (urlComponents.second == null) {
906 Uri uri = Uri.parse(urlString); 602 Uri uri = Uri.parse(urlString);
907 String scheme = uri.getScheme(); 603 String scheme = uri.getScheme();
908 if (!TextUtils.isEmpty(scheme) 604 if (!TextUtils.isEmpty(scheme)
909 && LocationBarLayout.UNSUPPORTED_SCHEMES_TO_SPLIT.contains(s cheme)) { 605 && LocationBarLayout.UNSUPPORTED_SCHEMES_TO_SPLIT.contains(s cheme)) {
910 return false; 606 return false;
911 } 607 }
912 } 608 }
913 609
914 setSelection(urlComponents.first.length()); 610 setSelection(urlComponents.first.length());
915 return true; 611 return true;
916 } 612 }
917 613
918 @Override 614 @Override
919 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
920 if (DEBUG) {
921 Log.i(TAG, "onTextChanged -- text: %s, start: %d, lengthBefore: %d, lengthAfter: %d",
922 text, start, lengthBefore, lengthAfter);
923 }
924
925 super.onTextChanged(text, start, lengthBefore, lengthAfter);
926 if (!mInBatchEditMode) {
927 limitDisplayableLength();
928 notifyAutocompleteTextStateChanged(lengthAfter == 0);
929 } else {
930 mTextDeletedInBatchMode = lengthAfter == 0;
931 }
932 mIsPastedText = false;
933 }
934
935 @Override
936 public void setText(CharSequence text, BufferType type) { 615 public void setText(CharSequence text, BufferType type) {
937 if (DEBUG) Log.i(TAG, "setText -- text: %s", text); 616 if (DEBUG) Log.i(TAG, "setText -- text: %s", text);
938 617 super.setText(text, type);
939 mDisableTextScrollingFromAutocomplete = false;
940
941 // Avoid setting the same text to the URL bar as it will mess up the scr oll/cursor
942 // position.
943 // Setting the text is also quite expensive, so only do it when the text has changed
944 // (since we apply spans when the URL is not focused, we only optimize t his when the
945 // URL is being edited).
946 if (!TextUtils.equals(getEditableText(), text)) {
947 // Certain OEM implementations of setText trigger disk reads. crbug. com/633298
948 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads( );
949 try {
950 super.setText(text, type);
951 } finally {
952 StrictMode.setThreadPolicy(oldPolicy);
953 }
954 }
955
956 // Verify the autocomplete is still valid after the text change.
957 // Note: mAutocompleteSpan may be still null here if setText() is called in View
958 // constructor.
959 if (mAutocompleteSpan != null
960 && mAutocompleteSpan.mUserText != null
961 && mAutocompleteSpan.mAutocompleteText != null) {
962 if (getText().getSpanStart(mAutocompleteSpan) < 0) {
963 mAutocompleteSpan.clearSpan();
964 } else {
965 clearAutocompleteSpanIfInvalid();
966 }
967 }
968
969 fixupTextDirection(); 618 fixupTextDirection();
970 } 619 }
971 620
972 private void clearAutocompleteSpanIfInvalid() {
973 Editable editableText = getEditableText();
974 CharSequence previousUserText = mAutocompleteSpan.mUserText;
975 CharSequence previousAutocompleteText = mAutocompleteSpan.mAutocompleteT ext;
976 if (editableText.length()
977 != (previousUserText.length() + previousAutocompleteText.length( ))) {
978 mAutocompleteSpan.clearSpan();
979 } else if (TextUtils.indexOf(getText(), previousUserText) != 0
980 || TextUtils.indexOf(getText(),
981 previousAutocompleteText, previousUserText.length()) != 0) {
982 mAutocompleteSpan.clearSpan();
983 }
984 }
985
986 private void limitDisplayableLength() { 621 private void limitDisplayableLength() {
987 // To limit displayable length we replace middle portion of the string w ith ellipsis. 622 // To limit displayable length we replace middle portion of the string w ith ellipsis.
988 // That affects only presentation of the text, and doesn't affect other aspects like 623 // That affects only presentation of the text, and doesn't affect other aspects like
989 // copying to the clipboard, getting text with getText(), etc. 624 // copying to the clipboard, getting text with getText(), etc.
990 final int maxLength = SysUtils.isLowEndDevice() 625 final int maxLength = SysUtils.isLowEndDevice()
991 ? MAX_DISPLAYABLE_LENGTH_LOW_END : MAX_DISPLAYABLE_LENGTH; 626 ? MAX_DISPLAYABLE_LENGTH_LOW_END : MAX_DISPLAYABLE_LENGTH;
992 627
993 Editable text = getText(); 628 Editable text = getText();
994 int textLength = text.length(); 629 int textLength = text.length();
995 if (textLength <= maxLength) { 630 if (textLength <= maxLength) {
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
1032 if (pathIndex > 0) { 667 if (pathIndex > 0) {
1033 urlPrePath = url.substring(0, pathIndex); 668 urlPrePath = url.substring(0, pathIndex);
1034 } else { 669 } else {
1035 urlPrePath = url; 670 urlPrePath = url;
1036 } 671 }
1037 } 672 }
1038 return urlPrePath; 673 return urlPrePath;
1039 } 674 }
1040 675
1041 @Override 676 @Override
1042 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
1043 if (mIgnoreTextChangeFromAutocomplete) {
1044 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECT ION_CHANGED
1045 || event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT _CHANGED) {
1046 return;
1047 }
1048 }
1049 super.sendAccessibilityEventUnchecked(event);
1050 }
1051
1052 @Override
1053 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 677 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1054 // Certain OEM implementations of onInitializeAccessibilityNodeInfo trig ger disk reads 678 // Certain OEM implementations of onInitializeAccessibilityNodeInfo trig ger disk reads
1055 // to access the clipboard. crbug.com/640993 679 // to access the clipboard. crbug.com/640993
1056 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); 680 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
1057 try { 681 try {
1058 super.onInitializeAccessibilityNodeInfo(info); 682 super.onInitializeAccessibilityNodeInfo(info);
1059 } finally { 683 } finally {
1060 StrictMode.setThreadPolicy(oldPolicy); 684 StrictMode.setThreadPolicy(oldPolicy);
1061 } 685 }
1062 } 686 }
1063 687
1064 @VisibleForTesting
1065 InputConnectionWrapper mInputConnection = new InputConnectionWrapper(null, t rue) {
1066 private final char[] mTempSelectionChar = new char[1];
1067
1068 @Override
1069 public boolean commitText(CharSequence text, int newCursorPosition) {
1070 Editable currentText = getText();
1071 if (currentText == null) return super.commitText(text, newCursorPosi tion);
1072
1073 int selectionStart = Selection.getSelectionStart(currentText);
1074 int selectionEnd = Selection.getSelectionEnd(currentText);
1075 int autocompleteIndex = currentText.getSpanStart(mAutocompleteSpan);
1076 // If the text being committed is a single character that matches th e next character
1077 // in the selection (assumed to be the autocomplete text), we only m ove the text
1078 // selection instead clearing the autocomplete text causing flickeri ng as the
1079 // autocomplete text will appear once the next suggestions are recei ved.
1080 //
1081 // To be confident that the selection is an autocomplete, we ensure the selection
1082 // is at least one character and the end of the selection is the end of the
1083 // currently entered text.
1084 if (newCursorPosition == 1 && selectionStart > 0 && selectionStart ! = selectionEnd
1085 && selectionEnd >= currentText.length()
1086 && autocompleteIndex == selectionStart
1087 && text.length() == 1) {
1088 currentText.getChars(selectionStart, selectionStart + 1, mTempSe lectionChar, 0);
1089 if (mTempSelectionChar[0] == text.charAt(0)) {
1090
1091 // Since the text isn't changing, TalkBack won't read out th e typed characters.
1092 // To work around this, explicitly send an accessibility eve nt. crbug.com/416595
1093 if (mAccessibilityManager != null && mAccessibilityManager.i sEnabled()) {
1094 AccessibilityEvent event = AccessibilityEvent.obtain(
1095 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
1096 event.setFromIndex(selectionStart);
1097 event.setRemovedCount(0);
1098 event.setAddedCount(1);
1099 event.setBeforeText(currentText.toString().substring(0, selectionStart));
1100 sendAccessibilityEventUnchecked(event);
1101 }
1102
1103 setAutocompleteText(
1104 currentText.subSequence(0, selectionStart + 1),
1105 currentText.subSequence(selectionStart + 1, selectio nEnd));
1106 if (!mInBatchEditMode) {
1107 notifyAutocompleteTextStateChanged(false);
1108 }
1109 return true;
1110 }
1111 }
1112
1113 boolean retVal = super.commitText(text, newCursorPosition);
1114
1115 // Ensure the autocomplete span is removed if it is no longer valid after committing the
1116 // text.
1117 if (getText().getSpanStart(mAutocompleteSpan) >= 0) clearAutocomplet eSpanIfInvalid();
1118
1119 return retVal;
1120 }
1121
1122 @Override
1123 public boolean setComposingText(CharSequence text, int newCursorPosition ) {
1124 Editable currentText = getText();
1125 int autoCompleteSpanStart = currentText.getSpanStart(mAutocompleteSp an);
1126 if (autoCompleteSpanStart >= 0) {
1127 int composingEnd = BaseInputConnection.getComposingSpanEnd(curre ntText);
1128
1129 // On certain device/keyboard combinations, the composing region s are specified
1130 // with a noticeable delay after the initial character is typed, and in certain
1131 // circumstances it does not check that the current state of the text matches the
1132 // expectations of it's composing region.
1133 // For example, you can be typing:
1134 // chrome://f
1135 // Chrome will autocomplete to:
1136 // chrome://f[lags]
1137 // And after the autocomplete has been set, the keyboard will se t the composing
1138 // region to the last character and it assumes it is 'f' as it w as the last
1139 // character the keyboard sent. If we commit this composition, the text will
1140 // look like:
1141 // chrome://flag[f]
1142 // And if we use the autocomplete clearing logic below, it will look like:
1143 // chrome://f[f]
1144 // To work around this, we see if the composition matches all th e characters prior
1145 // to the autocomplete and just readjust the composing region to be that subset.
1146 //
1147 // See crbug.com/366732
1148 if (composingEnd == currentText.length()
1149 && autoCompleteSpanStart >= text.length()
1150 && TextUtils.equals(
1151 currentText.subSequence(
1152 autoCompleteSpanStart - text.length(),
1153 autoCompleteSpanStart),
1154 text)) {
1155 setComposingRegion(
1156 autoCompleteSpanStart - text.length(), autoCompleteS panStart);
1157 }
1158
1159 // Once composing text is being modified, the autocomplete text has been accepted
1160 // or has to be deleted.
1161 mAutocompleteSpan.clearSpan();
1162 Selection.setSelection(currentText, autoCompleteSpanStart);
1163 currentText.delete(autoCompleteSpanStart, currentText.length());
1164 }
1165 return super.setComposingText(text, newCursorPosition);
1166 }
1167 };
1168
1169 @Override
1170 public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
1171 mInputConnection.setTarget(super.onCreateInputConnection(outAttrs));
1172 return mInputConnection;
1173 }
1174
1175 /** 688 /**
1176 * Emphasize components of the URL for readability. 689 * Emphasize components of the URL for readability.
1177 */ 690 */
1178 public void emphasizeUrl() { 691 public void emphasizeUrl() {
1179 Editable url = getText(); 692 Editable url = getText();
1180 if (OmniboxUrlEmphasizer.hasEmphasisSpans(url) || hasFocus()) { 693 if (OmniboxUrlEmphasizer.hasEmphasisSpans(url) || hasFocus()) {
1181 return; 694 return;
1182 } 695 }
1183 696
1184 if (url.length() < 1) { 697 if (url.length() < 1) {
(...skipping 16 matching lines...) Expand all
1201 mUseDarkColors, mUrlBarDelegate.shouldEmphasizeHttpsScheme()); 714 mUseDarkColors, mUrlBarDelegate.shouldEmphasizeHttpsScheme());
1202 } 715 }
1203 716
1204 /** 717 /**
1205 * Reset the modifications done to emphasize components of the URL. 718 * Reset the modifications done to emphasize components of the URL.
1206 */ 719 */
1207 public void deEmphasizeUrl() { 720 public void deEmphasizeUrl() {
1208 OmniboxUrlEmphasizer.deEmphasizeUrl(getText()); 721 OmniboxUrlEmphasizer.deEmphasizeUrl(getText());
1209 } 722 }
1210 723
1211 /**
1212 * @return Whether the current UrlBar input has been pasted from the clipboa rd.
1213 */
1214 public boolean isPastedText() {
1215 return mIsPastedText;
1216 }
1217
1218 @Override 724 @Override
1219 public CharSequence getAccessibilityClassName() { 725 public CharSequence getAccessibilityClassName() {
1220 // When UrlBar is used as a read-only TextView, force Talkback to pronou nce it like 726 // When UrlBar is used as a read-only TextView, force Talkback to pronou nce it like
1221 // TextView. Otherwise Talkback will say "Edit box, http://...". crbug.c om/636988 727 // TextView. Otherwise Talkback will say "Edit box, http://...". crbug.c om/636988
1222 if (isEnabled()) { 728 if (isEnabled()) {
1223 return super.getAccessibilityClassName(); 729 return super.getAccessibilityClassName();
1224 } else { 730 } else {
1225 return TextView.class.getName(); 731 return TextView.class.getName();
1226 } 732 }
1227 } 733 }
1228 734
1229 private void notifyAutocompleteTextStateChanged(boolean textDeleted) { 735 /**
736 * @return The search query text (non-null).
737 */
738 public String getQueryText() {
Ted C 2017/05/17 18:29:32 Can we just convert all the callers to use getText
Changwan Ryu 2017/05/17 20:18:56 Done.
739 return getTextWithAutocomplete();
740 }
741
742 @Override
743 public void setTextFromAutocomplete(String text) {
744 setUrl(text, null);
745 }
746
747 @Override
748 public void onAutocompleteTextStateChanged(boolean textDeleted, boolean upda teDisplay) {
1230 if (mUrlBarDelegate == null) return; 749 if (mUrlBarDelegate == null) return;
1231 if (!hasFocus()) return; 750 if (updateDisplay) limitDisplayableLength();
1232 if (mIgnoreTextChangeFromAutocomplete) return;
1233 751
1234 mLastUrlEditWasDelete = textDeleted;
1235 mUrlBarDelegate.onTextChangedForAutocomplete(textDeleted); 752 mUrlBarDelegate.onTextChangedForAutocomplete(textDeleted);
1236 } 753 }
1237 754
1238 /** 755 /**
1239 * Simple span used for tracking the current autocomplete state.
1240 */
1241 private class AutocompleteSpan {
1242 private CharSequence mUserText;
1243 private CharSequence mAutocompleteText;
1244
1245 /**
1246 * Adds the span to the current text.
1247 * @param userText The user entered text.
1248 * @param autocompleteText The autocomplete text being appended.
1249 */
1250 public void setSpan(CharSequence userText, CharSequence autocompleteText ) {
1251 Editable text = getText();
1252 text.removeSpan(this);
1253 mAutocompleteText = autocompleteText;
1254 mUserText = userText;
1255 text.setSpan(
1256 this,
1257 userText.length(),
1258 text.length(),
1259 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1260 }
1261
1262 /** Removes this span from the current text and clears the internal stat e. */
1263 public void clearSpan() {
1264 getText().removeSpan(this);
1265 mAutocompleteText = null;
1266 mUserText = null;
1267 }
1268 }
1269
1270 /**
1271 * Span that displays ellipsis instead of the text. Used to hide portion of 756 * Span that displays ellipsis instead of the text. Used to hide portion of
1272 * very large string to get decent performance from TextView. 757 * very large string to get decent performance from TextView.
1273 */ 758 */
1274 private static class EllipsisSpan extends ReplacementSpan { 759 private static class EllipsisSpan extends ReplacementSpan {
1275 private static final String ELLIPSIS = "..."; 760 private static final String ELLIPSIS = "...";
1276 761
1277 public static final EllipsisSpan INSTANCE = new EllipsisSpan(); 762 public static final EllipsisSpan INSTANCE = new EllipsisSpan();
1278 763
1279 @Override 764 @Override
1280 public int getSize(Paint paint, CharSequence text, 765 public int getSize(Paint paint, CharSequence text,
1281 int start, int end, Paint.FontMetricsInt fm) { 766 int start, int end, Paint.FontMetricsInt fm) {
1282 return (int) paint.measureText(ELLIPSIS); 767 return (int) paint.measureText(ELLIPSIS);
1283 } 768 }
1284 769
1285 @Override 770 @Override
1286 public void draw(Canvas canvas, CharSequence text, int start, int end, 771 public void draw(Canvas canvas, CharSequence text, int start, int end,
1287 float x, int top, int y, int bottom, Paint paint) { 772 float x, int top, int y, int bottom, Paint paint) {
1288 canvas.drawText(ELLIPSIS, x, y, paint); 773 canvas.drawText(ELLIPSIS, x, y, paint);
1289 } 774 }
1290 } 775 }
1291 } 776 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698