Home | History | Annotate | Download | only in browser
      1 /*
      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  */
     16 package com.android.browser;
     17 
     18 import com.android.browser.Controller;
     19 import com.android.browser.R;
     20 import com.android.browser.UI.DropdownChangeListener;
     21 import com.android.browser.provider.BrowserProvider;
     22 import com.android.browser.search.SearchEngine;
     23 
     24 import android.app.SearchManager;
     25 import android.content.Context;
     26 import android.database.AbstractCursor;
     27 import android.database.Cursor;
     28 import android.net.Uri;
     29 import android.os.Bundle;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.util.LruCache;
     33 import android.webkit.SearchBox;
     34 import android.webkit.WebView;
     35 
     36 import java.util.Collections;
     37 import java.util.List;
     38 
     39 public 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.getApplicationContext();
     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.
     81         if (mSearchBox == null || !isInstantPage()) {
     82             mWrapped.startSearch(context, query, appData, extraData);
     83             return;
     84         }
     85 
     86         mSearchBox.setQuery(query);
     87         mSearchBox.setVerbatim(true);
     88         mSearchBox.onsubmit(null);
     89     }
     90 
     91     private final class BrowserSearchboxListener extends SearchBox.SearchBoxListener {
     92         /*
     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() {
    178         final WebView current = getCurrentWebview();
    179         if (current == null) {
    180             return;
    181         }
    182 
    183         final SearchBox searchBox = current.getSearchBox();
    184         if (searchBox != mSearchBox) {
    185             if (mSearchBox != null) {
    186                 mSearchBox.removeSearchBoxListener(mListener);
    187                 mListener.clear();
    188             }
    189             mSearchBox = searchBox;
    190             if (mSearchBox != null) {
    191                 mSearchBox.addSearchBoxListener(mListener);
    192             }
    193         }
    194     }
    195 
    196     private boolean isInstantPage() {
    197         final WebView current = getCurrentWebview();
    198         if (current == null) {
    199             return false;
    200         }
    201 
    202         final String currentUrl = mController.getCurrentTab().getUrl();
    203 
    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() {
    224                 final WebView current = getCurrentWebview();
    225                 if (current != null) {
    226                     current.loadUrl(getInstantBaseUrl());
    227                 }
    228             }
    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 
    251         if (mSearchBox == null) {
    252             return mWrapped.getSuggestions(context, query);
    253         }
    254 
    255         mSearchBox.setDimensions(0, 0, 0, mHeight);
    256         mSearchBox.onresize(null);
    257 
    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);
    265         mSearchBox.onchange(null);
    266 
    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) {
    309         final WebView current = getCurrentWebview();
    310         if (current == null) {
    311             return 0;
    312         }
    313 
    314         final float scale = current.getScale();
    315         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;
    328             if (mSearchBox != null) {
    329                 mSearchBox.setDimensions(0, 0, 0, rescaledHeight);
    330                 mSearchBox.onresize(null);
    331             }
    332         }
    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 }
    431