Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 }