mobile/android/base/home/BrowserSearch.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.home;
michael@0 7
michael@0 8 import org.mozilla.gecko.GeckoAppShell;
michael@0 9 import org.mozilla.gecko.GeckoEvent;
michael@0 10 import org.mozilla.gecko.PrefsHelper;
michael@0 11 import org.mozilla.gecko.R;
michael@0 12 import org.mozilla.gecko.Tab;
michael@0 13 import org.mozilla.gecko.Tabs;
michael@0 14 import org.mozilla.gecko.Telemetry;
michael@0 15 import org.mozilla.gecko.TelemetryContract;
michael@0 16 import org.mozilla.gecko.db.BrowserDB.URLColumns;
michael@0 17 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
michael@0 18 import org.mozilla.gecko.home.SearchEngine;
michael@0 19 import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader;
michael@0 20 import org.mozilla.gecko.mozglue.RobocopTarget;
michael@0 21 import org.mozilla.gecko.toolbar.AutocompleteHandler;
michael@0 22 import org.mozilla.gecko.util.GeckoEventListener;
michael@0 23 import org.mozilla.gecko.util.StringUtils;
michael@0 24 import org.mozilla.gecko.util.ThreadUtils;
michael@0 25
michael@0 26 import org.json.JSONArray;
michael@0 27 import org.json.JSONException;
michael@0 28 import org.json.JSONObject;
michael@0 29
michael@0 30 import android.app.Activity;
michael@0 31 import android.content.Context;
michael@0 32 import android.database.Cursor;
michael@0 33 import android.net.Uri;
michael@0 34 import android.os.Bundle;
michael@0 35 import android.support.v4.app.LoaderManager.LoaderCallbacks;
michael@0 36 import android.support.v4.content.AsyncTaskLoader;
michael@0 37 import android.support.v4.content.Loader;
michael@0 38 import android.text.TextUtils;
michael@0 39 import android.util.AttributeSet;
michael@0 40 import android.util.Log;
michael@0 41 import android.view.LayoutInflater;
michael@0 42 import android.view.MotionEvent;
michael@0 43 import android.view.View;
michael@0 44 import android.view.View.OnClickListener;
michael@0 45 import android.view.ViewGroup;
michael@0 46 import android.view.ViewStub;
michael@0 47 import android.view.WindowManager;
michael@0 48 import android.view.WindowManager.LayoutParams;
michael@0 49 import android.view.animation.AccelerateInterpolator;
michael@0 50 import android.view.animation.Animation;
michael@0 51 import android.view.animation.TranslateAnimation;
michael@0 52 import android.widget.AdapterView;
michael@0 53 import android.widget.LinearLayout;
michael@0 54 import android.widget.ListView;
michael@0 55 import android.widget.TextView;
michael@0 56
michael@0 57 import java.util.ArrayList;
michael@0 58 import java.util.EnumSet;
michael@0 59
michael@0 60 /**
michael@0 61 * Fragment that displays frecency search results in a ListView.
michael@0 62 */
michael@0 63 public class BrowserSearch extends HomeFragment
michael@0 64 implements GeckoEventListener {
michael@0 65 // Logging tag name
michael@0 66 private static final String LOGTAG = "GeckoBrowserSearch";
michael@0 67
michael@0 68 // Cursor loader ID for search query
michael@0 69 private static final int LOADER_ID_SEARCH = 0;
michael@0 70
michael@0 71 // AsyncTask loader ID for suggestion query
michael@0 72 private static final int LOADER_ID_SUGGESTION = 1;
michael@0 73
michael@0 74 // Timeout for the suggestion client to respond
michael@0 75 private static final int SUGGESTION_TIMEOUT = 3000;
michael@0 76
michael@0 77 // Maximum number of results returned by the suggestion client
michael@0 78 private static final int SUGGESTION_MAX = 3;
michael@0 79
michael@0 80 // The maximum number of rows deep in a search we'll dig
michael@0 81 // for an autocomplete result
michael@0 82 private static final int MAX_AUTOCOMPLETE_SEARCH = 20;
michael@0 83
michael@0 84 // Length of https:// + 1 required to make autocomplete
michael@0 85 // fill in the domain, for both http:// and https://
michael@0 86 private static final int HTTPS_PREFIX_LENGTH = 9;
michael@0 87
michael@0 88 // Duration for fade-in animation
michael@0 89 private static final int ANIMATION_DURATION = 250;
michael@0 90
michael@0 91 // Holds the current search term to use in the query
michael@0 92 private volatile String mSearchTerm;
michael@0 93
michael@0 94 // Adapter for the list of search results
michael@0 95 private SearchAdapter mAdapter;
michael@0 96
michael@0 97 // The view shown by the fragment
michael@0 98 private LinearLayout mView;
michael@0 99
michael@0 100 // The list showing search results
michael@0 101 private HomeListView mList;
michael@0 102
michael@0 103 // Client that performs search suggestion queries
michael@0 104 private volatile SuggestClient mSuggestClient;
michael@0 105
michael@0 106 // List of search engines from gecko
michael@0 107 private ArrayList<SearchEngine> mSearchEngines;
michael@0 108
michael@0 109 // Whether search suggestions are enabled or not
michael@0 110 private boolean mSuggestionsEnabled;
michael@0 111
michael@0 112 // Callbacks used for the search loader
michael@0 113 private CursorLoaderCallbacks mCursorLoaderCallbacks;
michael@0 114
michael@0 115 // Callbacks used for the search suggestion loader
michael@0 116 private SuggestionLoaderCallbacks mSuggestionLoaderCallbacks;
michael@0 117
michael@0 118 // Autocomplete handler used when filtering results
michael@0 119 private AutocompleteHandler mAutocompleteHandler;
michael@0 120
michael@0 121 // On URL open listener
michael@0 122 private OnUrlOpenListener mUrlOpenListener;
michael@0 123
michael@0 124 // On search listener
michael@0 125 private OnSearchListener mSearchListener;
michael@0 126
michael@0 127 // On edit suggestion listener
michael@0 128 private OnEditSuggestionListener mEditSuggestionListener;
michael@0 129
michael@0 130 // Whether the suggestions will fade in when shown
michael@0 131 private boolean mAnimateSuggestions;
michael@0 132
michael@0 133 // Opt-in prompt view for search suggestions
michael@0 134 private View mSuggestionsOptInPrompt;
michael@0 135
michael@0 136 public interface OnSearchListener {
michael@0 137 public void onSearch(SearchEngine engine, String text);
michael@0 138 }
michael@0 139
michael@0 140 public interface OnEditSuggestionListener {
michael@0 141 public void onEditSuggestion(String suggestion);
michael@0 142 }
michael@0 143
michael@0 144 public static BrowserSearch newInstance() {
michael@0 145 BrowserSearch browserSearch = new BrowserSearch();
michael@0 146
michael@0 147 final Bundle args = new Bundle();
michael@0 148 args.putBoolean(HomePager.CAN_LOAD_ARG, true);
michael@0 149 browserSearch.setArguments(args);
michael@0 150
michael@0 151 return browserSearch;
michael@0 152 }
michael@0 153
michael@0 154 public BrowserSearch() {
michael@0 155 mSearchTerm = "";
michael@0 156 }
michael@0 157
michael@0 158 @Override
michael@0 159 public void onAttach(Activity activity) {
michael@0 160 super.onAttach(activity);
michael@0 161
michael@0 162 try {
michael@0 163 mUrlOpenListener = (OnUrlOpenListener) activity;
michael@0 164 } catch (ClassCastException e) {
michael@0 165 throw new ClassCastException(activity.toString()
michael@0 166 + " must implement BrowserSearch.OnUrlOpenListener");
michael@0 167 }
michael@0 168
michael@0 169 try {
michael@0 170 mSearchListener = (OnSearchListener) activity;
michael@0 171 } catch (ClassCastException e) {
michael@0 172 throw new ClassCastException(activity.toString()
michael@0 173 + " must implement BrowserSearch.OnSearchListener");
michael@0 174 }
michael@0 175
michael@0 176 try {
michael@0 177 mEditSuggestionListener = (OnEditSuggestionListener) activity;
michael@0 178 } catch (ClassCastException e) {
michael@0 179 throw new ClassCastException(activity.toString()
michael@0 180 + " must implement BrowserSearch.OnEditSuggestionListener");
michael@0 181 }
michael@0 182 }
michael@0 183
michael@0 184 @Override
michael@0 185 public void onDetach() {
michael@0 186 super.onDetach();
michael@0 187
michael@0 188 mAutocompleteHandler = null;
michael@0 189 mUrlOpenListener = null;
michael@0 190 mSearchListener = null;
michael@0 191 mEditSuggestionListener = null;
michael@0 192 }
michael@0 193
michael@0 194 @Override
michael@0 195 public void onCreate(Bundle savedInstanceState) {
michael@0 196 super.onCreate(savedInstanceState);
michael@0 197
michael@0 198 mSearchEngines = new ArrayList<SearchEngine>();
michael@0 199 }
michael@0 200
michael@0 201 @Override
michael@0 202 public void onDestroy() {
michael@0 203 super.onDestroy();
michael@0 204
michael@0 205 mSearchEngines = null;
michael@0 206 }
michael@0 207
michael@0 208 @Override
michael@0 209 public void onStart() {
michael@0 210 super.onStart();
michael@0 211
michael@0 212 // Adjusting the window size when showing the keyboard results in the underlying
michael@0 213 // activity being painted when the keyboard is hidden (bug 933422). This can be
michael@0 214 // prevented by not resizing the window.
michael@0 215 getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
michael@0 216 }
michael@0 217
michael@0 218 @Override
michael@0 219 public void onStop() {
michael@0 220 super.onStop();
michael@0 221
michael@0 222 getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
michael@0 223 }
michael@0 224
michael@0 225 @Override
michael@0 226 public void onResume() {
michael@0 227 super.onResume();
michael@0 228
michael@0 229 Telemetry.startUISession(TelemetryContract.Session.FRECENCY);
michael@0 230 }
michael@0 231
michael@0 232 @Override
michael@0 233 public void onPause() {
michael@0 234 super.onPause();
michael@0 235
michael@0 236 Telemetry.stopUISession(TelemetryContract.Session.FRECENCY);
michael@0 237 }
michael@0 238
michael@0 239 @Override
michael@0 240 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
michael@0 241 // All list views are styled to look the same with a global activity theme.
michael@0 242 // If the style of the list changes, inflate it from an XML.
michael@0 243 mView = (LinearLayout) inflater.inflate(R.layout.browser_search, container, false);
michael@0 244 mList = (HomeListView) mView.findViewById(R.id.home_list_view);
michael@0 245
michael@0 246 return mView;
michael@0 247 }
michael@0 248
michael@0 249 @Override
michael@0 250 public void onDestroyView() {
michael@0 251 super.onDestroyView();
michael@0 252
michael@0 253 unregisterEventListener("SearchEngines:Data");
michael@0 254
michael@0 255 mList.setAdapter(null);
michael@0 256 mList = null;
michael@0 257
michael@0 258 mView = null;
michael@0 259 mSuggestionsOptInPrompt = null;
michael@0 260 mSuggestClient = null;
michael@0 261 }
michael@0 262
michael@0 263 @Override
michael@0 264 public void onViewCreated(View view, Bundle savedInstanceState) {
michael@0 265 super.onViewCreated(view, savedInstanceState);
michael@0 266 mList.setTag(HomePager.LIST_TAG_BROWSER_SEARCH);
michael@0 267
michael@0 268 mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
michael@0 269 @Override
michael@0 270 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
michael@0 271 // Perform the user-entered search if the user clicks on a search engine row.
michael@0 272 // This row will be disabled if suggestions (in addition to the user-entered term) are showing.
michael@0 273 if (view instanceof SearchEngineRow) {
michael@0 274 ((SearchEngineRow) view).performUserEnteredSearch();
michael@0 275 return;
michael@0 276 }
michael@0 277
michael@0 278 // Account for the search engine rows.
michael@0 279 position -= getSuggestEngineCount();
michael@0 280 final Cursor c = mAdapter.getCursor(position);
michael@0 281 final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
michael@0 282
michael@0 283 // The "urlbar" and "frecency" sessions can be open at the same time. Use the LIST_ITEM
michael@0 284 // method to set this LOAD_URL event apart from the case where the user commits what's in
michael@0 285 // the url bar.
michael@0 286 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM);
michael@0 287
michael@0 288 // This item is a TwoLinePageRow, so we allow switch-to-tab.
michael@0 289 mUrlOpenListener.onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
michael@0 290 }
michael@0 291 });
michael@0 292
michael@0 293 mList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
michael@0 294 @Override
michael@0 295 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
michael@0 296 // Don't do anything when the user long-clicks on a search engine row.
michael@0 297 if (view instanceof SearchEngineRow) {
michael@0 298 return true;
michael@0 299 }
michael@0 300
michael@0 301 // Account for the search engine rows.
michael@0 302 position -= getSuggestEngineCount();
michael@0 303 return mList.onItemLongClick(parent, view, position, id);
michael@0 304 }
michael@0 305 });
michael@0 306
michael@0 307 final ListSelectionListener listener = new ListSelectionListener();
michael@0 308 mList.setOnItemSelectedListener(listener);
michael@0 309 mList.setOnFocusChangeListener(listener);
michael@0 310
michael@0 311 mList.setOnKeyListener(new View.OnKeyListener() {
michael@0 312 @Override
michael@0 313 public boolean onKey(View v, int keyCode, android.view.KeyEvent event) {
michael@0 314 final View selected = mList.getSelectedView();
michael@0 315
michael@0 316 if (selected instanceof SearchEngineRow) {
michael@0 317 return selected.onKeyDown(keyCode, event);
michael@0 318 }
michael@0 319 return false;
michael@0 320 }
michael@0 321 });
michael@0 322
michael@0 323 registerForContextMenu(mList);
michael@0 324 registerEventListener("SearchEngines:Data");
michael@0 325
michael@0 326 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
michael@0 327 }
michael@0 328
michael@0 329 @Override
michael@0 330 public void onActivityCreated(Bundle savedInstanceState) {
michael@0 331 super.onActivityCreated(savedInstanceState);
michael@0 332
michael@0 333 // Intialize the search adapter
michael@0 334 mAdapter = new SearchAdapter(getActivity());
michael@0 335 mList.setAdapter(mAdapter);
michael@0 336
michael@0 337 // Only create an instance when we need it
michael@0 338 mSuggestionLoaderCallbacks = null;
michael@0 339
michael@0 340 // Create callbacks before the initial loader is started
michael@0 341 mCursorLoaderCallbacks = new CursorLoaderCallbacks();
michael@0 342 loadIfVisible();
michael@0 343 }
michael@0 344
michael@0 345 @Override
michael@0 346 public void handleMessage(String event, final JSONObject message) {
michael@0 347 if (event.equals("SearchEngines:Data")) {
michael@0 348 ThreadUtils.postToUiThread(new Runnable() {
michael@0 349 @Override
michael@0 350 public void run() {
michael@0 351 setSearchEngines(message);
michael@0 352 }
michael@0 353 });
michael@0 354 }
michael@0 355 }
michael@0 356
michael@0 357 @Override
michael@0 358 protected void load() {
michael@0 359 SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
michael@0 360 }
michael@0 361
michael@0 362 private void handleAutocomplete(String searchTerm, Cursor c) {
michael@0 363 if (c == null ||
michael@0 364 mAutocompleteHandler == null ||
michael@0 365 TextUtils.isEmpty(searchTerm)) {
michael@0 366 return;
michael@0 367 }
michael@0 368
michael@0 369 // Avoid searching the path if we don't have to. Currently just
michael@0 370 // decided by whether there is a '/' character in the string.
michael@0 371 final boolean searchPath = searchTerm.indexOf('/') > 0;
michael@0 372 final String autocompletion = findAutocompletion(searchTerm, c, searchPath);
michael@0 373
michael@0 374 if (autocompletion == null || mAutocompleteHandler == null) {
michael@0 375 return;
michael@0 376 }
michael@0 377
michael@0 378 // Prefetch auto-completed domain since it's a likely target
michael@0 379 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", "http://" + autocompletion));
michael@0 380
michael@0 381 mAutocompleteHandler.onAutocomplete(autocompletion);
michael@0 382 mAutocompleteHandler = null;
michael@0 383 }
michael@0 384
michael@0 385 /**
michael@0 386 * Returns the substring of a provided URI, starting at the given offset,
michael@0 387 * and extending up to the end of the path segment in which the provided
michael@0 388 * index is found.
michael@0 389 *
michael@0 390 * For example, given
michael@0 391 *
michael@0 392 * "www.reddit.com/r/boop/abcdef", 0, ?
michael@0 393 *
michael@0 394 * this method returns
michael@0 395 *
michael@0 396 * ?=2: "www.reddit.com/"
michael@0 397 * ?=17: "www.reddit.com/r/boop/"
michael@0 398 * ?=21: "www.reddit.com/r/boop/"
michael@0 399 * ?=22: "www.reddit.com/r/boop/abcdef"
michael@0 400 *
michael@0 401 */
michael@0 402 private static String uriSubstringUpToMatchedPath(final String url, final int offset, final int begin) {
michael@0 403 final int afterEnd = url.length();
michael@0 404
michael@0 405 // We want to include the trailing slash, but not other characters.
michael@0 406 int chop = url.indexOf('/', begin);
michael@0 407 if (chop != -1) {
michael@0 408 ++chop;
michael@0 409 if (chop < offset) {
michael@0 410 // This isn't supposed to happen. Fall back to returning the whole damn thing.
michael@0 411 return url;
michael@0 412 }
michael@0 413 } else {
michael@0 414 chop = url.indexOf('?', begin);
michael@0 415 if (chop == -1) {
michael@0 416 chop = url.indexOf('#', begin);
michael@0 417 }
michael@0 418 if (chop == -1) {
michael@0 419 chop = afterEnd;
michael@0 420 }
michael@0 421 }
michael@0 422
michael@0 423 return url.substring(offset, chop);
michael@0 424 }
michael@0 425
michael@0 426 private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) {
michael@0 427 if (!c.moveToFirst()) {
michael@0 428 return null;
michael@0 429 }
michael@0 430
michael@0 431 final int searchLength = searchTerm.length();
michael@0 432 final int urlIndex = c.getColumnIndexOrThrow(URLColumns.URL);
michael@0 433 int searchCount = 0;
michael@0 434
michael@0 435 do {
michael@0 436 final String url = c.getString(urlIndex);
michael@0 437
michael@0 438 if (searchCount == 0) {
michael@0 439 // Prefetch the first item in the list since it's weighted the highest
michael@0 440 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", url.toString()));
michael@0 441 }
michael@0 442
michael@0 443 // Does the completion match against the whole URL? This will match
michael@0 444 // about: pages, as well as user input including "http://...".
michael@0 445 if (url.startsWith(searchTerm)) {
michael@0 446 return uriSubstringUpToMatchedPath(url, 0,
michael@0 447 (searchLength > HTTPS_PREFIX_LENGTH) ? searchLength : HTTPS_PREFIX_LENGTH);
michael@0 448 }
michael@0 449
michael@0 450 final Uri uri = Uri.parse(url);
michael@0 451 final String host = uri.getHost();
michael@0 452
michael@0 453 // Host may be null for about pages.
michael@0 454 if (host == null) {
michael@0 455 continue;
michael@0 456 }
michael@0 457
michael@0 458 if (host.startsWith(searchTerm)) {
michael@0 459 return host + "/";
michael@0 460 }
michael@0 461
michael@0 462 final String strippedHost = StringUtils.stripCommonSubdomains(host);
michael@0 463 if (strippedHost.startsWith(searchTerm)) {
michael@0 464 return strippedHost + "/";
michael@0 465 }
michael@0 466
michael@0 467 ++searchCount;
michael@0 468
michael@0 469 if (!searchPath) {
michael@0 470 continue;
michael@0 471 }
michael@0 472
michael@0 473 // Otherwise, if we're matching paths, let's compare against the string itself.
michael@0 474 final int hostOffset = url.indexOf(strippedHost);
michael@0 475 if (hostOffset == -1) {
michael@0 476 // This was a URL string that parsed to a different host (normalized?).
michael@0 477 // Give up.
michael@0 478 continue;
michael@0 479 }
michael@0 480
michael@0 481 // We already matched the non-stripped host, so now we're
michael@0 482 // substring-searching in the part of the URL without the common
michael@0 483 // subdomains.
michael@0 484 if (url.startsWith(searchTerm, hostOffset)) {
michael@0 485 // Great! Return including the rest of the path segment.
michael@0 486 return uriSubstringUpToMatchedPath(url, hostOffset, hostOffset + searchLength);
michael@0 487 }
michael@0 488 } while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext());
michael@0 489
michael@0 490 return null;
michael@0 491 }
michael@0 492
michael@0 493 private void filterSuggestions() {
michael@0 494 if (mSuggestClient == null || !mSuggestionsEnabled) {
michael@0 495 return;
michael@0 496 }
michael@0 497
michael@0 498 if (mSuggestionLoaderCallbacks == null) {
michael@0 499 mSuggestionLoaderCallbacks = new SuggestionLoaderCallbacks();
michael@0 500 }
michael@0 501
michael@0 502 getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSuggestionLoaderCallbacks);
michael@0 503 }
michael@0 504
michael@0 505 private void setSuggestions(ArrayList<String> suggestions) {
michael@0 506 mSearchEngines.get(0).setSuggestions(suggestions);
michael@0 507 mAdapter.notifyDataSetChanged();
michael@0 508 }
michael@0 509
michael@0 510 private void setSearchEngines(JSONObject data) {
michael@0 511 // This method is called via a Runnable posted from the Gecko thread, so
michael@0 512 // it's possible the fragment and/or its view has been destroyed by the
michael@0 513 // time we get here. If so, just abort.
michael@0 514 if (mView == null) {
michael@0 515 return;
michael@0 516 }
michael@0 517
michael@0 518 try {
michael@0 519 final JSONObject suggest = data.getJSONObject("suggest");
michael@0 520 final String suggestEngine = suggest.optString("engine", null);
michael@0 521 final String suggestTemplate = suggest.optString("template", null);
michael@0 522 final boolean suggestionsPrompted = suggest.getBoolean("prompted");
michael@0 523 final JSONArray engines = data.getJSONArray("searchEngines");
michael@0 524
michael@0 525 mSuggestionsEnabled = suggest.getBoolean("enabled");
michael@0 526
michael@0 527 ArrayList<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
michael@0 528 for (int i = 0; i < engines.length(); i++) {
michael@0 529 final JSONObject engineJSON = engines.getJSONObject(i);
michael@0 530 final SearchEngine engine = new SearchEngine(engineJSON);
michael@0 531
michael@0 532 if (engine.name.equals(suggestEngine) && suggestTemplate != null) {
michael@0 533 // Suggest engine should be at the front of the list.
michael@0 534 searchEngines.add(0, engine);
michael@0 535
michael@0 536 // The only time Tabs.getInstance().getSelectedTab() should
michael@0 537 // be null is when we're restoring after a crash. We should
michael@0 538 // never restore private tabs when that happens, so it
michael@0 539 // should be safe to assume that null means non-private.
michael@0 540 Tab tab = Tabs.getInstance().getSelectedTab();
michael@0 541 final boolean isPrivate = (tab != null && tab.isPrivate());
michael@0 542
michael@0 543 // Only create a new instance of SuggestClient if it hasn't been
michael@0 544 // set yet. e.g. Robocop tests might set it directly before search
michael@0 545 // engines are loaded.
michael@0 546 if (mSuggestClient == null && !isPrivate) {
michael@0 547 setSuggestClient(new SuggestClient(getActivity(), suggestTemplate,
michael@0 548 SUGGESTION_TIMEOUT, SUGGESTION_MAX));
michael@0 549 }
michael@0 550 } else {
michael@0 551 searchEngines.add(engine);
michael@0 552 }
michael@0 553 }
michael@0 554
michael@0 555 mSearchEngines = searchEngines;
michael@0 556
michael@0 557 if (mAdapter != null) {
michael@0 558 mAdapter.notifyDataSetChanged();
michael@0 559 }
michael@0 560
michael@0 561 // Show suggestions opt-in prompt only if suggestions are not enabled yet,
michael@0 562 // user hasn't been prompted and we're not on a private browsing tab.
michael@0 563 if (!mSuggestionsEnabled && !suggestionsPrompted && mSuggestClient != null) {
michael@0 564 showSuggestionsOptIn();
michael@0 565 }
michael@0 566 } catch (JSONException e) {
michael@0 567 Log.e(LOGTAG, "Error getting search engine JSON", e);
michael@0 568 }
michael@0 569
michael@0 570 filterSuggestions();
michael@0 571 }
michael@0 572
michael@0 573 /**
michael@0 574 * Sets the private SuggestClient instance. Should only be called if the suggestClient is
michael@0 575 * null (i.e. has not yet been initialized or has been nulled). Non-private access is
michael@0 576 * for testing purposes only.
michael@0 577 */
michael@0 578 @RobocopTarget
michael@0 579 public void setSuggestClient(final SuggestClient client) {
michael@0 580 if (mSuggestClient != null) {
michael@0 581 throw new IllegalStateException("Can only set the SuggestClient if it has not " +
michael@0 582 "yet been initialized!");
michael@0 583 }
michael@0 584 mSuggestClient = client;
michael@0 585 }
michael@0 586
michael@0 587 private void showSuggestionsOptIn() {
michael@0 588 // Return if the ViewStub was already inflated - an inflated ViewStub is removed from the
michael@0 589 // View hierarchy so a second call to findViewById will return null.
michael@0 590 if (mSuggestionsOptInPrompt != null) {
michael@0 591 return;
michael@0 592 }
michael@0 593
michael@0 594 mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate();
michael@0 595
michael@0 596 TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title);
michael@0 597 promptText.setText(getResources().getString(R.string.suggestions_prompt));
michael@0 598
michael@0 599 final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes);
michael@0 600 final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no);
michael@0 601
michael@0 602 final OnClickListener listener = new OnClickListener() {
michael@0 603 @Override
michael@0 604 public void onClick(View v) {
michael@0 605 // Prevent the buttons from being clicked multiple times (bug 816902)
michael@0 606 yesButton.setOnClickListener(null);
michael@0 607 noButton.setOnClickListener(null);
michael@0 608
michael@0 609 setSuggestionsEnabled(v == yesButton);
michael@0 610 }
michael@0 611 };
michael@0 612
michael@0 613 yesButton.setOnClickListener(listener);
michael@0 614 noButton.setOnClickListener(listener);
michael@0 615
michael@0 616 // If the prompt gains focus, automatically pass focus to the
michael@0 617 // yes button in the prompt.
michael@0 618 final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt);
michael@0 619 prompt.setOnFocusChangeListener(new View.OnFocusChangeListener() {
michael@0 620 @Override
michael@0 621 public void onFocusChange(View v, boolean hasFocus) {
michael@0 622 if (hasFocus) {
michael@0 623 yesButton.requestFocus();
michael@0 624 }
michael@0 625 }
michael@0 626 });
michael@0 627 }
michael@0 628
michael@0 629 private void setSuggestionsEnabled(final boolean enabled) {
michael@0 630 // Clicking the yes/no buttons quickly can cause the click events be
michael@0 631 // queued before the listeners are removed above, so it's possible
michael@0 632 // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt
michael@0 633 // can be null if this happens (bug 828480).
michael@0 634 if (mSuggestionsOptInPrompt == null) {
michael@0 635 return;
michael@0 636 }
michael@0 637
michael@0 638 // Make suggestions appear immediately after the user opts in
michael@0 639 ThreadUtils.postToBackgroundThread(new Runnable() {
michael@0 640 @Override
michael@0 641 public void run() {
michael@0 642 SuggestClient client = mSuggestClient;
michael@0 643 if (client != null) {
michael@0 644 client.query(mSearchTerm);
michael@0 645 }
michael@0 646 }
michael@0 647 });
michael@0 648
michael@0 649 // Pref observer in gecko will also set prompted = true
michael@0 650 PrefsHelper.setPref("browser.search.suggest.enabled", enabled);
michael@0 651
michael@0 652 TranslateAnimation slideAnimation = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0);
michael@0 653 slideAnimation.setDuration(ANIMATION_DURATION);
michael@0 654 slideAnimation.setInterpolator(new AccelerateInterpolator());
michael@0 655 slideAnimation.setFillAfter(true);
michael@0 656 final View prompt = mSuggestionsOptInPrompt.findViewById(R.id.prompt);
michael@0 657
michael@0 658 TranslateAnimation shrinkAnimation = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight());
michael@0 659 shrinkAnimation.setDuration(ANIMATION_DURATION);
michael@0 660 shrinkAnimation.setFillAfter(true);
michael@0 661 shrinkAnimation.setStartOffset(slideAnimation.getDuration());
michael@0 662 shrinkAnimation.setAnimationListener(new Animation.AnimationListener() {
michael@0 663 @Override
michael@0 664 public void onAnimationStart(Animation a) {
michael@0 665 // Increase the height of the view so a gap isn't shown during animation
michael@0 666 mView.getLayoutParams().height = mView.getHeight() +
michael@0 667 mSuggestionsOptInPrompt.getHeight();
michael@0 668 mView.requestLayout();
michael@0 669 }
michael@0 670
michael@0 671 @Override
michael@0 672 public void onAnimationRepeat(Animation a) {}
michael@0 673
michael@0 674 @Override
michael@0 675 public void onAnimationEnd(Animation a) {
michael@0 676 // Removing the view immediately results in a NPE in
michael@0 677 // dispatchDraw(), possibly because this callback executes
michael@0 678 // before drawing is finished. Posting this as a Runnable fixes
michael@0 679 // the issue.
michael@0 680 mView.post(new Runnable() {
michael@0 681 @Override
michael@0 682 public void run() {
michael@0 683 mView.removeView(mSuggestionsOptInPrompt);
michael@0 684 mList.clearAnimation();
michael@0 685 mSuggestionsOptInPrompt = null;
michael@0 686
michael@0 687 if (enabled) {
michael@0 688 // Reset the view height
michael@0 689 mView.getLayoutParams().height = LayoutParams.MATCH_PARENT;
michael@0 690
michael@0 691 mSuggestionsEnabled = enabled;
michael@0 692 mAnimateSuggestions = true;
michael@0 693 mAdapter.notifyDataSetChanged();
michael@0 694 filterSuggestions();
michael@0 695 }
michael@0 696 }
michael@0 697 });
michael@0 698 }
michael@0 699 });
michael@0 700
michael@0 701 prompt.startAnimation(slideAnimation);
michael@0 702 mSuggestionsOptInPrompt.startAnimation(shrinkAnimation);
michael@0 703 mList.startAnimation(shrinkAnimation);
michael@0 704 }
michael@0 705
michael@0 706 private int getSuggestEngineCount() {
michael@0 707 return (TextUtils.isEmpty(mSearchTerm) || mSuggestClient == null || !mSuggestionsEnabled) ? 0 : 1;
michael@0 708 }
michael@0 709
michael@0 710 private void registerEventListener(String eventName) {
michael@0 711 GeckoAppShell.registerEventListener(eventName, this);
michael@0 712 }
michael@0 713
michael@0 714 private void unregisterEventListener(String eventName) {
michael@0 715 GeckoAppShell.unregisterEventListener(eventName, this);
michael@0 716 }
michael@0 717
michael@0 718 private void restartSearchLoader() {
michael@0 719 SearchLoader.restart(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
michael@0 720 }
michael@0 721
michael@0 722 private void initSearchLoader() {
michael@0 723 SearchLoader.init(getLoaderManager(), LOADER_ID_SEARCH, mCursorLoaderCallbacks, mSearchTerm);
michael@0 724 }
michael@0 725
michael@0 726 public void filter(String searchTerm, AutocompleteHandler handler) {
michael@0 727 if (TextUtils.isEmpty(searchTerm)) {
michael@0 728 return;
michael@0 729 }
michael@0 730
michael@0 731 final boolean isNewFilter = !TextUtils.equals(mSearchTerm, searchTerm);
michael@0 732
michael@0 733 mSearchTerm = searchTerm;
michael@0 734 mAutocompleteHandler = handler;
michael@0 735
michael@0 736 if (isVisible()) {
michael@0 737 if (isNewFilter) {
michael@0 738 // The adapter depends on the search term to determine its number
michael@0 739 // of items. Make it we notify the view about it.
michael@0 740 mAdapter.notifyDataSetChanged();
michael@0 741
michael@0 742 // Restart loaders with the new search term
michael@0 743 restartSearchLoader();
michael@0 744 filterSuggestions();
michael@0 745 } else {
michael@0 746 // The search term hasn't changed, simply reuse any existing
michael@0 747 // loader for the current search term. This will ensure autocompletion
michael@0 748 // is consistently triggered (see bug 933739).
michael@0 749 initSearchLoader();
michael@0 750 }
michael@0 751 }
michael@0 752 }
michael@0 753
michael@0 754 private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> {
michael@0 755 private final SuggestClient mSuggestClient;
michael@0 756 private final String mSearchTerm;
michael@0 757 private ArrayList<String> mSuggestions;
michael@0 758
michael@0 759 public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
michael@0 760 super(context);
michael@0 761 mSuggestClient = suggestClient;
michael@0 762 mSearchTerm = searchTerm;
michael@0 763 mSuggestions = null;
michael@0 764 }
michael@0 765
michael@0 766 @Override
michael@0 767 public ArrayList<String> loadInBackground() {
michael@0 768 return mSuggestClient.query(mSearchTerm);
michael@0 769 }
michael@0 770
michael@0 771 @Override
michael@0 772 public void deliverResult(ArrayList<String> suggestions) {
michael@0 773 mSuggestions = suggestions;
michael@0 774
michael@0 775 if (isStarted()) {
michael@0 776 super.deliverResult(mSuggestions);
michael@0 777 }
michael@0 778 }
michael@0 779
michael@0 780 @Override
michael@0 781 protected void onStartLoading() {
michael@0 782 if (mSuggestions != null) {
michael@0 783 deliverResult(mSuggestions);
michael@0 784 }
michael@0 785
michael@0 786 if (takeContentChanged() || mSuggestions == null) {
michael@0 787 forceLoad();
michael@0 788 }
michael@0 789 }
michael@0 790
michael@0 791 @Override
michael@0 792 protected void onStopLoading() {
michael@0 793 cancelLoad();
michael@0 794 }
michael@0 795
michael@0 796 @Override
michael@0 797 protected void onReset() {
michael@0 798 super.onReset();
michael@0 799
michael@0 800 onStopLoading();
michael@0 801 mSuggestions = null;
michael@0 802 }
michael@0 803 }
michael@0 804
michael@0 805 private class SearchAdapter extends MultiTypeCursorAdapter {
michael@0 806 private static final int ROW_SEARCH = 0;
michael@0 807 private static final int ROW_STANDARD = 1;
michael@0 808 private static final int ROW_SUGGEST = 2;
michael@0 809
michael@0 810 public SearchAdapter(Context context) {
michael@0 811 super(context, null, new int[] { ROW_STANDARD,
michael@0 812 ROW_SEARCH,
michael@0 813 ROW_SUGGEST },
michael@0 814 new int[] { R.layout.home_item_row,
michael@0 815 R.layout.home_search_item_row,
michael@0 816 R.layout.home_search_item_row });
michael@0 817 }
michael@0 818
michael@0 819 @Override
michael@0 820 public int getItemViewType(int position) {
michael@0 821 final int engine = getEngineIndex(position);
michael@0 822
michael@0 823 if (engine == -1) {
michael@0 824 return ROW_STANDARD;
michael@0 825 } else if (engine == 0 && mSuggestionsEnabled) {
michael@0 826 // Give suggestion views their own type to prevent them from
michael@0 827 // sharing other recycled search engine views. Using other
michael@0 828 // recycled views for the suggestion row can break animations
michael@0 829 // (bug 815937).
michael@0 830 return ROW_SUGGEST;
michael@0 831 }
michael@0 832
michael@0 833 return ROW_SEARCH;
michael@0 834 }
michael@0 835
michael@0 836 @Override
michael@0 837 public boolean isEnabled(int position) {
michael@0 838 // If we're using a gamepad or keyboard, allow the row to be
michael@0 839 // focused so it can pass the focus to its child suggestion views.
michael@0 840 if (!mList.isInTouchMode()) {
michael@0 841 return true;
michael@0 842 }
michael@0 843
michael@0 844 // If the suggestion row only contains one item (the user-entered
michael@0 845 // query), allow the entire row to be clickable; clicking the row
michael@0 846 // has the same effect as clicking the single suggestion. If the
michael@0 847 // row contains multiple items, clicking the row will do nothing.
michael@0 848 final int index = getEngineIndex(position);
michael@0 849 if (index != -1) {
michael@0 850 return !mSearchEngines.get(index).hasSuggestions();
michael@0 851 }
michael@0 852
michael@0 853 return true;
michael@0 854 }
michael@0 855
michael@0 856 // Add the search engines to the number of reported results.
michael@0 857 @Override
michael@0 858 public int getCount() {
michael@0 859 final int resultCount = super.getCount();
michael@0 860
michael@0 861 // Don't show search engines or suggestions if search field is empty
michael@0 862 if (TextUtils.isEmpty(mSearchTerm)) {
michael@0 863 return resultCount;
michael@0 864 }
michael@0 865
michael@0 866 return resultCount + mSearchEngines.size();
michael@0 867 }
michael@0 868
michael@0 869 @Override
michael@0 870 public void bindView(View view, Context context, int position) {
michael@0 871 final int type = getItemViewType(position);
michael@0 872
michael@0 873 if (type == ROW_SEARCH || type == ROW_SUGGEST) {
michael@0 874 final SearchEngineRow row = (SearchEngineRow) view;
michael@0 875 row.setOnUrlOpenListener(mUrlOpenListener);
michael@0 876 row.setOnSearchListener(mSearchListener);
michael@0 877 row.setOnEditSuggestionListener(mEditSuggestionListener);
michael@0 878 row.setSearchTerm(mSearchTerm);
michael@0 879
michael@0 880 final SearchEngine engine = mSearchEngines.get(getEngineIndex(position));
michael@0 881 final boolean animate = (mAnimateSuggestions && engine.hasSuggestions());
michael@0 882 row.updateFromSearchEngine(engine, animate);
michael@0 883 if (animate) {
michael@0 884 // Only animate suggestions the first time they are shown
michael@0 885 mAnimateSuggestions = false;
michael@0 886 }
michael@0 887 } else {
michael@0 888 // Account for the search engines
michael@0 889 position -= getSuggestEngineCount();
michael@0 890
michael@0 891 final Cursor c = getCursor(position);
michael@0 892 final TwoLinePageRow row = (TwoLinePageRow) view;
michael@0 893 row.updateFromCursor(c);
michael@0 894 }
michael@0 895 }
michael@0 896
michael@0 897 private int getEngineIndex(int position) {
michael@0 898 final int resultCount = super.getCount();
michael@0 899 final int suggestEngineCount = getSuggestEngineCount();
michael@0 900
michael@0 901 // Return suggest engine index
michael@0 902 if (position < suggestEngineCount) {
michael@0 903 return position;
michael@0 904 }
michael@0 905
michael@0 906 // Not an engine
michael@0 907 if (position - suggestEngineCount < resultCount) {
michael@0 908 return -1;
michael@0 909 }
michael@0 910
michael@0 911 // Return search engine index
michael@0 912 return position - resultCount;
michael@0 913 }
michael@0 914 }
michael@0 915
michael@0 916 private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
michael@0 917 @Override
michael@0 918 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
michael@0 919 return SearchLoader.createInstance(getActivity(), args);
michael@0 920 }
michael@0 921
michael@0 922 @Override
michael@0 923 public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
michael@0 924 mAdapter.swapCursor(c);
michael@0 925
michael@0 926 // We should handle autocompletion based on the search term
michael@0 927 // associated with the loader that has just provided
michael@0 928 // the results.
michael@0 929 SearchCursorLoader searchLoader = (SearchCursorLoader) loader;
michael@0 930 handleAutocomplete(searchLoader.getSearchTerm(), c);
michael@0 931 }
michael@0 932
michael@0 933 @Override
michael@0 934 public void onLoaderReset(Loader<Cursor> loader) {
michael@0 935 mAdapter.swapCursor(null);
michael@0 936 }
michael@0 937 }
michael@0 938
michael@0 939 private class SuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
michael@0 940 @Override
michael@0 941 public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) {
michael@0 942 // mSuggestClient is set to null in onDestroyView(), so using it
michael@0 943 // safely here relies on the fact that onCreateLoader() is called
michael@0 944 // synchronously in restartLoader().
michael@0 945 return new SuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm);
michael@0 946 }
michael@0 947
michael@0 948 @Override
michael@0 949 public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) {
michael@0 950 setSuggestions(suggestions);
michael@0 951 }
michael@0 952
michael@0 953 @Override
michael@0 954 public void onLoaderReset(Loader<ArrayList<String>> loader) {
michael@0 955 setSuggestions(new ArrayList<String>());
michael@0 956 }
michael@0 957 }
michael@0 958
michael@0 959 private static class ListSelectionListener implements View.OnFocusChangeListener,
michael@0 960 AdapterView.OnItemSelectedListener {
michael@0 961 private SearchEngineRow mSelectedEngineRow;
michael@0 962
michael@0 963 @Override
michael@0 964 public void onFocusChange(View v, boolean hasFocus) {
michael@0 965 if (hasFocus) {
michael@0 966 View selectedRow = ((ListView) v).getSelectedView();
michael@0 967 if (selectedRow != null) {
michael@0 968 selectRow(selectedRow);
michael@0 969 }
michael@0 970 } else {
michael@0 971 deselectRow();
michael@0 972 }
michael@0 973 }
michael@0 974
michael@0 975 @Override
michael@0 976 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
michael@0 977 deselectRow();
michael@0 978 selectRow(view);
michael@0 979 }
michael@0 980
michael@0 981 @Override
michael@0 982 public void onNothingSelected(AdapterView<?> parent) {
michael@0 983 deselectRow();
michael@0 984 }
michael@0 985
michael@0 986 private void selectRow(View row) {
michael@0 987 if (row instanceof SearchEngineRow) {
michael@0 988 mSelectedEngineRow = (SearchEngineRow) row;
michael@0 989 mSelectedEngineRow.onSelected();
michael@0 990 }
michael@0 991 }
michael@0 992
michael@0 993 private void deselectRow() {
michael@0 994 if (mSelectedEngineRow != null) {
michael@0 995 mSelectedEngineRow.onDeselected();
michael@0 996 mSelectedEngineRow = null;
michael@0 997 }
michael@0 998 }
michael@0 999 }
michael@0 1000
michael@0 1001 /**
michael@0 1002 * HomeSearchListView is a list view for displaying search engine results on the awesome screen.
michael@0 1003 */
michael@0 1004 public static class HomeSearchListView extends HomeListView {
michael@0 1005 public HomeSearchListView(Context context, AttributeSet attrs) {
michael@0 1006 this(context, attrs, R.attr.homeListViewStyle);
michael@0 1007 }
michael@0 1008
michael@0 1009 public HomeSearchListView(Context context, AttributeSet attrs, int defStyle) {
michael@0 1010 super(context, attrs, defStyle);
michael@0 1011 }
michael@0 1012
michael@0 1013 @Override
michael@0 1014 public boolean onTouchEvent(MotionEvent event) {
michael@0 1015 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
michael@0 1016 // Dismiss the soft keyboard.
michael@0 1017 requestFocus();
michael@0 1018 }
michael@0 1019
michael@0 1020 return super.onTouchEvent(event);
michael@0 1021 }
michael@0 1022 }
michael@0 1023 }

mercurial