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