blob: 6ff7db75840508cff148f10e93aa6cec6fe28d9e [file] [log] [blame]
Narayan Kamath5119edd2011-02-23 15:49:17 +00001/*
2 * Copyright (C) 2011 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 */
16package com.android.browser;
17
Narayan Kamath5119edd2011-02-23 15:49:17 +000018import com.android.browser.Controller;
19import com.android.browser.R;
20import com.android.browser.UI.DropdownChangeListener;
Narayan Kamath5119edd2011-02-23 15:49:17 +000021import com.android.browser.search.SearchEngine;
22
23import android.app.SearchManager;
24import android.content.Context;
25import android.database.AbstractCursor;
26import android.database.Cursor;
27import android.net.Uri;
28import android.os.Bundle;
29import android.text.TextUtils;
30import android.util.Log;
31import android.util.LruCache;
32import android.webkit.SearchBox;
33import android.webkit.WebView;
34
35import java.util.Collections;
36import java.util.List;
37
38public class InstantSearchEngine implements SearchEngine, DropdownChangeListener {
39 private static final String TAG = "Browser.InstantSearchEngine";
40 private static final boolean DBG = false;
41
42 private Controller mController;
43 private SearchBox mSearchBox;
44 private final BrowserSearchboxListener mListener = new BrowserSearchboxListener();
45 private int mHeight;
46
47 private String mInstantBaseUrl;
48 private final Context mContext;
49 // Used for startSearch( ) calls if for some reason instant
50 // is off, or no searchbox is present.
51 private final SearchEngine mWrapped;
52
53 public InstantSearchEngine(Context context, SearchEngine wrapped) {
54 mContext = context;
55 mWrapped = wrapped;
56 }
57
58 public void setController(Controller controller) {
59 mController = controller;
60 }
61
62 @Override
63 public String getName() {
64 return SearchEngine.GOOGLE;
65 }
66
67 @Override
68 public CharSequence getLabel() {
69 return mContext.getResources().getString(R.string.instant_search_label);
70 }
71
72 @Override
73 public void startSearch(Context context, String query, Bundle appData, String extraData) {
74 if (DBG) Log.d(TAG, "startSearch(" + query + ")");
75
76 switchSearchboxIfNeeded();
77
78 // If for some reason we are in a bad state, ensure that the
79 // user gets default search results at the very least.
Narayan Kamath41feb052011-03-04 18:57:30 +000080 if (mSearchBox == null || !isInstantPage()) {
Narayan Kamath5119edd2011-02-23 15:49:17 +000081 mWrapped.startSearch(context, query, appData, extraData);
82 return;
83 }
84
85 mSearchBox.setQuery(query);
86 mSearchBox.setVerbatim(true);
87 mSearchBox.onsubmit();
88 }
89
90 private final class BrowserSearchboxListener implements SearchBox.SearchBoxListener {
91 /*
92 * The maximum number of out of order suggestions we accept
93 * before giving up the wait.
94 */
95 private static final int MAX_OUT_OF_ORDER = 5;
96
97 /*
98 * We wait for suggestions in increments of 600ms. This is primarily to
99 * guard against suggestions arriving out of order.
100 */
101 private static final int WAIT_INCREMENT_MS = 600;
102
103 /*
104 * A cache of suggestions received, keyed by the queries they were
105 * received for.
106 */
107 private final LruCache<String, List<String>> mSuggestions =
108 new LruCache<String, List<String>>(20);
109
110 /*
111 * The last set of suggestions received. We use this reduce UI flicker
112 * in case there is a delay in recieving suggestions.
113 */
114 private List<String> mLatestSuggestion = Collections.emptyList();
115
116 @Override
117 public synchronized void onSuggestionsReceived(String query, List<String> suggestions) {
118 if (DBG) Log.d(TAG, "onSuggestionsReceived(" + query + ")");
119
120 if (!TextUtils.isEmpty(query)) {
121 mSuggestions.put(query, suggestions);
122 mLatestSuggestion = suggestions;
123 }
124
125 notifyAll();
126 }
127
128 public synchronized List<String> tryWaitForSuggestions(String query) {
129 if (DBG) Log.d(TAG, "tryWait(" + query + ")");
130
131 int numWaitReturns = 0;
132
133 // This slightly unusual waiting construct is used to safeguard
134 // to some extent against suggestions arriving out of order. We
135 // wait for upto 5 notifyAll( ) calls to check if we received
136 // suggestions for a given query.
137 while (mSuggestions.get(query) == null) {
138 try {
139 wait(WAIT_INCREMENT_MS);
140 ++numWaitReturns;
141 if (numWaitReturns > MAX_OUT_OF_ORDER) {
142 // We've waited too long for suggestions to be returned.
143 // return the last available suggestion.
144 break;
145 }
146 } catch (InterruptedException e) {
147 return Collections.emptyList();
148 }
149 }
150
151 List<String> suggestions = mSuggestions.get(query);
152 if (suggestions == null) {
153 return mLatestSuggestion;
154 }
155
156 return suggestions;
157 }
158
159 public synchronized void clear() {
160 mSuggestions.evictAll();
161 }
162 }
163
164 private WebView getCurrentWebview() {
165 if (mController != null) {
166 return mController.getTabControl().getCurrentTopWebView();
167 }
168
169 return null;
170 }
171
172 /**
173 * Attaches the searchbox to the right browser page, i.e, the currently
174 * visible tab.
175 */
176 private void switchSearchboxIfNeeded() {
Narayan Kamath931e4d02011-03-28 13:14:05 +0100177 final WebView current = getCurrentWebview();
178 if (current == null) {
179 return;
180 }
181
182 final SearchBox searchBox = current.getSearchBox();
Narayan Kamath5119edd2011-02-23 15:49:17 +0000183 if (searchBox != mSearchBox) {
184 if (mSearchBox != null) {
185 mSearchBox.removeSearchBoxListener(mListener);
186 mListener.clear();
187 }
188 mSearchBox = searchBox;
Michael Kolb00a22752011-03-03 17:13:30 -0800189 if (mSearchBox != null) {
190 mSearchBox.addSearchBoxListener(mListener);
191 }
Narayan Kamath5119edd2011-02-23 15:49:17 +0000192 }
193 }
194
195 private boolean isInstantPage() {
Narayan Kamath931e4d02011-03-28 13:14:05 +0100196 final WebView current = getCurrentWebview();
197 if (current == null) {
198 return false;
199 }
200
201 final String currentUrl = current.getUrl();
Narayan Kamath5119edd2011-02-23 15:49:17 +0000202
203 if (currentUrl != null) {
204 Uri uri = Uri.parse(currentUrl);
205 final String host = uri.getHost();
206 final String path = uri.getPath();
207
208 // Is there a utility class that does this ?
209 if (path != null && host != null) {
210 return host.startsWith("www.google.") &&
211 (path.startsWith("/search") || path.startsWith("/webhp"));
212 }
213 return false;
214 }
215
216 return false;
217 }
218
219 private void loadInstantPage() {
220 mController.getActivity().runOnUiThread(new Runnable() {
221 @Override
222 public void run() {
Narayan Kamath931e4d02011-03-28 13:14:05 +0100223 final WebView current = getCurrentWebview();
224 if (current != null) {
225 current.loadUrl(getInstantBaseUrl());
226 }
Narayan Kamath5119edd2011-02-23 15:49:17 +0000227 }
228 });
229 }
230
231 /**
232 * Queries for a given search term and returns a cursor containing
233 * suggestions ordered by best match.
234 */
235 @Override
236 public Cursor getSuggestions(Context context, String query) {
237 if (DBG) Log.d(TAG, "getSuggestions(" + query + ")");
238 if (query == null) {
239 return null;
240 }
241
242 if (!isInstantPage()) {
243 loadInstantPage();
244 }
245
246 switchSearchboxIfNeeded();
247
248 mController.registerDropdownChangeListener(this);
249
Narayan Kamath41feb052011-03-04 18:57:30 +0000250 if (mSearchBox == null) {
251 return mWrapped.getSuggestions(context, query);
252 }
253
Narayan Kamath5119edd2011-02-23 15:49:17 +0000254 mSearchBox.setDimensions(0, 0, 0, mHeight);
255 mSearchBox.onresize();
256
257 if (TextUtils.isEmpty(query)) {
258 // To force the SRP to render an empty (no results) page.
259 mSearchBox.setVerbatim(true);
260 } else {
261 mSearchBox.setVerbatim(false);
262 }
263 mSearchBox.setQuery(query);
264 mSearchBox.onchange();
265
266 // Don't bother waiting for suggestions for an empty query. We still
267 // set the query so that the SRP clears itself.
268 if (TextUtils.isEmpty(query)) {
269 return new SuggestionsCursor(Collections.<String>emptyList());
270 } else {
271 return new SuggestionsCursor(mListener.tryWaitForSuggestions(query));
272 }
273 }
274
275 @Override
276 public boolean supportsSuggestions() {
277 return true;
278 }
279
280 @Override
281 public void close() {
282 if (mController != null) {
283 mController.registerDropdownChangeListener(null);
284 }
285 if (mSearchBox != null) {
286 mSearchBox.removeSearchBoxListener(mListener);
287 }
288 mListener.clear();
289 mWrapped.close();
290 }
291
292 @Override
293 public boolean supportsVoiceSearch() {
294 return false;
295 }
296
297 @Override
298 public String toString() {
299 return "InstantSearchEngine {" + hashCode() + "}";
300 }
301
302 @Override
303 public boolean wantsEmptyQuery() {
304 return true;
305 }
306
307 private int rescaleHeight(int height) {
Narayan Kamath931e4d02011-03-28 13:14:05 +0100308 final WebView current = getCurrentWebview();
309 if (current == null) {
310 return 0;
311 }
312
313 final float scale = current.getScale();
Narayan Kamath5119edd2011-02-23 15:49:17 +0000314 if (scale != 0) {
315 return (int) (height / scale);
316 }
317
318 return height;
319 }
320
321 @Override
322 public void onNewDropdownDimensions(int height) {
323 final int rescaledHeight = rescaleHeight(height);
324
325 if (rescaledHeight != mHeight) {
326 mHeight = rescaledHeight;
Narayan Kamath931e4d02011-03-28 13:14:05 +0100327 if (mSearchBox != null) {
328 mSearchBox.setDimensions(0, 0, 0, rescaledHeight);
329 mSearchBox.onresize();
330 }
Narayan Kamath5119edd2011-02-23 15:49:17 +0000331 }
332 }
333
334 private String getInstantBaseUrl() {
335 if (mInstantBaseUrl == null) {
336 String url = mContext.getResources().getString(R.string.instant_base);
337 if (url.indexOf("{CID}") != -1) {
338 url = url.replace("{CID}",
339 BrowserProvider.getClientId(mContext.getContentResolver()));
340 }
341 mInstantBaseUrl = url;
342 }
343
344 return mInstantBaseUrl;
345 }
346
347 // Indices of the columns in the below arrays.
348 private static final int COLUMN_INDEX_ID = 0;
349 private static final int COLUMN_INDEX_QUERY = 1;
350 private static final int COLUMN_INDEX_ICON = 2;
351 private static final int COLUMN_INDEX_TEXT_1 = 3;
352
353 private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] {
354 "_id",
355 SearchManager.SUGGEST_COLUMN_QUERY,
356 SearchManager.SUGGEST_COLUMN_ICON_1,
357 SearchManager.SUGGEST_COLUMN_TEXT_1,
358 };
359
360 private static class SuggestionsCursor extends AbstractCursor {
361 private final List<String> mSuggestions;
362
363 public SuggestionsCursor(List<String> suggestions) {
364 mSuggestions = suggestions;
365 }
366
367 @Override
368 public int getCount() {
369 return mSuggestions.size();
370 }
371
372 @Override
373 public String[] getColumnNames() {
374 return COLUMNS_WITHOUT_DESCRIPTION;
375 }
376
377 private String format(String suggestion) {
378 if (TextUtils.isEmpty(suggestion)) {
379 return "";
380 }
381 return suggestion;
382 }
383
384 @Override
385 public String getString(int column) {
386 if (mPos >= 0 && mPos < mSuggestions.size()) {
387 if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
388 return format(mSuggestions.get(mPos));
389 } else if (column == COLUMN_INDEX_ICON) {
390 return String.valueOf(R.drawable.magnifying_glass);
391 }
392 }
393 return null;
394 }
395
396 @Override
397 public double getDouble(int column) {
398 throw new UnsupportedOperationException();
399 }
400
401 @Override
402 public float getFloat(int column) {
403 throw new UnsupportedOperationException();
404 }
405
406 @Override
407 public int getInt(int column) {
408 if (column == COLUMN_INDEX_ID) {
409 return mPos;
410 }
411 throw new UnsupportedOperationException();
412 }
413
414 @Override
415 public long getLong(int column) {
416 throw new UnsupportedOperationException();
417 }
418
419 @Override
420 public short getShort(int column) {
421 throw new UnsupportedOperationException();
422 }
423
424 @Override
425 public boolean isNull(int column) {
426 throw new UnsupportedOperationException();
427 }
428 }
429}