GoogleSuggestionProvider.java revision dfabb7cfae91f6af3b5a9a9584cdec5557d1fceb
1/* 2 * Copyright (C) 2008 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 17package com.android.quicksearchbox.google; 18 19import com.android.quicksearchbox.R; 20 21import org.apache.http.HttpResponse; 22import org.apache.http.client.HttpClient; 23import org.apache.http.client.methods.HttpGet; 24import org.apache.http.params.HttpParams; 25import org.apache.http.util.EntityUtils; 26import org.json.JSONArray; 27import org.json.JSONException; 28 29import android.app.SearchManager; 30import android.content.ContentProvider; 31import android.content.ContentValues; 32import android.content.Context; 33import android.database.AbstractCursor; 34import android.database.Cursor; 35import android.net.ConnectivityManager; 36import android.net.NetworkInfo; 37import android.net.Uri; 38import android.net.http.AndroidHttpClient; 39import android.text.TextUtils; 40import android.util.Log; 41 42import java.io.IOException; 43import java.io.UnsupportedEncodingException; 44import java.net.URLEncoder; 45import java.util.Locale; 46 47/** 48 * Use network-based Google Suggests to provide search suggestions. 49 * 50 * Future: Merge live suggestions with saved recent queries 51 */ 52public class GoogleSuggestionProvider extends ContentProvider { 53 54 private static final boolean DBG = true; 55 private static final String LOG_TAG = "GoogleSearch"; 56 57 private static final String USER_AGENT = "Android/1.0"; 58 private String mSuggestUri; 59 private static final int HTTP_TIMEOUT_MS = 1000; 60 61 // TODO: this should be defined somewhere 62 private static final String HTTP_TIMEOUT = "http.connection-manager.timeout"; 63 64 // Indexes into COLUMNS 65 private static final int COL_ID = 0; 66 private static final int COL_TEXT_1 = 1; 67 private static final int COL_TEXT_2 = 2; 68 private static final int COL_ICON_1 = 3; 69 private static final int COL_ICON_2 = 4; 70 private static final int COL_QUERY = 5; 71 72 /* The suggestion columns used */ 73 private static final String[] COLUMNS = new String[] { 74 "_id", 75 SearchManager.SUGGEST_COLUMN_TEXT_1, 76 SearchManager.SUGGEST_COLUMN_TEXT_2, 77 SearchManager.SUGGEST_COLUMN_ICON_1, 78 SearchManager.SUGGEST_COLUMN_ICON_2, 79 SearchManager.SUGGEST_COLUMN_QUERY 80 }; 81 82 private HttpClient mHttpClient; 83 84 @Override 85 public boolean onCreate() { 86 mHttpClient = AndroidHttpClient.newInstance(USER_AGENT, getContext()); 87 HttpParams params = mHttpClient.getParams(); 88 params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS); 89 90 // NOTE: Do not look up the resource here; Localization changes may not have completed 91 // yet (e.g. we may still be reading the SIM card). 92 mSuggestUri = null; 93 return true; 94 } 95 96 /** 97 * This will always return {@link SearchManager#SUGGEST_MIME_TYPE} as this 98 * provider is purely to provide suggestions. 99 */ 100 @Override 101 public String getType(Uri uri) { 102 return SearchManager.SUGGEST_MIME_TYPE; 103 } 104 105 /** 106 * Queries for a given search term and returns a cursor containing 107 * suggestions ordered by best match. 108 */ 109 @Override 110 public Cursor query(Uri uri, String[] projection, String selection, 111 String[] selectionArgs, String sortOrder) { 112 String query = getQuery(uri); 113 if (TextUtils.isEmpty(query)) { 114 return null; 115 } 116 if (!isNetworkConnected()) { 117 Log.i(LOG_TAG, "Not connected to network."); 118 return null; 119 } 120 try { 121 query = URLEncoder.encode(query, "UTF-8"); 122 // NOTE: This code uses resources to optionally select the search Uri, based on the 123 // MCC value from the SIM. iThe default string will most likely be fine. It is 124 // paramerterized to accept info from the Locale, the language code is the first 125 // parameter (%1$s) and the country code is the second (%2$s). This code *must* 126 // function in the same way as a similar lookup in 127 // com.android.browser.BrowserActivity#onCreate(). If you change 128 // either of these functions, change them both. (The same is true for the underlying 129 // resource strings, which are stored in mcc-specific xml files.) 130 if (mSuggestUri == null) { 131 Locale l = Locale.getDefault(); 132 String language = l.getLanguage(); 133 String country = l.getCountry().toLowerCase(); 134 // Chinese and Portuguese have two langauge variants. 135 if ("zh".equals(language)) { 136 if ("cn".equals(country)) { 137 language = "zh-CN"; 138 } else if ("tw".equals(country)) { 139 language = "zh-TW"; 140 } 141 } else if ("pt".equals(language)) { 142 if ("br".equals(country)) { 143 language = "pt-BR"; 144 } else if ("pt".equals(country)) { 145 language = "pt-PT"; 146 } 147 } 148 mSuggestUri = getContext().getResources().getString(R.string.google_suggest_base, 149 language, 150 country) 151 + "json=true&q="; 152 } 153 154 String suggestUri = mSuggestUri + query; 155 if (DBG) Log.d(LOG_TAG, "Sending request: " + suggestUri); 156 HttpGet method = new HttpGet(suggestUri); 157 HttpResponse response = mHttpClient.execute(method); 158 if (response.getStatusLine().getStatusCode() == 200) { 159 160 /* Goto http://www.google.com/complete/search?json=true&q=foo 161 * to see what the data format looks like. It's basically a json 162 * array containing 4 other arrays. We only care about the middle 163 * 2 which contain the suggestions and their popularity. 164 */ 165 JSONArray results = new JSONArray(EntityUtils.toString(response.getEntity())); 166 JSONArray suggestions = results.getJSONArray(1); 167 JSONArray popularity = results.getJSONArray(2); 168 if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length() + " results"); 169 return new SuggestionsCursor(suggestions, popularity); 170 } else { 171 if (DBG) Log.d(LOG_TAG, "Request failed " + response.getStatusLine()); 172 } 173 } catch (UnsupportedEncodingException e) { 174 Log.w(LOG_TAG, "Error", e); 175 } catch (IOException e) { 176 Log.w(LOG_TAG, "Error", e); 177 } catch (JSONException e) { 178 Log.w(LOG_TAG, "Error", e); 179 } 180 return null; 181 } 182 183 /** 184 * Gets the search text from a uri. 185 */ 186 private String getQuery(Uri uri) { 187 if (uri.getPathSegments().size() > 1) { 188 return uri.getLastPathSegment(); 189 } else { 190 return ""; 191 } 192 } 193 194 private boolean isNetworkConnected() { 195 NetworkInfo networkInfo = getActiveNetworkInfo(); 196 return networkInfo != null && networkInfo.isConnected(); 197 } 198 199 private NetworkInfo getActiveNetworkInfo() { 200 ConnectivityManager connectivity = 201 (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE); 202 if (connectivity == null) { 203 return null; 204 } 205 return connectivity.getActiveNetworkInfo(); 206 } 207 208 private static class SuggestionsCursor extends AbstractCursor { 209 210 /* Contains the actual suggestions */ 211 final JSONArray mSuggestions; 212 213 /* This contains the popularity of each suggestion 214 * i.e. 165,000 results. It's not related to sorting. 215 */ 216 final JSONArray mPopularity; 217 public SuggestionsCursor(JSONArray suggestions, JSONArray popularity) { 218 mSuggestions = suggestions; 219 mPopularity = popularity; 220 } 221 222 @Override 223 public int getCount() { 224 return mSuggestions.length(); 225 } 226 227 @Override 228 public String[] getColumnNames() { 229 return COLUMNS; 230 } 231 232 @Override 233 public String getString(int column) { 234 if (mPos == -1) return null; 235 try { 236 switch (column) { 237 case COL_ID: 238 return String.valueOf(mPos); 239 case COL_TEXT_1: 240 case COL_QUERY: 241 return mSuggestions.getString(mPos); 242 case COL_TEXT_2: 243 return mPopularity.getString(mPos); 244 case COL_ICON_1: 245 return String.valueOf(R.drawable.magnifying_glass); 246 case COL_ICON_2: 247 return null; 248 default: 249 Log.w(LOG_TAG, "Bad column: " + column); 250 return null; 251 } 252 } catch (JSONException e) { 253 Log.w(LOG_TAG, "Error parsing response: " + e); 254 return null; 255 } 256 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 == COL_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 Uri insert(Uri uri, ContentValues values) { 295 throw new UnsupportedOperationException(); 296 } 297 298 @Override 299 public int update(Uri uri, ContentValues values, String selection, 300 String[] selectionArgs) { 301 throw new UnsupportedOperationException(); 302 } 303 304 @Override 305 public int delete(Uri uri, String selection, String[] selectionArgs) { 306 throw new UnsupportedOperationException(); 307 } 308} 309