1 /* 2 * Copyright (C) 2011 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.android.browser.autocomplete; 17 18 import com.android.browser.BrowserSettings; 19 import com.android.browser.SuggestionsAdapter; 20 import com.android.browser.SuggestionsAdapter.SuggestItem; 21 import com.android.browser.autocomplete.SuggestedTextController.TextChangeWatcher; 22 import com.android.internal.R; 23 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.database.DataSetObserver; 27 import android.graphics.Rect; 28 import android.os.Parcelable; 29 import android.text.Editable; 30 import android.text.Html; 31 import android.text.Selection; 32 import android.text.TextUtils; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.view.AbsSavedState; 36 import android.view.KeyEvent; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.WindowManager; 41 import android.view.inputmethod.CompletionInfo; 42 import android.view.inputmethod.EditorInfo; 43 import android.view.inputmethod.InputMethodManager; 44 import android.widget.AdapterView; 45 import android.widget.EditText; 46 import android.widget.Filter; 47 import android.widget.Filterable; 48 import android.widget.ListAdapter; 49 import android.widget.ListPopupWindow; 50 import android.widget.TextView; 51 52 53 /** 54 * This is a stripped down version of the framework AutoCompleteTextView 55 * class with added support for displaying completions in-place. Note that 56 * this cannot be implemented as a subclass of the above without making 57 * substantial changes to it and its descendants. 58 * 59 * @see android.widget.AutoCompleteTextView 60 */ 61 public class SuggestiveAutoCompleteTextView extends EditText implements Filter.FilterListener { 62 private static final boolean DEBUG = false; 63 private static final String TAG = "SuggestiveAutoCompleteTextView"; 64 65 private CharSequence mHintText; 66 private TextView mHintView; 67 private int mHintResource; 68 69 private SuggestionsAdapter mAdapter; 70 private Filter mFilter; 71 private int mThreshold; 72 73 private ListPopupWindow mPopup; 74 private int mDropDownAnchorId; 75 76 private AdapterView.OnItemClickListener mItemClickListener; 77 78 private boolean mDropDownDismissedOnCompletion = true; 79 80 private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 81 82 // Set to true when text is set directly and no filtering shall be performed 83 private boolean mBlockCompletion; 84 85 // When set, an update in the underlying adapter will update the result list popup. 86 // Set to false when the list is hidden to prevent asynchronous updates to popup the list again. 87 private boolean mPopupCanBeUpdated = true; 88 89 private PassThroughClickListener mPassThroughClickListener; 90 private PopupDataSetObserver mObserver; 91 private SuggestedTextController mController; 92 SuggestiveAutoCompleteTextView(Context context)93 public SuggestiveAutoCompleteTextView(Context context) { 94 this(context, null); 95 } 96 SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs)97 public SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs) { 98 this(context, attrs, R.attr.autoCompleteTextViewStyle); 99 } 100 SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle)101 public SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { 102 super(context, attrs, defStyle); 103 104 // The completions are always shown in the same color as the hint 105 // text. 106 mController = new SuggestedTextController(this, getHintTextColors().getDefaultColor()); 107 mPopup = new ListPopupWindow(context, attrs, 108 R.attr.autoCompleteTextViewStyle); 109 mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); 110 mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW); 111 112 TypedArray a = context.obtainStyledAttributes( 113 attrs, R.styleable.AutoCompleteTextView, defStyle, 0); 114 115 mThreshold = a.getInt( 116 R.styleable.AutoCompleteTextView_completionThreshold, 2); 117 118 mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector)); 119 mPopup.setVerticalOffset((int) 120 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f)); 121 mPopup.setHorizontalOffset((int) 122 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f)); 123 124 // Get the anchor's id now, but the view won't be ready, so wait to actually get the 125 // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. 126 // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return 127 // this TextView, as a default anchoring point. 128 mDropDownAnchorId = a.getResourceId( 129 R.styleable.AutoCompleteTextView_dropDownAnchor, View.NO_ID); 130 131 // For dropdown width, the developer can specify a specific width, or MATCH_PARENT 132 // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). 133 mPopup.setWidth(a.getLayoutDimension( 134 R.styleable.AutoCompleteTextView_dropDownWidth, 135 ViewGroup.LayoutParams.WRAP_CONTENT)); 136 mPopup.setHeight(a.getLayoutDimension( 137 R.styleable.AutoCompleteTextView_dropDownHeight, 138 ViewGroup.LayoutParams.WRAP_CONTENT)); 139 140 mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, 141 R.layout.simple_dropdown_hint); 142 143 mPopup.setOnItemClickListener(new DropDownItemClickListener()); 144 setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint)); 145 146 // Always turn on the auto complete input type flag, since it 147 // makes no sense to use this widget without it. 148 int inputType = getInputType(); 149 if ((inputType&EditorInfo.TYPE_MASK_CLASS) 150 == EditorInfo.TYPE_CLASS_TEXT) { 151 inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE; 152 setRawInputType(inputType); 153 } 154 155 a.recycle(); 156 157 setFocusable(true); 158 159 mController.addUserTextChangeWatcher(new MyWatcher()); 160 161 mPassThroughClickListener = new PassThroughClickListener(); 162 super.setOnClickListener(mPassThroughClickListener); 163 } 164 165 @Override setOnClickListener(OnClickListener listener)166 public void setOnClickListener(OnClickListener listener) { 167 mPassThroughClickListener.mWrapped = listener; 168 } 169 170 /** 171 * Private hook into the on click event, dispatched from {@link PassThroughClickListener} 172 */ onClickImpl()173 private void onClickImpl() { 174 // If the dropdown is showing, bring the keyboard to the front 175 // when the user touches the text field. 176 if (isPopupShowing()) { 177 ensureImeVisible(true); 178 } 179 } 180 181 /** 182 * <p>Sets the optional hint text that is displayed at the bottom of the 183 * the matching list. This can be used as a cue to the user on how to 184 * best use the list, or to provide extra information.</p> 185 * 186 * @param hint the text to be displayed to the user 187 * 188 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 189 */ setCompletionHint(CharSequence hint)190 private void setCompletionHint(CharSequence hint) { 191 mHintText = hint; 192 if (hint != null) { 193 if (mHintView == null) { 194 final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate( 195 mHintResource, null).findViewById(R.id.text1); 196 hintView.setText(mHintText); 197 mHintView = hintView; 198 mPopup.setPromptView(hintView); 199 } else { 200 mHintView.setText(hint); 201 } 202 } else { 203 mPopup.setPromptView(null); 204 mHintView = null; 205 } 206 } 207 getDropDownWidth()208 protected int getDropDownWidth() { 209 return mPopup.getWidth(); 210 } 211 setDropDownWidth(int width)212 public void setDropDownWidth(int width) { 213 mPopup.setWidth(width); 214 } 215 setDropDownVerticalOffset(int offset)216 protected void setDropDownVerticalOffset(int offset) { 217 mPopup.setVerticalOffset(offset); 218 } 219 setDropDownHorizontalOffset(int offset)220 public void setDropDownHorizontalOffset(int offset) { 221 mPopup.setHorizontalOffset(offset); 222 } 223 getDropDownHorizontalOffset()224 protected int getDropDownHorizontalOffset() { 225 return mPopup.getHorizontalOffset(); 226 } 227 setThreshold(int threshold)228 public void setThreshold(int threshold) { 229 if (threshold <= 0) { 230 threshold = 1; 231 } 232 233 mThreshold = threshold; 234 } 235 setOnItemClickListener(AdapterView.OnItemClickListener l)236 protected void setOnItemClickListener(AdapterView.OnItemClickListener l) { 237 mItemClickListener = l; 238 } 239 setAdapter(SuggestionsAdapter adapter)240 public void setAdapter(SuggestionsAdapter adapter) { 241 if (mObserver == null) { 242 mObserver = new PopupDataSetObserver(); 243 } else if (mAdapter != null) { 244 mAdapter.unregisterDataSetObserver(mObserver); 245 } 246 mAdapter = adapter; 247 if (mAdapter != null) { 248 mFilter = mAdapter.getFilter(); 249 adapter.registerDataSetObserver(mObserver); 250 } else { 251 mFilter = null; 252 } 253 254 mPopup.setAdapter(mAdapter); 255 } 256 257 @Override onKeyPreIme(int keyCode, KeyEvent event)258 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 259 if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing() 260 && !mPopup.isDropDownAlwaysVisible()) { 261 // special case for the back key, we do not even try to send it 262 // to the drop down list but instead, consume it immediately 263 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 264 KeyEvent.DispatcherState state = getKeyDispatcherState(); 265 if (state != null) { 266 state.startTracking(event, this); 267 } 268 return true; 269 } else if (event.getAction() == KeyEvent.ACTION_UP) { 270 KeyEvent.DispatcherState state = getKeyDispatcherState(); 271 if (state != null) { 272 state.handleUpEvent(event); 273 } 274 if (event.isTracking() && !event.isCanceled()) { 275 dismissDropDown(); 276 return true; 277 } 278 } 279 } 280 return super.onKeyPreIme(keyCode, event); 281 } 282 283 @Override onKeyUp(int keyCode, KeyEvent event)284 public boolean onKeyUp(int keyCode, KeyEvent event) { 285 boolean consumed = mPopup.onKeyUp(keyCode, event); 286 if (consumed) { 287 switch (keyCode) { 288 // if the list accepts the key events and the key event 289 // was a click, the text view gets the selected item 290 // from the drop down as its content 291 case KeyEvent.KEYCODE_ENTER: 292 case KeyEvent.KEYCODE_DPAD_CENTER: 293 case KeyEvent.KEYCODE_TAB: 294 if (event.hasNoModifiers()) { 295 performCompletion(); 296 } 297 return true; 298 } 299 } 300 301 if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) { 302 performCompletion(); 303 return true; 304 } 305 306 return super.onKeyUp(keyCode, event); 307 } 308 309 @Override onKeyDown(int keyCode, KeyEvent event)310 public boolean onKeyDown(int keyCode, KeyEvent event) { 311 if (mPopup.onKeyDown(keyCode, event)) { 312 return true; 313 } 314 315 if (!isPopupShowing()) { 316 switch(keyCode) { 317 case KeyEvent.KEYCODE_DPAD_DOWN: 318 if (event.hasNoModifiers()) { 319 performValidation(); 320 } 321 } 322 } 323 324 if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) { 325 return true; 326 } 327 328 mLastKeyCode = keyCode; 329 boolean handled = super.onKeyDown(keyCode, event); 330 mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 331 332 if (handled && isPopupShowing()) { 333 clearListSelection(); 334 } 335 336 return handled; 337 } 338 339 /** 340 * Returns <code>true</code> if the amount of text in the field meets 341 * or exceeds the {@link #getThreshold} requirement. You can override 342 * this to impose a different standard for when filtering will be 343 * triggered. 344 */ enoughToFilter()345 private boolean enoughToFilter() { 346 if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getUserText().length() 347 + " threshold=" + mThreshold); 348 return getUserText().length() >= mThreshold; 349 } 350 351 /** 352 * This is used to watch for edits to the text view. Note that we call 353 * to methods on the auto complete text view class so that we can access 354 * private vars without going through thunks. 355 */ 356 private class MyWatcher implements TextChangeWatcher { 357 @Override onTextChanged(String newText)358 public void onTextChanged(String newText) { 359 doAfterTextChanged(); 360 } 361 } 362 363 /** 364 * @hide 365 */ setBlockCompletion(boolean block)366 protected void setBlockCompletion(boolean block) { 367 mBlockCompletion = block; 368 } 369 doAfterTextChanged()370 void doAfterTextChanged() { 371 if (DEBUG) Log.d(TAG, "doAfterTextChanged(" + getText() + ")"); 372 if (mBlockCompletion) return; 373 374 // the drop down is shown only when a minimum number of characters 375 // was typed in the text view 376 if (enoughToFilter()) { 377 if (mFilter != null) { 378 mPopupCanBeUpdated = true; 379 performFiltering(getUserText(), mLastKeyCode); 380 buildImeCompletions(); 381 } 382 } else { 383 // drop down is automatically dismissed when enough characters 384 // are deleted from the text view 385 if (!mPopup.isDropDownAlwaysVisible()) { 386 dismissDropDown(); 387 } 388 if (mFilter != null) { 389 performFiltering(null, mLastKeyCode); 390 } 391 } 392 } 393 394 /** 395 * <p>Indicates whether the popup menu is showing.</p> 396 * 397 * @return true if the popup menu is showing, false otherwise 398 */ isPopupShowing()399 public boolean isPopupShowing() { 400 return mPopup.isShowing(); 401 } 402 403 /** 404 * <p>Converts the selected item from the drop down list into a sequence 405 * of character that can be used in the edit box.</p> 406 * 407 * @param selectedItem the item selected by the user for completion 408 * 409 * @return a sequence of characters representing the selected suggestion 410 */ convertSelectionToString(Object selectedItem)411 protected CharSequence convertSelectionToString(Object selectedItem) { 412 return mFilter.convertResultToString(selectedItem); 413 } 414 415 /** 416 * <p>Clear the list selection. This may only be temporary, as user input will often bring 417 * it back. 418 */ clearListSelection()419 private void clearListSelection() { 420 mPopup.clearListSelection(); 421 } 422 423 /** 424 * <p>Starts filtering the content of the drop down list. The filtering 425 * pattern is the content of the edit box. Subclasses should override this 426 * method to filter with a different pattern, for instance a substring of 427 * <code>text</code>.</p> 428 * 429 * @param text the filtering pattern 430 * @param keyCode the last character inserted in the edit box; beware that 431 * this will be null when text is being added through a soft input method. 432 */ 433 @SuppressWarnings({ "UnusedDeclaration" }) performFiltering(CharSequence text, int keyCode)434 protected void performFiltering(CharSequence text, int keyCode) { 435 if (DEBUG) Log.d(TAG, "performFiltering(" + text + ")"); 436 437 mFilter.filter(text, this); 438 } 439 performForcedFiltering()440 protected void performForcedFiltering() { 441 boolean wasSuspended = false; 442 if (mController.isCursorHandlingSuspended()) { 443 mController.resumeCursorMovementHandlingAndApplyChanges(); 444 wasSuspended = true; 445 } 446 447 mFilter.filter(getUserText().toString(), this); 448 449 if (wasSuspended) { 450 mController.suspendCursorMovementHandling(); 451 } 452 } 453 454 /** 455 * <p>Performs the text completion by converting the selected item from 456 * the drop down list into a string, replacing the text box's content with 457 * this string and finally dismissing the drop down menu.</p> 458 */ performCompletion()459 private void performCompletion() { 460 performCompletion(null, -1, -1); 461 } 462 463 @Override onCommitCompletion(CompletionInfo completion)464 public void onCommitCompletion(CompletionInfo completion) { 465 if (isPopupShowing()) { 466 mBlockCompletion = true; 467 replaceText(completion.getText()); 468 mBlockCompletion = false; 469 470 mPopup.performItemClick(completion.getPosition()); 471 } 472 } 473 performCompletion(View selectedView, int position, long id)474 private void performCompletion(View selectedView, int position, long id) { 475 if (isPopupShowing()) { 476 Object selectedItem; 477 if (position < 0) { 478 selectedItem = mPopup.getSelectedItem(); 479 } else { 480 selectedItem = mAdapter.getItem(position); 481 } 482 if (selectedItem == null) { 483 Log.w(TAG, "performCompletion: no selected item"); 484 return; 485 } 486 487 mBlockCompletion = true; 488 replaceText(convertSelectionToString(selectedItem)); 489 mBlockCompletion = false; 490 491 if (mItemClickListener != null) { 492 final ListPopupWindow list = mPopup; 493 494 if (selectedView == null || position < 0) { 495 selectedView = list.getSelectedView(); 496 position = list.getSelectedItemPosition(); 497 id = list.getSelectedItemId(); 498 } 499 mItemClickListener.onItemClick(list.getListView(), selectedView, position, id); 500 } 501 } 502 503 if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) { 504 dismissDropDown(); 505 } 506 } 507 508 /** 509 * <p>Performs the text completion by replacing the current text by the 510 * selected item. Subclasses should override this method to avoid replacing 511 * the whole content of the edit box.</p> 512 * 513 * @param text the selected suggestion in the drop down list 514 */ replaceText(CharSequence text)515 protected void replaceText(CharSequence text) { 516 clearComposingText(); 517 518 setText(text); 519 // make sure we keep the caret at the end of the text view 520 Editable spannable = getText(); 521 Selection.setSelection(spannable, spannable.length()); 522 } 523 524 /** {@inheritDoc} */ 525 @Override onFilterComplete(int count)526 public void onFilterComplete(int count) { 527 updateDropDownForFilter(count); 528 } 529 updateDropDownForFilter(int count)530 private void updateDropDownForFilter(int count) { 531 // Not attached to window, don't update drop-down 532 if (getWindowVisibility() == View.GONE) return; 533 534 /* 535 * This checks enoughToFilter() again because filtering requests 536 * are asynchronous, so the result may come back after enough text 537 * has since been deleted to make it no longer appropriate 538 * to filter. 539 */ 540 541 final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible(); 542 if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter() && 543 getUserText().length() > 0) { 544 if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) { 545 showDropDown(); 546 } 547 } else if (!dropDownAlwaysVisible && isPopupShowing()) { 548 dismissDropDown(); 549 // When the filter text is changed, the first update from the adapter may show an empty 550 // count (when the query is being performed on the network). Future updates when some 551 // content has been retrieved should still be able to update the list. 552 mPopupCanBeUpdated = true; 553 } 554 } 555 556 @Override onWindowFocusChanged(boolean hasWindowFocus)557 public void onWindowFocusChanged(boolean hasWindowFocus) { 558 super.onWindowFocusChanged(hasWindowFocus); 559 if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) { 560 dismissDropDown(); 561 } 562 } 563 564 @Override onDisplayHint(int hint)565 protected void onDisplayHint(int hint) { 566 super.onDisplayHint(hint); 567 switch (hint) { 568 case INVISIBLE: 569 if (!mPopup.isDropDownAlwaysVisible()) { 570 dismissDropDown(); 571 } 572 break; 573 } 574 } 575 576 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)577 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 578 // TextView makes several cursor movements when gaining focus, and this interferes with 579 // the suggested vs user entered text. Tell the controller to temporarily ignore cursor 580 // movements while this is going on. 581 mController.suspendCursorMovementHandling(); 582 583 super.onFocusChanged(focused, direction, previouslyFocusedRect); 584 // Perform validation if the view is losing focus. 585 if (!focused) { 586 performValidation(); 587 } 588 if (!focused && !mPopup.isDropDownAlwaysVisible()) { 589 dismissDropDown(); 590 } 591 592 mController.resumeCursorMovementHandlingAndApplyChanges(); 593 } 594 595 @Override onAttachedToWindow()596 protected void onAttachedToWindow() { 597 super.onAttachedToWindow(); 598 } 599 600 @Override onDetachedFromWindow()601 protected void onDetachedFromWindow() { 602 dismissDropDown(); 603 super.onDetachedFromWindow(); 604 } 605 606 /** 607 * <p>Closes the drop down if present on screen.</p> 608 */ dismissDropDown()609 protected void dismissDropDown() { 610 InputMethodManager imm = InputMethodManager.peekInstance(); 611 if (imm != null) { 612 imm.displayCompletions(this, null); 613 } 614 mPopup.dismiss(); 615 mPopupCanBeUpdated = false; 616 } 617 618 @Override setFrame(final int l, int t, final int r, int b)619 protected boolean setFrame(final int l, int t, final int r, int b) { 620 boolean result = super.setFrame(l, t, r, b); 621 622 if (isPopupShowing()) { 623 showDropDown(); 624 } 625 626 return result; 627 } 628 629 /** 630 * Ensures that the drop down is not obscuring the IME. 631 * @param visible whether the ime should be in front. If false, the ime is pushed to 632 * the background. 633 * @hide internal used only here and SearchDialog 634 */ ensureImeVisible(boolean visible)635 private void ensureImeVisible(boolean visible) { 636 mPopup.setInputMethodMode(visible 637 ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 638 showDropDown(); 639 } 640 641 /** 642 * <p>Displays the drop down on screen.</p> 643 */ showDropDown()644 protected void showDropDown() { 645 if (mPopup.getAnchorView() == null) { 646 if (mDropDownAnchorId != View.NO_ID) { 647 mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId)); 648 } else { 649 mPopup.setAnchorView(this); 650 } 651 } 652 if (!isPopupShowing()) { 653 // Make sure the list does not obscure the IME when shown for the first time. 654 mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED); 655 } 656 mPopup.show(); 657 } 658 buildImeCompletions()659 private void buildImeCompletions() { 660 final ListAdapter adapter = mAdapter; 661 if (adapter != null) { 662 InputMethodManager imm = InputMethodManager.peekInstance(); 663 if (imm != null) { 664 final int count = Math.min(adapter.getCount(), 20); 665 CompletionInfo[] completions = new CompletionInfo[count]; 666 int realCount = 0; 667 668 for (int i = 0; i < count; i++) { 669 if (adapter.isEnabled(i)) { 670 realCount++; 671 Object item = adapter.getItem(i); 672 long id = adapter.getItemId(i); 673 completions[i] = new CompletionInfo(id, i, 674 convertSelectionToString(item)); 675 } 676 } 677 678 if (realCount != count) { 679 CompletionInfo[] tmp = new CompletionInfo[realCount]; 680 System.arraycopy(completions, 0, tmp, 0, realCount); 681 completions = tmp; 682 } 683 684 imm.displayCompletions(this, completions); 685 } 686 } 687 } 688 performValidation()689 private void performValidation() { 690 } 691 692 /** 693 * Returns the Filter obtained from {@link Filterable#getFilter}, 694 * or <code>null</code> if {@link #setAdapter} was not called with 695 * a Filterable. 696 */ getFilter()697 protected Filter getFilter() { 698 return mFilter; 699 } 700 701 private class DropDownItemClickListener implements AdapterView.OnItemClickListener { 702 @Override onItemClick(AdapterView parent, View v, int position, long id)703 public void onItemClick(AdapterView parent, View v, int position, long id) { 704 performCompletion(v, position, id); 705 } 706 } 707 708 /** 709 * Allows us a private hook into the on click event without preventing users from setting 710 * their own click listener. 711 */ 712 private class PassThroughClickListener implements OnClickListener { 713 714 private View.OnClickListener mWrapped; 715 716 /** {@inheritDoc} */ 717 @Override onClick(View v)718 public void onClick(View v) { 719 onClickImpl(); 720 721 if (mWrapped != null) mWrapped.onClick(v); 722 } 723 } 724 725 private class PopupDataSetObserver extends DataSetObserver { 726 @Override onChanged()727 public void onChanged() { 728 if (mAdapter != null) { 729 // If the popup is not showing already, showing it will cause 730 // the list of data set observers attached to the adapter to 731 // change. We can't do it from here, because we are in the middle 732 // of iterating through the list of observers. 733 post(new Runnable() { 734 @Override 735 public void run() { 736 final SuggestionsAdapter adapter = mAdapter; 737 if (adapter != null) { 738 // This will re-layout, thus resetting mDataChanged, so that the 739 // listView click listener stays responsive 740 updateDropDownForFilter(adapter.getCount()); 741 } 742 743 updateText(adapter); 744 } 745 }); 746 } 747 } 748 } 749 getUserText()750 public String getUserText() { 751 return mController.getUserText(); 752 } 753 updateText(SuggestionsAdapter adapter)754 private void updateText(SuggestionsAdapter adapter) { 755 if (!BrowserSettings.getInstance().useInstantSearch()) { 756 return; 757 } 758 759 if (!isPopupShowing()) { 760 setSuggestedText(null); 761 return; 762 } 763 764 if (mAdapter.getCount() > 0 && !TextUtils.isEmpty(getUserText())) { 765 for (int i = 0; i < mAdapter.getCount(); ++i) { 766 SuggestItem current = mAdapter.getItem(i); 767 if (current.type == SuggestionsAdapter.TYPE_SUGGEST) { 768 setSuggestedText(current.title); 769 break; 770 } 771 } 772 } 773 } 774 775 @Override setText(CharSequence text, BufferType type)776 public void setText(CharSequence text, BufferType type) { 777 Editable buffer = getEditableText(); 778 if (text == null) text = ""; 779 // if we already have a buffer, we must not replace it with a new one as this will break 780 // mController. Write the new text into the existing buffer instead. 781 if (buffer == null) { 782 super.setText(text, type); 783 } else { 784 buffer.replace(0, buffer.length(), text); 785 invalidate(); 786 } 787 } 788 setText(CharSequence text, boolean filter)789 public void setText(CharSequence text, boolean filter) { 790 if (filter) { 791 setText(text); 792 } else { 793 mBlockCompletion = true; 794 // If cursor movement handling was suspended (the view is 795 // not in focus), resume it and apply the pending change. 796 // Since we don't want to perform any filtering, this change 797 // is safe. 798 boolean wasSuspended = false; 799 if (mController.isCursorHandlingSuspended()) { 800 mController.resumeCursorMovementHandlingAndApplyChanges(); 801 wasSuspended = true; 802 } 803 804 setText(text); 805 806 if (wasSuspended) { 807 mController.suspendCursorMovementHandling(); 808 } 809 mBlockCompletion = false; 810 } 811 } 812 813 @Override onSaveInstanceState()814 public Parcelable onSaveInstanceState() { 815 Parcelable superState = super.onSaveInstanceState(); 816 if (superState instanceof TextView.SavedState) { 817 // get rid of TextView's saved state, we override it. 818 superState = ((TextView.SavedState) superState).getSuperState(); 819 } 820 if (superState == null) { 821 superState = AbsSavedState.EMPTY_STATE; 822 } 823 return mController.saveInstanceState(superState); 824 } 825 826 @Override onRestoreInstanceState(Parcelable state)827 public void onRestoreInstanceState(Parcelable state) { 828 super.onRestoreInstanceState(mController.restoreInstanceState(state)); 829 } 830 addQueryTextWatcher(final SuggestedTextController.TextChangeWatcher watcher)831 public void addQueryTextWatcher(final SuggestedTextController.TextChangeWatcher watcher) { 832 mController.addUserTextChangeWatcher(watcher); 833 } 834 setSuggestedText(String text)835 public void setSuggestedText(String text) { 836 if (!TextUtils.isEmpty(text)) { 837 String htmlStripped = Html.fromHtml(text).toString(); 838 mController.setSuggestedText(htmlStripped); 839 } else { 840 mController.setSuggestedText(null); 841 } 842 } 843 getPopupDrawableRect(Rect rect)844 public void getPopupDrawableRect(Rect rect) { 845 if (mPopup.getListView() != null) { 846 mPopup.getListView().getDrawingRect(rect); 847 } 848 } 849 } 850