mobile/android/base/toolbar/ToolbarEditText.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     2  * This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 package org.mozilla.gecko.toolbar;
     8 import org.mozilla.gecko.R;
     9 import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
    10 import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
    11 import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
    12 import org.mozilla.gecko.CustomEditText;
    13 import org.mozilla.gecko.CustomEditText.OnKeyPreImeListener;
    14 import org.mozilla.gecko.InputMethods;
    15 import org.mozilla.gecko.util.GamepadUtils;
    16 import org.mozilla.gecko.util.StringUtils;
    18 import android.content.Context;
    19 import android.graphics.Rect;
    20 import android.text.Editable;
    21 import android.text.InputType;
    22 import android.text.Spanned;
    23 import android.text.TextUtils;
    24 import android.text.TextWatcher;
    25 import android.util.AttributeSet;
    26 import android.util.Log;
    27 import android.view.KeyEvent;
    28 import android.view.View;
    29 import android.view.View.OnKeyListener;
    30 import android.view.inputmethod.EditorInfo;
    31 import android.view.inputmethod.InputMethodManager;
    33 /**
    34 * {@code ToolbarEditText} is the text entry used when the toolbar
    35 * is in edit state. It handles all the necessary input method machinery
    36 * as well as the tracking of different text types (empty, search, or url).
    37 * It's meant to be owned by {@code ToolbarEditLayout}.
    38 */
    39 public class ToolbarEditText extends CustomEditText
    40                              implements AutocompleteHandler {
    42     private static final String LOGTAG = "GeckoToolbarEditText";
    44     // Used to track the current type of content in the
    45     // text entry so that ToolbarEditLayout can update its
    46     // state accordingly.
    47     enum TextType {
    48         EMPTY,
    49         SEARCH_QUERY,
    50         URL
    51     }
    53     interface OnTextTypeChangeListener {
    54         public void onTextTypeChange(ToolbarEditText editText, TextType textType);
    55     }
    57     private final Context mContext;
    59     // Type of the URL bar go/search button
    60     private TextType mToolbarTextType;
    61     // Type of the keyboard go/search button (cannot be EMPTY)
    62     private TextType mKeyboardTextType;
    64     private OnCommitListener mCommitListener;
    65     private OnDismissListener mDismissListener;
    66     private OnFilterListener mFilterListener;
    67     private OnTextTypeChangeListener mTextTypeListener;
    69     // The previous autocomplete result returned to us
    70     private String mAutoCompleteResult = "";
    72     // The user typed part of the autocomplete result
    73     private String mAutoCompletePrefix = null;
    75     private boolean mDelayRestartInput;
    77     public ToolbarEditText(Context context, AttributeSet attrs) {
    78         super(context, attrs);
    79         mContext = context;
    81         mToolbarTextType = TextType.EMPTY;
    82         mKeyboardTextType = TextType.URL;
    83     }
    85     void setOnCommitListener(OnCommitListener listener) {
    86         mCommitListener = listener;
    87     }
    89     void setOnDismissListener(OnDismissListener listener) {
    90         mDismissListener = listener;
    91     }
    93     void setOnFilterListener(OnFilterListener listener) {
    94         mFilterListener = listener;
    95     }
    97     void setOnTextTypeChangeListener(OnTextTypeChangeListener listener) {
    98         mTextTypeListener = listener;
    99     }
   101     @Override
   102     public void onAttachedToWindow() {
   103         setOnKeyListener(new KeyListener());
   104         setOnKeyPreImeListener(new KeyPreImeListener());
   105         addTextChangedListener(new TextChangeListener());
   106     }
   108     @Override
   109     public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
   110         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
   112         if (gainFocus) {
   113             resetAutocompleteState();
   114             return;
   115         }
   117         InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
   118         try {
   119             imm.hideSoftInputFromWindow(getWindowToken(), 0);
   120         } catch (NullPointerException e) {
   121             Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
   122                           + " a NullPointerException? See bug 782096", e);
   123         }
   124     }
   126     // Return early if we're backspacing through the string, or
   127     // have no autocomplete results
   128     @Override
   129     public final void onAutocomplete(final String result) {
   130         if (!isEnabled()) {
   131             return;
   132         }
   134         final String text = getText().toString();
   136         if (result == null) {
   137             mAutoCompleteResult = "";
   138             return;
   139         }
   141         if (!result.startsWith(text) || text.equals(result)) {
   142             return;
   143         }
   145         mAutoCompleteResult = result;
   146         getText().append(result.substring(text.length()));
   147         setSelection(text.length(), result.length());
   148     }
   150     @Override
   151     public void setEnabled(boolean enabled) {
   152         super.setEnabled(enabled);
   153         updateTextTypeFromText(getText().toString());
   154     }
   156     private void resetAutocompleteState() {
   157         mAutoCompleteResult = "";
   158         mAutoCompletePrefix = null;
   159     }
   161     private void updateKeyboardInputType() {
   162         // If the user enters a space, then we know they are entering
   163         // search terms, not a URL. We can then switch to text mode so,
   164         //   1) the IME auto-inserts spaces between words
   165         //   2) the IME doesn't reset input keyboard to Latin keyboard.
   166         final String text = getText().toString();
   167         final int currentInputType = getInputType();
   169         final int newInputType = StringUtils.isSearchQuery(text, false)
   170                                  ? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode
   171                                  : (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode
   173         if (newInputType != currentInputType) {
   174             setRawInputType(newInputType);
   175         }
   176     }
   178     private static boolean hasCompositionString(Editable content) {
   179         Object[] spans = content.getSpans(0, content.length(), Object.class);
   181         if (spans != null) {
   182             for (Object span : spans) {
   183                 if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) {
   184                     // Found composition string.
   185                     return true;
   186                 }
   187             }
   188         }
   190         return false;
   191     }
   193     private void setTextType(TextType textType) {
   194         mToolbarTextType = textType;
   196         if (textType != TextType.EMPTY) {
   197             mKeyboardTextType = textType;
   198         }
   199         if (mTextTypeListener != null) {
   200             mTextTypeListener.onTextTypeChange(this, textType);
   201         }
   202     }
   204     private void updateTextTypeFromText(String text) {
   205         if (text.length() == 0) {
   206             setTextType(TextType.EMPTY);
   207             return;
   208         }
   210         if (InputMethods.shouldDisableUrlBarUpdate(mContext)) {
   211             // Set button type to match the previous keyboard type
   212             setTextType(mKeyboardTextType);
   213             return;
   214         }
   216         final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION;
   218         final int imeAction;
   219         if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) {
   220             imeAction = EditorInfo.IME_ACTION_SEARCH;
   221         } else {
   222             imeAction = EditorInfo.IME_ACTION_GO;
   223         }
   225         InputMethodManager imm = InputMethods.getInputMethodManager(mContext);
   226         if (imm == null) {
   227             return;
   228         }
   230         boolean restartInput = false;
   231         if (actionBits != imeAction) {
   232             int optionBits = getImeOptions() & ~EditorInfo.IME_MASK_ACTION;
   233             setImeOptions(optionBits | imeAction);
   235             mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) &&
   236                                  (InputMethods.shouldDelayUrlBarUpdate(mContext));
   238             if (!mDelayRestartInput) {
   239                 restartInput = true;
   240             }
   241         } else if (mDelayRestartInput) {
   242             // Only call delayed restartInput when actionBits == imeAction
   243             // so if there are two restarts in a row, the first restarts will
   244             // be discarded and the second restart will be properly delayed
   245             mDelayRestartInput = false;
   246             restartInput = true;
   247         }
   249         if (!restartInput) {
   250             // If the text content was previously empty, the toolbar text type
   251             // is empty as well. Since the keyboard text type cannot be empty,
   252             // the two text types are now inconsistent. Reset the toolbar text
   253             // type here to the keyboard text type to ensure consistency.
   254             setTextType(mKeyboardTextType);
   255             return;
   256         }
   257         updateKeyboardInputType();
   258         imm.restartInput(ToolbarEditText.this);
   260         setTextType(imeAction == EditorInfo.IME_ACTION_GO ?
   261                     TextType.URL : TextType.SEARCH_QUERY);
   262     }
   264     private class TextChangeListener implements TextWatcher {
   265         @Override
   266         public void afterTextChanged(final Editable s) {
   267             if (!isEnabled()) {
   268                 return;
   269             }
   271             final String text = s.toString();
   273             boolean useHandler = false;
   274             boolean reuseAutocomplete = false;
   276             if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) {
   277                 useHandler = true;
   279                 // If you're hitting backspace (the string is getting smaller
   280                 // or is unchanged), don't autocomplete.
   281                 if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) {
   282                     useHandler = false;
   283                 } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) {
   284                     // If this text already matches our autocomplete text, autocomplete likely
   285                     // won't change. Just reuse the old autocomplete value.
   286                     useHandler = false;
   287                     reuseAutocomplete = true;
   288                 }
   289             }
   291             // If this is the autocomplete text being set, don't run the filter.
   292             if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) {
   293                 if (mFilterListener != null) {
   294                     mFilterListener.onFilter(text, useHandler ? ToolbarEditText.this : null);
   295                 }
   297                 mAutoCompletePrefix = text;
   299                 if (reuseAutocomplete) {
   300                     onAutocomplete(mAutoCompleteResult);
   301                 }
   302             }
   304             // If the edit text has a composition string, don't call updateGoButton().
   305             // That method resets IME and composition state will be broken.
   306             if (!hasCompositionString(s) || InputMethods.isGestureKeyboard(mContext)) {
   307                 updateTextTypeFromText(text);
   308             }
   309         }
   311         @Override
   312         public void beforeTextChanged(CharSequence s, int start, int count,
   313                                       int after) {
   314             // do nothing
   315         }
   317         @Override
   318         public void onTextChanged(CharSequence s, int start, int before,
   319                                   int count) {
   320             // do nothing
   321         }
   322     }
   324     private class KeyPreImeListener implements OnKeyPreImeListener {
   325         @Override
   326         public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) {
   327             // We only want to process one event per tap
   328             if (event.getAction() != KeyEvent.ACTION_DOWN) {
   329                 return false;
   330             }
   332             if (keyCode == KeyEvent.KEYCODE_ENTER) {
   333                 // If the edit text has a composition string, don't submit the text yet.
   334                 // ENTER is needed to commit the composition string.
   335                 final Editable content = getText();
   336                 if (!hasCompositionString(content)) {
   337                     if (mCommitListener != null) {
   338                         mCommitListener.onCommit();
   339                     }
   341                     return true;
   342                 }
   343             }
   345             if (keyCode == KeyEvent.KEYCODE_BACK) {
   346                 // Drop the virtual keyboard.
   347                 clearFocus();
   348                 return true;
   349             }
   351             return false;
   352         }
   353     }
   355     private class KeyListener implements View.OnKeyListener {
   356         @Override
   357         public boolean onKey(View v, int keyCode, KeyEvent event) {
   358             if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) {
   359                 if (event.getAction() != KeyEvent.ACTION_DOWN) {
   360                     return true;
   361                 }
   363                 if (mCommitListener != null) {
   364                     mCommitListener.onCommit();
   365                 }
   367                 return true;
   368             } else if (GamepadUtils.isBackKey(event)) {
   369                 if (mDismissListener != null) {
   370                     mDismissListener.onDismiss();
   371                 }
   373                 return true;
   374             }
   376             return false;
   377         }
   378     }
   379 }

mercurial