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