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