OpenSearchSearchEngine.java revision 430057dad085f3c3dbc386f127b1f5a10a9851da
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 */ 16package com.android.browser.search; 17 18import com.android.browser.R; 19 20import org.apache.http.HttpResponse; 21import org.apache.http.client.HttpClient; 22import org.apache.http.client.methods.HttpGet; 23import org.apache.http.params.HttpParams; 24import org.apache.http.util.EntityUtils; 25import org.json.JSONArray; 26import org.json.JSONException; 27 28import android.app.SearchManager; 29import android.content.Context; 30import android.content.Intent; 31import android.database.AbstractCursor; 32import android.database.Cursor; 33import android.net.ConnectivityManager; 34import android.net.NetworkInfo; 35import android.net.Uri; 36import android.net.http.AndroidHttpClient; 37import android.os.Bundle; 38import android.provider.Browser; 39import android.text.TextUtils; 40import android.util.Log; 41 42import java.io.IOException; 43 44/** 45 * Provides search suggestions, if any, for a given web search provider. 46 */ 47public 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 * @param requestHeaders Request headers. 172 * @return The response content. This is the empty string if the response 173 * contained no content. 174 */ 175 public String readUrl(String url) { 176 try { 177 HttpGet method = new HttpGet(url); 178 HttpResponse response = mHttpClient.execute(method); 179 if (response.getStatusLine().getStatusCode() == 200) { 180 return EntityUtils.toString(response.getEntity()); 181 } else { 182 Log.i(TAG, "Suggestion request failed"); 183 return null; 184 } 185 } catch (IOException e) { 186 Log.w(TAG, "Error", e); 187 return null; 188 } 189 } 190 191 public boolean supportsSuggestions() { 192 return mSearchEngineInfo.supportsSuggestions(); 193 } 194 195 public void close() { 196 mHttpClient.close(); 197 } 198 199 public boolean supportsVoiceSearch() { 200 return getName().equals(SearchEngine.GOOGLE); 201 } 202 203 private boolean isNetworkConnected(Context context) { 204 NetworkInfo networkInfo = getActiveNetworkInfo(context); 205 return networkInfo != null && networkInfo.isConnected(); 206 } 207 208 private NetworkInfo getActiveNetworkInfo(Context context) { 209 ConnectivityManager connectivity = 210 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 211 if (connectivity == null) { 212 return null; 213 } 214 return connectivity.getActiveNetworkInfo(); 215 } 216 217 private static class SuggestionsCursor extends AbstractCursor { 218 219 private final JSONArray mSuggestions; 220 221 private final JSONArray mDescriptions; 222 223 public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) { 224 mSuggestions = suggestions; 225 mDescriptions = descriptions; 226 } 227 228 @Override 229 public int getCount() { 230 return mSuggestions.length(); 231 } 232 233 @Override 234 public String[] getColumnNames() { 235 return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION); 236 } 237 238 @Override 239 public String getString(int column) { 240 if (mPos != -1) { 241 if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) { 242 try { 243 return mSuggestions.getString(mPos); 244 } catch (JSONException e) { 245 Log.w(TAG, "Error", e); 246 } 247 } else if (column == COLUMN_INDEX_TEXT_2) { 248 try { 249 return mDescriptions.getString(mPos); 250 } catch (JSONException e) { 251 Log.w(TAG, "Error", e); 252 } 253 } else if (column == COLUMN_INDEX_ICON) { 254 return String.valueOf(R.drawable.magnifying_glass); 255 } 256 } 257 return null; 258 } 259 260 @Override 261 public double getDouble(int column) { 262 throw new UnsupportedOperationException(); 263 } 264 265 @Override 266 public float getFloat(int column) { 267 throw new UnsupportedOperationException(); 268 } 269 270 @Override 271 public int getInt(int column) { 272 throw new UnsupportedOperationException(); 273 } 274 275 @Override 276 public long getLong(int column) { 277 if (column == COLUMN_INDEX_ID) { 278 return mPos; // use row# as the _Id 279 } 280 throw new UnsupportedOperationException(); 281 } 282 283 @Override 284 public short getShort(int column) { 285 throw new UnsupportedOperationException(); 286 } 287 288 @Override 289 public boolean isNull(int column) { 290 throw new UnsupportedOperationException(); 291 } 292 } 293 294 @Override 295 public String toString() { 296 return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}"; 297 } 298 299} 300