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 java.io.InputStream; 21 import java.net.HttpURLConnection; 22 import java.net.URL; 23 import java.nio.charset.Charset; 24 import java.nio.charset.IllegalCharsetNameException; 25 import java.nio.charset.UnsupportedCharsetException; 26 import libcore.io.Streams; 27 import libcore.net.http.ResponseUtils; 28 import org.json.JSONArray; 29 import org.json.JSONException; 30 31 import android.app.SearchManager; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.database.AbstractCursor; 35 import android.database.Cursor; 36 import android.net.ConnectivityManager; 37 import android.net.NetworkInfo; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.provider.Browser; 41 import android.text.TextUtils; 42 import android.util.Log; 43 44 import java.io.IOException; 45 46 /** 47 * Provides search suggestions, if any, for a given web search provider. 48 */ 49 public class OpenSearchSearchEngine implements SearchEngine { 50 51 private static final String TAG = "OpenSearchSearchEngine"; 52 53 private static final String USER_AGENT = "Android/1.0"; 54 private static final int HTTP_TIMEOUT_MS = 1000; 55 56 // Indices of the columns in the below arrays. 57 private static final int COLUMN_INDEX_ID = 0; 58 private static final int COLUMN_INDEX_QUERY = 1; 59 private static final int COLUMN_INDEX_ICON = 2; 60 private static final int COLUMN_INDEX_TEXT_1 = 3; 61 private static final int COLUMN_INDEX_TEXT_2 = 4; 62 63 // The suggestion columns used. If you are adding a new entry to these arrays make sure to 64 // update the list of indices declared above. 65 private static final String[] COLUMNS = new String[] { 66 "_id", 67 SearchManager.SUGGEST_COLUMN_QUERY, 68 SearchManager.SUGGEST_COLUMN_ICON_1, 69 SearchManager.SUGGEST_COLUMN_TEXT_1, 70 SearchManager.SUGGEST_COLUMN_TEXT_2, 71 }; 72 73 private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] { 74 "_id", 75 SearchManager.SUGGEST_COLUMN_QUERY, 76 SearchManager.SUGGEST_COLUMN_ICON_1, 77 SearchManager.SUGGEST_COLUMN_TEXT_1, 78 }; 79 80 private final SearchEngineInfo mSearchEngineInfo; 81 82 public OpenSearchSearchEngine(Context context, SearchEngineInfo searchEngineInfo) { 83 mSearchEngineInfo = searchEngineInfo; 84 } 85 86 public String getName() { 87 return mSearchEngineInfo.getName(); 88 } 89 90 public CharSequence getLabel() { 91 return mSearchEngineInfo.getLabel(); 92 } 93 94 public void startSearch(Context context, String query, Bundle appData, String extraData) { 95 String uri = mSearchEngineInfo.getSearchUriForQuery(query); 96 if (uri == null) { 97 Log.e(TAG, "Unable to get search URI for " + mSearchEngineInfo); 98 } else { 99 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); 100 // Make sure the intent goes to the Browser itself 101 intent.setPackage(context.getPackageName()); 102 intent.addCategory(Intent.CATEGORY_DEFAULT); 103 intent.putExtra(SearchManager.QUERY, query); 104 if (appData != null) { 105 intent.putExtra(SearchManager.APP_DATA, appData); 106 } 107 if (extraData != null) { 108 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 109 } 110 intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); 111 context.startActivity(intent); 112 } 113 } 114 115 /** 116 * Queries for a given search term and returns a cursor containing 117 * suggestions ordered by best match. 118 */ 119 public Cursor getSuggestions(Context context, String query) { 120 if (TextUtils.isEmpty(query)) { 121 return null; 122 } 123 if (!isNetworkConnected(context)) { 124 Log.i(TAG, "Not connected to network."); 125 return null; 126 } 127 128 String suggestUri = mSearchEngineInfo.getSuggestUriForQuery(query); 129 if (TextUtils.isEmpty(suggestUri)) { 130 // No suggest URI available for this engine 131 return null; 132 } 133 134 try { 135 String content = readUrl(suggestUri); 136 if (content == null) return null; 137 /* The data format is a JSON array with items being regular strings or JSON arrays 138 * themselves. We are interested in the second and third elements, both of which 139 * should be JSON arrays. The second element/array contains the suggestions and the 140 * third element contains the descriptions. Some search engines don't support 141 * suggestion descriptions so the third element is optional. 142 */ 143 JSONArray results = new JSONArray(content); 144 JSONArray suggestions = results.getJSONArray(1); 145 JSONArray descriptions = null; 146 if (results.length() > 2) { 147 descriptions = results.getJSONArray(2); 148 // Some search engines given an empty array "[]" for descriptions instead of 149 // not including it in the response. 150 if (descriptions.length() == 0) { 151 descriptions = null; 152 } 153 } 154 return new SuggestionsCursor(suggestions, descriptions); 155 } catch (JSONException e) { 156 Log.w(TAG, "Error", e); 157 } 158 return null; 159 } 160 161 /** 162 * Executes a GET request and returns the response content. 163 * 164 * @param urlString Request URI. 165 * @return The response content. This is the empty string if the response 166 * contained no content. 167 */ 168 public String readUrl(String urlString) { 169 try { 170 URL url = new URL(urlString); 171 HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); 172 urlConnection.setRequestProperty("User-Agent", USER_AGENT); 173 urlConnection.setConnectTimeout(HTTP_TIMEOUT_MS); 174 175 if (urlConnection.getResponseCode() == 200) { 176 final Charset responseCharset; 177 try { 178 responseCharset = ResponseUtils.responseCharset(urlConnection.getContentType()); 179 } catch (UnsupportedCharsetException ucse) { 180 Log.i(TAG, "Unsupported response charset", ucse); 181 return null; 182 } catch (IllegalCharsetNameException icne) { 183 Log.i(TAG, "Illegal response charset", icne); 184 return null; 185 } 186 187 byte[] responseBytes = Streams.readFully(urlConnection.getInputStream()); 188 return new String(responseBytes, responseCharset); 189 } else { 190 Log.i(TAG, "Suggestion request failed"); 191 return null; 192 } 193 } catch (IOException e) { 194 Log.w(TAG, "Error", e); 195 return null; 196 } 197 } 198 199 public boolean supportsSuggestions() { 200 return mSearchEngineInfo.supportsSuggestions(); 201 } 202 203 public void close() { 204 } 205 206 private boolean isNetworkConnected(Context context) { 207 NetworkInfo networkInfo = getActiveNetworkInfo(context); 208 return networkInfo != null && networkInfo.isConnected(); 209 } 210 211 private NetworkInfo getActiveNetworkInfo(Context context) { 212 ConnectivityManager connectivity = 213 (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 214 if (connectivity == null) { 215 return null; 216 } 217 return connectivity.getActiveNetworkInfo(); 218 } 219 220 private static class SuggestionsCursor extends AbstractCursor { 221 222 private final JSONArray mSuggestions; 223 224 private final JSONArray mDescriptions; 225 226 public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) { 227 mSuggestions = suggestions; 228 mDescriptions = descriptions; 229 } 230 231 @Override 232 public int getCount() { 233 return mSuggestions.length(); 234 } 235 236 @Override 237 public String[] getColumnNames() { 238 return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION); 239 } 240 241 @Override 242 public String getString(int column) { 243 if (mPos != -1) { 244 if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) { 245 try { 246 return mSuggestions.getString(mPos); 247 } catch (JSONException e) { 248 Log.w(TAG, "Error", e); 249 } 250 } else if (column == COLUMN_INDEX_TEXT_2) { 251 try { 252 return mDescriptions.getString(mPos); 253 } catch (JSONException e) { 254 Log.w(TAG, "Error", e); 255 } 256 } else if (column == COLUMN_INDEX_ICON) { 257 return String.valueOf(R.drawable.magnifying_glass); 258 } 259 } 260 return null; 261 } 262 263 @Override 264 public double getDouble(int column) { 265 throw new UnsupportedOperationException(); 266 } 267 268 @Override 269 public float getFloat(int column) { 270 throw new UnsupportedOperationException(); 271 } 272 273 @Override 274 public int getInt(int column) { 275 throw new UnsupportedOperationException(); 276 } 277 278 @Override 279 public long getLong(int column) { 280 if (column == COLUMN_INDEX_ID) { 281 return mPos; // use row# as the _Id 282 } 283 throw new UnsupportedOperationException(); 284 } 285 286 @Override 287 public short getShort(int column) { 288 throw new UnsupportedOperationException(); 289 } 290 291 @Override 292 public boolean isNull(int column) { 293 throw new UnsupportedOperationException(); 294 } 295 } 296 297 @Override 298 public String toString() { 299 return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}"; 300 } 301 302 @Override 303 public boolean wantsEmptyQuery() { 304 return false; 305 } 306 307 } 308