GoogleSuggestionProvider.java revision 8e5c87f8793bcfbfba354e962a545952af365cc7
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 = false;
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.conn-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