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

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

mercurial