1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/toolbar/ToolbarEditText.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,379 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.toolbar; 1.10 + 1.11 +import org.mozilla.gecko.R; 1.12 +import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; 1.13 +import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; 1.14 +import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; 1.15 +import org.mozilla.gecko.CustomEditText; 1.16 +import org.mozilla.gecko.CustomEditText.OnKeyPreImeListener; 1.17 +import org.mozilla.gecko.InputMethods; 1.18 +import org.mozilla.gecko.util.GamepadUtils; 1.19 +import org.mozilla.gecko.util.StringUtils; 1.20 + 1.21 +import android.content.Context; 1.22 +import android.graphics.Rect; 1.23 +import android.text.Editable; 1.24 +import android.text.InputType; 1.25 +import android.text.Spanned; 1.26 +import android.text.TextUtils; 1.27 +import android.text.TextWatcher; 1.28 +import android.util.AttributeSet; 1.29 +import android.util.Log; 1.30 +import android.view.KeyEvent; 1.31 +import android.view.View; 1.32 +import android.view.View.OnKeyListener; 1.33 +import android.view.inputmethod.EditorInfo; 1.34 +import android.view.inputmethod.InputMethodManager; 1.35 + 1.36 +/** 1.37 +* {@code ToolbarEditText} is the text entry used when the toolbar 1.38 +* is in edit state. It handles all the necessary input method machinery 1.39 +* as well as the tracking of different text types (empty, search, or url). 1.40 +* It's meant to be owned by {@code ToolbarEditLayout}. 1.41 +*/ 1.42 +public class ToolbarEditText extends CustomEditText 1.43 + implements AutocompleteHandler { 1.44 + 1.45 + private static final String LOGTAG = "GeckoToolbarEditText"; 1.46 + 1.47 + // Used to track the current type of content in the 1.48 + // text entry so that ToolbarEditLayout can update its 1.49 + // state accordingly. 1.50 + enum TextType { 1.51 + EMPTY, 1.52 + SEARCH_QUERY, 1.53 + URL 1.54 + } 1.55 + 1.56 + interface OnTextTypeChangeListener { 1.57 + public void onTextTypeChange(ToolbarEditText editText, TextType textType); 1.58 + } 1.59 + 1.60 + private final Context mContext; 1.61 + 1.62 + // Type of the URL bar go/search button 1.63 + private TextType mToolbarTextType; 1.64 + // Type of the keyboard go/search button (cannot be EMPTY) 1.65 + private TextType mKeyboardTextType; 1.66 + 1.67 + private OnCommitListener mCommitListener; 1.68 + private OnDismissListener mDismissListener; 1.69 + private OnFilterListener mFilterListener; 1.70 + private OnTextTypeChangeListener mTextTypeListener; 1.71 + 1.72 + // The previous autocomplete result returned to us 1.73 + private String mAutoCompleteResult = ""; 1.74 + 1.75 + // The user typed part of the autocomplete result 1.76 + private String mAutoCompletePrefix = null; 1.77 + 1.78 + private boolean mDelayRestartInput; 1.79 + 1.80 + public ToolbarEditText(Context context, AttributeSet attrs) { 1.81 + super(context, attrs); 1.82 + mContext = context; 1.83 + 1.84 + mToolbarTextType = TextType.EMPTY; 1.85 + mKeyboardTextType = TextType.URL; 1.86 + } 1.87 + 1.88 + void setOnCommitListener(OnCommitListener listener) { 1.89 + mCommitListener = listener; 1.90 + } 1.91 + 1.92 + void setOnDismissListener(OnDismissListener listener) { 1.93 + mDismissListener = listener; 1.94 + } 1.95 + 1.96 + void setOnFilterListener(OnFilterListener listener) { 1.97 + mFilterListener = listener; 1.98 + } 1.99 + 1.100 + void setOnTextTypeChangeListener(OnTextTypeChangeListener listener) { 1.101 + mTextTypeListener = listener; 1.102 + } 1.103 + 1.104 + @Override 1.105 + public void onAttachedToWindow() { 1.106 + setOnKeyListener(new KeyListener()); 1.107 + setOnKeyPreImeListener(new KeyPreImeListener()); 1.108 + addTextChangedListener(new TextChangeListener()); 1.109 + } 1.110 + 1.111 + @Override 1.112 + public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1.113 + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1.114 + 1.115 + if (gainFocus) { 1.116 + resetAutocompleteState(); 1.117 + return; 1.118 + } 1.119 + 1.120 + InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 1.121 + try { 1.122 + imm.hideSoftInputFromWindow(getWindowToken(), 0); 1.123 + } catch (NullPointerException e) { 1.124 + Log.e(LOGTAG, "InputMethodManagerService, why are you throwing" 1.125 + + " a NullPointerException? See bug 782096", e); 1.126 + } 1.127 + } 1.128 + 1.129 + // Return early if we're backspacing through the string, or 1.130 + // have no autocomplete results 1.131 + @Override 1.132 + public final void onAutocomplete(final String result) { 1.133 + if (!isEnabled()) { 1.134 + return; 1.135 + } 1.136 + 1.137 + final String text = getText().toString(); 1.138 + 1.139 + if (result == null) { 1.140 + mAutoCompleteResult = ""; 1.141 + return; 1.142 + } 1.143 + 1.144 + if (!result.startsWith(text) || text.equals(result)) { 1.145 + return; 1.146 + } 1.147 + 1.148 + mAutoCompleteResult = result; 1.149 + getText().append(result.substring(text.length())); 1.150 + setSelection(text.length(), result.length()); 1.151 + } 1.152 + 1.153 + @Override 1.154 + public void setEnabled(boolean enabled) { 1.155 + super.setEnabled(enabled); 1.156 + updateTextTypeFromText(getText().toString()); 1.157 + } 1.158 + 1.159 + private void resetAutocompleteState() { 1.160 + mAutoCompleteResult = ""; 1.161 + mAutoCompletePrefix = null; 1.162 + } 1.163 + 1.164 + private void updateKeyboardInputType() { 1.165 + // If the user enters a space, then we know they are entering 1.166 + // search terms, not a URL. We can then switch to text mode so, 1.167 + // 1) the IME auto-inserts spaces between words 1.168 + // 2) the IME doesn't reset input keyboard to Latin keyboard. 1.169 + final String text = getText().toString(); 1.170 + final int currentInputType = getInputType(); 1.171 + 1.172 + final int newInputType = StringUtils.isSearchQuery(text, false) 1.173 + ? (currentInputType & ~InputType.TYPE_TEXT_VARIATION_URI) // Text mode 1.174 + : (currentInputType | InputType.TYPE_TEXT_VARIATION_URI); // URL mode 1.175 + 1.176 + if (newInputType != currentInputType) { 1.177 + setRawInputType(newInputType); 1.178 + } 1.179 + } 1.180 + 1.181 + private static boolean hasCompositionString(Editable content) { 1.182 + Object[] spans = content.getSpans(0, content.length(), Object.class); 1.183 + 1.184 + if (spans != null) { 1.185 + for (Object span : spans) { 1.186 + if ((content.getSpanFlags(span) & Spanned.SPAN_COMPOSING) != 0) { 1.187 + // Found composition string. 1.188 + return true; 1.189 + } 1.190 + } 1.191 + } 1.192 + 1.193 + return false; 1.194 + } 1.195 + 1.196 + private void setTextType(TextType textType) { 1.197 + mToolbarTextType = textType; 1.198 + 1.199 + if (textType != TextType.EMPTY) { 1.200 + mKeyboardTextType = textType; 1.201 + } 1.202 + if (mTextTypeListener != null) { 1.203 + mTextTypeListener.onTextTypeChange(this, textType); 1.204 + } 1.205 + } 1.206 + 1.207 + private void updateTextTypeFromText(String text) { 1.208 + if (text.length() == 0) { 1.209 + setTextType(TextType.EMPTY); 1.210 + return; 1.211 + } 1.212 + 1.213 + if (InputMethods.shouldDisableUrlBarUpdate(mContext)) { 1.214 + // Set button type to match the previous keyboard type 1.215 + setTextType(mKeyboardTextType); 1.216 + return; 1.217 + } 1.218 + 1.219 + final int actionBits = getImeOptions() & EditorInfo.IME_MASK_ACTION; 1.220 + 1.221 + final int imeAction; 1.222 + if (StringUtils.isSearchQuery(text, actionBits == EditorInfo.IME_ACTION_SEARCH)) { 1.223 + imeAction = EditorInfo.IME_ACTION_SEARCH; 1.224 + } else { 1.225 + imeAction = EditorInfo.IME_ACTION_GO; 1.226 + } 1.227 + 1.228 + InputMethodManager imm = InputMethods.getInputMethodManager(mContext); 1.229 + if (imm == null) { 1.230 + return; 1.231 + } 1.232 + 1.233 + boolean restartInput = false; 1.234 + if (actionBits != imeAction) { 1.235 + int optionBits = getImeOptions() & ~EditorInfo.IME_MASK_ACTION; 1.236 + setImeOptions(optionBits | imeAction); 1.237 + 1.238 + mDelayRestartInput = (imeAction == EditorInfo.IME_ACTION_GO) && 1.239 + (InputMethods.shouldDelayUrlBarUpdate(mContext)); 1.240 + 1.241 + if (!mDelayRestartInput) { 1.242 + restartInput = true; 1.243 + } 1.244 + } else if (mDelayRestartInput) { 1.245 + // Only call delayed restartInput when actionBits == imeAction 1.246 + // so if there are two restarts in a row, the first restarts will 1.247 + // be discarded and the second restart will be properly delayed 1.248 + mDelayRestartInput = false; 1.249 + restartInput = true; 1.250 + } 1.251 + 1.252 + if (!restartInput) { 1.253 + // If the text content was previously empty, the toolbar text type 1.254 + // is empty as well. Since the keyboard text type cannot be empty, 1.255 + // the two text types are now inconsistent. Reset the toolbar text 1.256 + // type here to the keyboard text type to ensure consistency. 1.257 + setTextType(mKeyboardTextType); 1.258 + return; 1.259 + } 1.260 + updateKeyboardInputType(); 1.261 + imm.restartInput(ToolbarEditText.this); 1.262 + 1.263 + setTextType(imeAction == EditorInfo.IME_ACTION_GO ? 1.264 + TextType.URL : TextType.SEARCH_QUERY); 1.265 + } 1.266 + 1.267 + private class TextChangeListener implements TextWatcher { 1.268 + @Override 1.269 + public void afterTextChanged(final Editable s) { 1.270 + if (!isEnabled()) { 1.271 + return; 1.272 + } 1.273 + 1.274 + final String text = s.toString(); 1.275 + 1.276 + boolean useHandler = false; 1.277 + boolean reuseAutocomplete = false; 1.278 + 1.279 + if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) { 1.280 + useHandler = true; 1.281 + 1.282 + // If you're hitting backspace (the string is getting smaller 1.283 + // or is unchanged), don't autocomplete. 1.284 + if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) { 1.285 + useHandler = false; 1.286 + } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) { 1.287 + // If this text already matches our autocomplete text, autocomplete likely 1.288 + // won't change. Just reuse the old autocomplete value. 1.289 + useHandler = false; 1.290 + reuseAutocomplete = true; 1.291 + } 1.292 + } 1.293 + 1.294 + // If this is the autocomplete text being set, don't run the filter. 1.295 + if (TextUtils.isEmpty(mAutoCompleteResult) || !mAutoCompleteResult.equals(text)) { 1.296 + if (mFilterListener != null) { 1.297 + mFilterListener.onFilter(text, useHandler ? ToolbarEditText.this : null); 1.298 + } 1.299 + 1.300 + mAutoCompletePrefix = text; 1.301 + 1.302 + if (reuseAutocomplete) { 1.303 + onAutocomplete(mAutoCompleteResult); 1.304 + } 1.305 + } 1.306 + 1.307 + // If the edit text has a composition string, don't call updateGoButton(). 1.308 + // That method resets IME and composition state will be broken. 1.309 + if (!hasCompositionString(s) || InputMethods.isGestureKeyboard(mContext)) { 1.310 + updateTextTypeFromText(text); 1.311 + } 1.312 + } 1.313 + 1.314 + @Override 1.315 + public void beforeTextChanged(CharSequence s, int start, int count, 1.316 + int after) { 1.317 + // do nothing 1.318 + } 1.319 + 1.320 + @Override 1.321 + public void onTextChanged(CharSequence s, int start, int before, 1.322 + int count) { 1.323 + // do nothing 1.324 + } 1.325 + } 1.326 + 1.327 + private class KeyPreImeListener implements OnKeyPreImeListener { 1.328 + @Override 1.329 + public boolean onKeyPreIme(View v, int keyCode, KeyEvent event) { 1.330 + // We only want to process one event per tap 1.331 + if (event.getAction() != KeyEvent.ACTION_DOWN) { 1.332 + return false; 1.333 + } 1.334 + 1.335 + if (keyCode == KeyEvent.KEYCODE_ENTER) { 1.336 + // If the edit text has a composition string, don't submit the text yet. 1.337 + // ENTER is needed to commit the composition string. 1.338 + final Editable content = getText(); 1.339 + if (!hasCompositionString(content)) { 1.340 + if (mCommitListener != null) { 1.341 + mCommitListener.onCommit(); 1.342 + } 1.343 + 1.344 + return true; 1.345 + } 1.346 + } 1.347 + 1.348 + if (keyCode == KeyEvent.KEYCODE_BACK) { 1.349 + // Drop the virtual keyboard. 1.350 + clearFocus(); 1.351 + return true; 1.352 + } 1.353 + 1.354 + return false; 1.355 + } 1.356 + } 1.357 + 1.358 + private class KeyListener implements View.OnKeyListener { 1.359 + @Override 1.360 + public boolean onKey(View v, int keyCode, KeyEvent event) { 1.361 + if (keyCode == KeyEvent.KEYCODE_ENTER || GamepadUtils.isActionKey(event)) { 1.362 + if (event.getAction() != KeyEvent.ACTION_DOWN) { 1.363 + return true; 1.364 + } 1.365 + 1.366 + if (mCommitListener != null) { 1.367 + mCommitListener.onCommit(); 1.368 + } 1.369 + 1.370 + return true; 1.371 + } else if (GamepadUtils.isBackKey(event)) { 1.372 + if (mDismissListener != null) { 1.373 + mDismissListener.onDismiss(); 1.374 + } 1.375 + 1.376 + return true; 1.377 + } 1.378 + 1.379 + return false; 1.380 + } 1.381 + } 1.382 +}