1 /* 2 * Copyright (C) 2010 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 17 package com.android.browser; 18 19 import android.content.Context; 20 import android.content.res.Configuration; 21 import android.content.res.TypedArray; 22 import android.database.DataSetObserver; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.text.TextUtils; 26 import android.util.AttributeSet; 27 import android.util.Patterns; 28 import android.view.KeyEvent; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.inputmethod.EditorInfo; 32 import android.view.inputmethod.InputMethodManager; 33 import android.widget.AdapterView; 34 import android.widget.AdapterView.OnItemClickListener; 35 import android.widget.TextView; 36 import android.widget.TextView.OnEditorActionListener; 37 38 import com.android.browser.SuggestionsAdapter.CompletionListener; 39 import com.android.browser.SuggestionsAdapter.SuggestItem; 40 import com.android.browser.UI.DropdownChangeListener; 41 import com.android.browser.autocomplete.SuggestedTextController.TextChangeWatcher; 42 import com.android.browser.autocomplete.SuggestiveAutoCompleteTextView; 43 import com.android.browser.search.SearchEngine; 44 import com.android.browser.search.SearchEngineInfo; 45 import com.android.browser.search.SearchEngines; 46 import com.android.internal.R; 47 48 import java.util.List; 49 50 /** 51 * url/search input view 52 * handling suggestions 53 */ 54 public class UrlInputView extends SuggestiveAutoCompleteTextView 55 implements OnEditorActionListener, 56 CompletionListener, OnItemClickListener, TextChangeWatcher { 57 58 static final String TYPED = "browser-type"; 59 static final String SUGGESTED = "browser-suggest"; 60 static final String VOICE = "voice-search"; 61 62 static final int POST_DELAY = 100; 63 64 static interface StateListener { 65 static final int STATE_NORMAL = 0; 66 static final int STATE_HIGHLIGHTED = 1; 67 static final int STATE_EDITED = 2; 68 onStateChanged(int state)69 public void onStateChanged(int state); 70 } 71 72 private UrlInputListener mListener; 73 private InputMethodManager mInputManager; 74 private SuggestionsAdapter mAdapter; 75 private View mContainer; 76 private boolean mLandscape; 77 private boolean mIncognitoMode; 78 private boolean mNeedsUpdate; 79 private DropdownChangeListener mDropdownListener; 80 81 private int mState; 82 private StateListener mStateListener; 83 private Rect mPopupPadding; 84 UrlInputView(Context context, AttributeSet attrs, int defStyle)85 public UrlInputView(Context context, AttributeSet attrs, int defStyle) { 86 super(context, attrs, defStyle); 87 TypedArray a = context.obtainStyledAttributes( 88 attrs, com.android.internal.R.styleable.PopupWindow, 89 R.attr.autoCompleteTextViewStyle, 0); 90 91 Drawable popupbg = a.getDrawable(R.styleable.PopupWindow_popupBackground); 92 a.recycle(); 93 mPopupPadding = new Rect(); 94 popupbg.getPadding(mPopupPadding); 95 init(context); 96 } 97 UrlInputView(Context context, AttributeSet attrs)98 public UrlInputView(Context context, AttributeSet attrs) { 99 this(context, attrs, R.attr.autoCompleteTextViewStyle); 100 } 101 UrlInputView(Context context)102 public UrlInputView(Context context) { 103 this(context, null); 104 } 105 init(Context ctx)106 private void init(Context ctx) { 107 mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE); 108 setOnEditorActionListener(this); 109 mAdapter = new SuggestionsAdapter(ctx, this); 110 setAdapter(mAdapter); 111 setSelectAllOnFocus(true); 112 onConfigurationChanged(ctx.getResources().getConfiguration()); 113 setThreshold(1); 114 setOnItemClickListener(this); 115 mNeedsUpdate = false; 116 mDropdownListener = null; 117 addQueryTextWatcher(this); 118 119 mAdapter.registerDataSetObserver(new DataSetObserver() { 120 @Override 121 public void onChanged() { 122 if (!isPopupShowing()) { 123 return; 124 } 125 dispatchChange(); 126 } 127 128 @Override 129 public void onInvalidated() { 130 dispatchChange(); 131 } 132 }); 133 mState = StateListener.STATE_NORMAL; 134 } 135 onFocusChanged(boolean focused, int direction, Rect prevRect)136 protected void onFocusChanged(boolean focused, int direction, Rect prevRect) { 137 super.onFocusChanged(focused, direction, prevRect); 138 int state = -1; 139 if (focused) { 140 if (hasSelection()) { 141 state = StateListener.STATE_HIGHLIGHTED; 142 } else { 143 state = StateListener.STATE_EDITED; 144 } 145 } else { 146 // reset the selection state 147 state = StateListener.STATE_NORMAL; 148 } 149 final int s = state; 150 post(new Runnable() { 151 public void run() { 152 changeState(s); 153 } 154 }); 155 } 156 157 @Override onTouchEvent(MotionEvent evt)158 public boolean onTouchEvent(MotionEvent evt) { 159 boolean hasSelection = hasSelection(); 160 boolean res = super.onTouchEvent(evt); 161 if ((MotionEvent.ACTION_DOWN == evt.getActionMasked()) 162 && hasSelection) { 163 postDelayed(new Runnable() { 164 public void run() { 165 changeState(StateListener.STATE_EDITED); 166 }}, POST_DELAY); 167 } 168 return res; 169 } 170 171 /** 172 * check if focus change requires a title bar update 173 */ needsUpdate()174 boolean needsUpdate() { 175 return mNeedsUpdate; 176 } 177 178 /** 179 * clear the focus change needs title bar update flag 180 */ clearNeedsUpdate()181 void clearNeedsUpdate() { 182 mNeedsUpdate = false; 183 } 184 setController(UiController controller)185 void setController(UiController controller) { 186 UrlSelectionActionMode urlSelectionMode 187 = new UrlSelectionActionMode(controller); 188 setCustomSelectionActionModeCallback(urlSelectionMode); 189 } 190 setContainer(View container)191 void setContainer(View container) { 192 mContainer = container; 193 } 194 setUrlInputListener(UrlInputListener listener)195 public void setUrlInputListener(UrlInputListener listener) { 196 mListener = listener; 197 } 198 setStateListener(StateListener listener)199 public void setStateListener(StateListener listener) { 200 mStateListener = listener; 201 // update listener 202 changeState(mState); 203 } 204 changeState(int newState)205 private void changeState(int newState) { 206 mState = newState; 207 if (mStateListener != null) { 208 mStateListener.onStateChanged(mState); 209 } 210 } 211 getState()212 int getState() { 213 return mState; 214 } 215 setVoiceResults(List<String> voiceResults)216 void setVoiceResults(List<String> voiceResults) { 217 mAdapter.setVoiceResults(voiceResults); 218 } 219 220 @Override onConfigurationChanged(Configuration config)221 protected void onConfigurationChanged(Configuration config) { 222 super.onConfigurationChanged(config); 223 mLandscape = (config.orientation & 224 Configuration.ORIENTATION_LANDSCAPE) != 0; 225 mAdapter.setLandscapeMode(mLandscape); 226 if (isPopupShowing() && (getVisibility() == View.VISIBLE)) { 227 setupDropDown(); 228 performFiltering(getUserText(), 0); 229 } 230 } 231 232 @Override showDropDown()233 public void showDropDown() { 234 setupDropDown(); 235 super.showDropDown(); 236 } 237 238 @Override dismissDropDown()239 public void dismissDropDown() { 240 super.dismissDropDown(); 241 mAdapter.clearCache(); 242 } 243 setupDropDown()244 private void setupDropDown() { 245 int width = mContainer != null ? mContainer.getWidth() : getWidth(); 246 width += mPopupPadding.left + mPopupPadding.right; 247 if (width != getDropDownWidth()) { 248 setDropDownWidth(width); 249 } 250 int left = getLeft(); 251 left += mPopupPadding.left; 252 if (left != -getDropDownHorizontalOffset()) { 253 setDropDownHorizontalOffset(-left); 254 } 255 } 256 257 @Override onEditorAction(TextView v, int actionId, KeyEvent event)258 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 259 if (BrowserSettings.getInstance().useInstantSearch() && 260 (actionId == EditorInfo.IME_ACTION_NEXT)) { 261 // When instant is turned on AND the user chooses to complete 262 // using the tab key, then use the completion rather than the 263 // text that the user has typed. 264 finishInput(getText().toString(), null, TYPED); 265 } else { 266 finishInput(getUserText(), null, TYPED); 267 } 268 269 return true; 270 } 271 forceFilter()272 void forceFilter() { 273 performForcedFiltering(); 274 showDropDown(); 275 } 276 forceIme()277 void forceIme() { 278 mInputManager.focusIn(this); 279 mInputManager.showSoftInput(this, 0); 280 } 281 hideIME()282 void hideIME() { 283 mInputManager.hideSoftInputFromWindow(getWindowToken(), 0); 284 } 285 finishInput(String url, String extra, String source)286 private void finishInput(String url, String extra, String source) { 287 mNeedsUpdate = true; 288 dismissDropDown(); 289 mInputManager.hideSoftInputFromWindow(getWindowToken(), 0); 290 if (TextUtils.isEmpty(url)) { 291 mListener.onDismiss(); 292 } else { 293 if (mIncognitoMode && isSearch(url)) { 294 // To prevent logging, intercept this request 295 // TODO: This is a quick hack, refactor this 296 SearchEngine searchEngine = BrowserSettings.getInstance() 297 .getSearchEngine(); 298 if (searchEngine == null) return; 299 SearchEngineInfo engineInfo = SearchEngines 300 .getSearchEngineInfo(mContext, searchEngine.getName()); 301 if (engineInfo == null) return; 302 url = engineInfo.getSearchUriForQuery(url); 303 // mLister.onAction can take it from here without logging 304 } 305 mListener.onAction(url, extra, source); 306 } 307 } 308 isSearch(String inUrl)309 boolean isSearch(String inUrl) { 310 String url = UrlUtils.fixUrl(inUrl).trim(); 311 if (TextUtils.isEmpty(url)) return false; 312 313 if (Patterns.WEB_URL.matcher(url).matches() 314 || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) { 315 return false; 316 } 317 return true; 318 } 319 320 // Completion Listener 321 322 @Override onSearch(String search)323 public void onSearch(String search) { 324 mListener.onCopySuggestion(search); 325 } 326 327 @Override onSelect(String url, int type, String extra)328 public void onSelect(String url, int type, String extra) { 329 finishInput(url, extra, (type == SuggestionsAdapter.TYPE_VOICE_SEARCH) 330 ? VOICE : SUGGESTED); 331 } 332 333 @Override onItemClick( AdapterView<?> parent, View view, int position, long id)334 public void onItemClick( 335 AdapterView<?> parent, View view, int position, long id) { 336 SuggestItem item = mAdapter.getItem(position); 337 onSelect(SuggestionsAdapter.getSuggestionUrl(item), item.type, item.extra); 338 } 339 340 interface UrlInputListener { 341 onDismiss()342 public void onDismiss(); 343 onAction(String text, String extra, String source)344 public void onAction(String text, String extra, String source); 345 onCopySuggestion(String text)346 public void onCopySuggestion(String text); 347 348 } 349 setIncognitoMode(boolean incognito)350 public void setIncognitoMode(boolean incognito) { 351 mIncognitoMode = incognito; 352 mAdapter.setIncognitoMode(mIncognitoMode); 353 } 354 355 @Override onKeyDown(int keyCode, KeyEvent evt)356 public boolean onKeyDown(int keyCode, KeyEvent evt) { 357 if (keyCode == KeyEvent.KEYCODE_ESCAPE && !isInTouchMode()) { 358 finishInput(null, null, null); 359 return true; 360 } 361 return super.onKeyDown(keyCode, evt); 362 } 363 getAdapter()364 public SuggestionsAdapter getAdapter() { 365 return mAdapter; 366 } 367 dispatchChange()368 private void dispatchChange() { 369 final Rect popupRect = new Rect(); 370 getPopupDrawableRect(popupRect); 371 372 if (mDropdownListener != null) { 373 mDropdownListener.onNewDropdownDimensions(popupRect.height()); 374 } 375 } 376 registerDropdownChangeListener(DropdownChangeListener d)377 void registerDropdownChangeListener(DropdownChangeListener d) { 378 mDropdownListener = d; 379 } 380 381 /* 382 * no-op to prevent scrolling of webview when embedded titlebar 383 * gets edited 384 */ 385 @Override requestRectangleOnScreen(Rect rect, boolean immediate)386 public boolean requestRectangleOnScreen(Rect rect, boolean immediate) { 387 return false; 388 } 389 390 @Override onTextChanged(String newText)391 public void onTextChanged(String newText) { 392 if (StateListener.STATE_HIGHLIGHTED == mState) { 393 changeState(StateListener.STATE_EDITED); 394 } 395 } 396 397 } 398