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     private boolean isNetworkConnected(Context context) {
    199         NetworkInfo networkInfo = getActiveNetworkInfo(context);
    200         return networkInfo != null && networkInfo.isConnected();
    201     }
    202 
    203     private NetworkInfo getActiveNetworkInfo(Context context) {
    204         ConnectivityManager connectivity =
    205                 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    206         if (connectivity == null) {
    207             return null;
    208         }
    209         return connectivity.getActiveNetworkInfo();
    210     }
    211 
    212     private static class SuggestionsCursor extends AbstractCursor {
    213 
    214         private final JSONArray mSuggestions;
    215 
    216         private final JSONArray mDescriptions;
    217 
    218         public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) {
    219             mSuggestions = suggestions;
    220             mDescriptions = descriptions;
    221         }
    222 
    223         @Override
    224         public int getCount() {
    225             return mSuggestions.length();
    226         }
    227 
    228         @Override
    229         public String[] getColumnNames() {
    230             return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION);
    231         }
    232 
    233         @Override
    234         public String getString(int column) {
    235             if (mPos != -1) {
    236                 if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
    237                     try {
    238                         return mSuggestions.getString(mPos);
    239                     } catch (JSONException e) {
    240                         Log.w(TAG, "Error", e);
    241                     }
    242                 } else if (column == COLUMN_INDEX_TEXT_2) {
    243                     try {
    244                         return mDescriptions.getString(mPos);
    245                     } catch (JSONException e) {
    246                         Log.w(TAG, "Error", e);
    247                     }
    248                 } else if (column == COLUMN_INDEX_ICON) {
    249                     return String.valueOf(R.drawable.magnifying_glass);
    250                 }
    251             }
    252             return null;
    253         }
    254 
    255         @Override
    256         public double getDouble(int column) {
    257             throw new UnsupportedOperationException();
    258         }
    259 
    260         @Override
    261         public float getFloat(int column) {
    262             throw new UnsupportedOperationException();
    263         }
    264 
    265         @Override
    266         public int getInt(int column) {
    267             throw new UnsupportedOperationException();
    268         }
    269 
    270         @Override
    271         public long getLong(int column) {
    272             if (column == COLUMN_INDEX_ID) {
    273                 return mPos;        // use row# as the _Id
    274             }
    275             throw new UnsupportedOperationException();
    276         }
    277 
    278         @Override
    279         public short getShort(int column) {
    280             throw new UnsupportedOperationException();
    281         }
    282 
    283         @Override
    284         public boolean isNull(int column) {
    285             throw new UnsupportedOperationException();
    286         }
    287     }
    288 
    289     @Override
    290     public String toString() {
    291         return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}";
    292     }
    293 
    294     @Override
    295     public boolean wantsEmptyQuery() {
    296         return false;
    297     }
    298 
    299 }
    300