Home | History | Annotate | Download | only in search
      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 package com.android.browser.search;
     17 
     18 import com.android.browser.R;
     19 
     20 import org.apache.http.HttpResponse;
     21 import org.apache.http.client.HttpClient;
     22 import org.apache.http.client.methods.HttpGet;
     23 import org.apache.http.params.HttpParams;
     24 import org.apache.http.util.EntityUtils;
     25 import org.json.JSONArray;
     26 import org.json.JSONException;
     27 
     28 import android.app.SearchManager;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.database.AbstractCursor;
     32 import android.database.Cursor;
     33 import android.net.ConnectivityManager;
     34 import android.net.NetworkInfo;
     35 import android.net.Uri;
     36 import android.net.http.AndroidHttpClient;
     37 import android.os.Bundle;
     38 import android.provider.Browser;
     39 import android.text.TextUtils;
     40 import android.util.Log;
     41 
     42 import java.io.IOException;
     43 
     44 /**
     45  * Provides search suggestions, if any, for a given web search provider.
     46  */
     47 public class OpenSearchSearchEngine implements SearchEngine {
     48 
     49     private static final String TAG = "OpenSearchSearchEngine";
     50 
     51     private static final String USER_AGENT = "Android/1.0";
     52     private static final int HTTP_TIMEOUT_MS = 1000;
     53 
     54     // TODO: this should be defined somewhere
     55     private static final String HTTP_TIMEOUT = "http.connection-manager.timeout";
     56 
     57     // Indices of the columns in the below arrays.
     58     private static final int COLUMN_INDEX_ID = 0;
     59     private static final int COLUMN_INDEX_QUERY = 1;
     60     private static final int COLUMN_INDEX_ICON = 2;
     61     private static final int COLUMN_INDEX_TEXT_1 = 3;
     62     private static final int COLUMN_INDEX_TEXT_2 = 4;
     63 
     64     // The suggestion columns used. If you are adding a new entry to these arrays make sure to
     65     // update the list of indices declared above.
     66     private static final String[] COLUMNS = new String[] {
     67         "_id",
     68         SearchManager.SUGGEST_COLUMN_QUERY,
     69         SearchManager.SUGGEST_COLUMN_ICON_1,
     70         SearchManager.SUGGEST_COLUMN_TEXT_1,
     71         SearchManager.SUGGEST_COLUMN_TEXT_2,
     72     };
     73 
     74     private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] {
     75         "_id",
     76         SearchManager.SUGGEST_COLUMN_QUERY,
     77         SearchManager.SUGGEST_COLUMN_ICON_1,
     78         SearchManager.SUGGEST_COLUMN_TEXT_1,
     79     };
     80 
     81     private final SearchEngineInfo mSearchEngineInfo;
     82 
     83     private final AndroidHttpClient mHttpClient;
     84 
     85     public OpenSearchSearchEngine(Context context, SearchEngineInfo searchEngineInfo) {
     86         mSearchEngineInfo = searchEngineInfo;
     87         mHttpClient = AndroidHttpClient.newInstance(USER_AGENT);
     88         HttpParams params = mHttpClient.getParams();
     89         params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS);
     90     }
     91 
     92     public String getName() {
     93         return mSearchEngineInfo.getName();
     94     }
     95 
     96     public CharSequence getLabel() {
     97         return mSearchEngineInfo.getLabel();
     98     }
     99 
    100     public void startSearch(Context context, String query, Bundle appData, String extraData) {
    101         String uri = mSearchEngineInfo.getSearchUriForQuery(query);
    102         if (uri == null) {
    103             Log.e(TAG, "Unable to get search URI for " + mSearchEngineInfo);
    104         } else {
    105             Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
    106             // Make sure the intent goes to the Browser itself
    107             intent.setPackage(context.getPackageName());
    108             intent.addCategory(Intent.CATEGORY_DEFAULT);
    109             intent.putExtra(SearchManager.QUERY, query);
    110             if (appData != null) {
    111                 intent.putExtra(SearchManager.APP_DATA, appData);
    112             }
    113             if (extraData != null) {
    114                 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
    115             }
    116             intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
    117             context.startActivity(intent);
    118         }
    119     }
    120 
    121     /**
    122      * Queries for a given search term and returns a cursor containing
    123      * suggestions ordered by best match.
    124      */
    125     public Cursor getSuggestions(Context context, String query) {
    126         if (TextUtils.isEmpty(query)) {
    127             return null;
    128         }
    129         if (!isNetworkConnected(context)) {
    130             Log.i(TAG, "Not connected to network.");
    131             return null;
    132         }
    133 
    134         String suggestUri = mSearchEngineInfo.getSuggestUriForQuery(query);
    135         if (TextUtils.isEmpty(suggestUri)) {
    136             // No suggest URI available for this engine
    137             return null;
    138         }
    139 
    140         try {
    141             String content = readUrl(suggestUri);
    142             if (content == null) return null;
    143             /* The data format is a JSON array with items being regular strings or JSON arrays
    144              * themselves. We are interested in the second and third elements, both of which
    145              * should be JSON arrays. The second element/array contains the suggestions and the
    146              * third element contains the descriptions. Some search engines don't support
    147              * suggestion descriptions so the third element is optional.
    148              */
    149             JSONArray results = new JSONArray(content);
    150             JSONArray suggestions = results.getJSONArray(1);
    151             JSONArray descriptions = null;
    152             if (results.length() > 2) {
    153                 descriptions = results.getJSONArray(2);
    154                 // Some search engines given an empty array "[]" for descriptions instead of
    155                 // not including it in the response.
    156                 if (descriptions.length() == 0) {
    157                     descriptions = null;
    158                 }
    159             }
    160             return new SuggestionsCursor(suggestions, descriptions);
    161         } catch (JSONException e) {
    162             Log.w(TAG, "Error", e);
    163         }
    164         return null;
    165     }
    166 
    167     /**
    168      * Executes a GET request and returns the response content.
    169      *
    170      * @param url Request URI.
    171      * @return The response content. This is the empty string if the response
    172      *         contained no content.
    173      */
    174     public String readUrl(String url) {
    175         try {
    176             HttpGet method = new HttpGet(url);
    177             HttpResponse response = mHttpClient.execute(method);
    178             if (response.getStatusLine().getStatusCode() == 200) {
    179                 return EntityUtils.toString(response.getEntity());
    180             } else {
    181                 Log.i(TAG, "Suggestion request failed");
    182                 return null;
    183             }
    184         } catch (IOException e) {
    185             Log.w(TAG, "Error", e);
    186             return null;
    187         }
    188     }
    189 
    190     public boolean supportsSuggestions() {
    191         return mSearchEngineInfo.supportsSuggestions();
    192     }
    193 
    194     public void close() {
    195         mHttpClient.close();
    196     }
    197 
    198     public boolean supportsVoiceSearch() {
    199         return getName().equals(SearchEngine.GOOGLE);
    200     }
    201 
    202     private boolean isNetworkConnected(Context context) {
    203         NetworkInfo networkInfo = getActiveNetworkInfo(context);
    204         return networkInfo != null && networkInfo.isConnected();
    205     }
    206 
    207     private NetworkInfo getActiveNetworkInfo(Context context) {
    208         ConnectivityManager connectivity =
    209                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    210         if (connectivity == null) {
    211             return null;
    212         }
    213         return connectivity.getActiveNetworkInfo();
    214     }
    215 
    216     private static class SuggestionsCursor extends AbstractCursor {
    217 
    218         private final JSONArray mSuggestions;
    219 
    220         private final JSONArray mDescriptions;
    221 
    222         public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) {
    223             mSuggestions = suggestions;
    224             mDescriptions = descriptions;
    225         }
    226 
    227         @Override
    228         public int getCount() {
    229             return mSuggestions.length();
    230         }
    231 
    232         @Override
    233         public String[] getColumnNames() {
    234             return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION);
    235         }
    236 
    237         @Override
    238         public String getString(int column) {
    239             if (mPos != -1) {
    240                 if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
    241                     try {
    242                         return mSuggestions.getString(mPos);
    243                     } catch (JSONException e) {
    244                         Log.w(TAG, "Error", e);
    245                     }
    246                 } else if (column == COLUMN_INDEX_TEXT_2) {
    247                     try {
    248                         return mDescriptions.getString(mPos);
    249                     } catch (JSONException e) {
    250                         Log.w(TAG, "Error", e);
    251                     }
    252                 } else if (column == COLUMN_INDEX_ICON) {
    253                     return String.valueOf(R.drawable.magnifying_glass);
    254                 }
    255             }
    256             return null;
    257         }
    258 
    259         @Override
    260         public double getDouble(int column) {
    261             throw new UnsupportedOperationException();
    262         }
    263 
    264         @Override
    265         public float getFloat(int column) {
    266             throw new UnsupportedOperationException();
    267         }
    268 
    269         @Override
    270         public int getInt(int column) {
    271             throw new UnsupportedOperationException();
    272         }
    273 
    274         @Override
    275         public long getLong(int column) {
    276             if (column == COLUMN_INDEX_ID) {
    277                 return mPos;        // use row# as the _Id
    278             }
    279             throw new UnsupportedOperationException();
    280         }
    281 
    282         @Override
    283         public short getShort(int column) {
    284             throw new UnsupportedOperationException();
    285         }
    286 
    287         @Override
    288         public boolean isNull(int column) {
    289             throw new UnsupportedOperationException();
    290         }
    291     }
    292 
    293     @Override
    294     public String toString() {
    295         return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}";
    296     }
    297 
    298     @Override
    299     public boolean wantsEmptyQuery() {
    300         return false;
    301     }
    302 
    303 }
    304