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