• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
OpenSearchSearchEngine(Context context, SearchEngineInfo searchEngineInfo)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 
getName()92     public String getName() {
93         return mSearchEngineInfo.getName();
94     }
95 
getLabel()96     public CharSequence getLabel() {
97         return mSearchEngineInfo.getLabel();
98     }
99 
startSearch(Context context, String query, Bundle appData, String extraData)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      */
getSuggestions(Context context, String query)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      */
readUrl(String url)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 
supportsSuggestions()190     public boolean supportsSuggestions() {
191         return mSearchEngineInfo.supportsSuggestions();
192     }
193 
close()194     public void close() {
195         mHttpClient.close();
196     }
197 
isNetworkConnected(Context context)198     private boolean isNetworkConnected(Context context) {
199         NetworkInfo networkInfo = getActiveNetworkInfo(context);
200         return networkInfo != null && networkInfo.isConnected();
201     }
202 
getActiveNetworkInfo(Context context)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 
SuggestionsCursor(JSONArray suggestions, JSONArray descriptions)218         public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) {
219             mSuggestions = suggestions;
220             mDescriptions = descriptions;
221         }
222 
223         @Override
getCount()224         public int getCount() {
225             return mSuggestions.length();
226         }
227 
228         @Override
getColumnNames()229         public String[] getColumnNames() {
230             return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION);
231         }
232 
233         @Override
getString(int column)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
getDouble(int column)256         public double getDouble(int column) {
257             throw new UnsupportedOperationException();
258         }
259 
260         @Override
getFloat(int column)261         public float getFloat(int column) {
262             throw new UnsupportedOperationException();
263         }
264 
265         @Override
getInt(int column)266         public int getInt(int column) {
267             throw new UnsupportedOperationException();
268         }
269 
270         @Override
getLong(int column)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
getShort(int column)279         public short getShort(int column) {
280             throw new UnsupportedOperationException();
281         }
282 
283         @Override
isNull(int column)284         public boolean isNull(int column) {
285             throw new UnsupportedOperationException();
286         }
287     }
288 
289     @Override
toString()290     public String toString() {
291         return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}";
292     }
293 
294     @Override
wantsEmptyQuery()295     public boolean wantsEmptyQuery() {
296         return false;
297     }
298 
299 }
300