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 
17 package com.android.quicksearchbox.google;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.net.ConnectivityManager;
22 import android.net.NetworkInfo;
23 import android.net.http.AndroidHttpClient;
24 import android.os.Build;
25 import android.os.Handler;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import com.android.quicksearchbox.Config;
30 import com.android.quicksearchbox.R;
31 import com.android.quicksearchbox.Source;
32 import com.android.quicksearchbox.SourceResult;
33 import com.android.quicksearchbox.SuggestionCursor;
34 import com.android.quicksearchbox.util.NamedTaskExecutor;
35 
36 import org.apache.http.HttpResponse;
37 import org.apache.http.client.HttpClient;
38 import org.apache.http.client.methods.HttpGet;
39 import org.apache.http.params.HttpParams;
40 import org.apache.http.util.EntityUtils;
41 import org.json.JSONArray;
42 import org.json.JSONException;
43 
44 import java.io.IOException;
45 import java.io.UnsupportedEncodingException;
46 import java.net.URLEncoder;
47 import java.util.Locale;
48 
49 /**
50  * Use network-based Google Suggests to provide search suggestions.
51  */
52 public class GoogleSuggestClient extends AbstractGoogleSource {
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/" + Build.VERSION.RELEASE;
58     private String mSuggestUri;
59 
60     // TODO: this should be defined somewhere
61     private static final String HTTP_TIMEOUT = "http.conn-manager.timeout";
62 
63     private final HttpClient mHttpClient;
64 
GoogleSuggestClient(Context context, Handler uiThread, NamedTaskExecutor iconLoader, Config config)65     public GoogleSuggestClient(Context context, Handler uiThread,
66             NamedTaskExecutor iconLoader, Config config) {
67         super(context, uiThread, iconLoader);
68         mHttpClient = AndroidHttpClient.newInstance(USER_AGENT, context);
69         HttpParams params = mHttpClient.getParams();
70         params.setLongParameter(HTTP_TIMEOUT, config.getHttpConnectTimeout());
71 
72         // NOTE:  Do not look up the resource here;  Localization changes may not have completed
73         // yet (e.g. we may still be reading the SIM card).
74         mSuggestUri = null;
75     }
76 
77     @Override
getIntentComponent()78     public ComponentName getIntentComponent() {
79         return new ComponentName(getContext(), GoogleSearch.class);
80     }
81 
82     @Override
queryInternal(String query)83     public SourceResult queryInternal(String query) {
84         return query(query);
85     }
86 
87     @Override
queryExternal(String query)88     public SourceResult queryExternal(String query) {
89         return query(query);
90     }
91 
92     /**
93      * Queries for a given search term and returns a cursor containing
94      * suggestions ordered by best match.
95      */
query(String query)96     private SourceResult query(String query) {
97         if (TextUtils.isEmpty(query)) {
98             return null;
99         }
100         if (!isNetworkConnected()) {
101             Log.i(LOG_TAG, "Not connected to network.");
102             return null;
103         }
104         try {
105             String encodedQuery = URLEncoder.encode(query, "UTF-8");
106             if (mSuggestUri == null) {
107                 Locale l = Locale.getDefault();
108                 String language = GoogleSearch.getLanguage(l);
109                 mSuggestUri = getContext().getResources().getString(R.string.google_suggest_base,
110                                                                     language);
111             }
112 
113             String suggestUri = mSuggestUri + encodedQuery;
114             if (DBG) Log.d(LOG_TAG, "Sending request: " + suggestUri);
115             HttpGet method = new HttpGet(suggestUri);
116             HttpResponse response = mHttpClient.execute(method);
117             if (response.getStatusLine().getStatusCode() == 200) {
118 
119                 /* Goto http://www.google.com/complete/search?json=true&q=foo
120                  * to see what the data format looks like. It's basically a json
121                  * array containing 4 other arrays. We only care about the middle
122                  * 2 which contain the suggestions and their popularity.
123                  */
124                 JSONArray results = new JSONArray(EntityUtils.toString(response.getEntity()));
125                 JSONArray suggestions = results.getJSONArray(1);
126                 JSONArray popularity = results.getJSONArray(2);
127                 if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length() + " results");
128                 return new GoogleSuggestCursor(this, query, suggestions, popularity);
129             } else {
130                 if (DBG) Log.d(LOG_TAG, "Request failed " + response.getStatusLine());
131             }
132         } catch (UnsupportedEncodingException e) {
133             Log.w(LOG_TAG, "Error", e);
134         } catch (IOException e) {
135             Log.w(LOG_TAG, "Error", e);
136         } catch (JSONException e) {
137             Log.w(LOG_TAG, "Error", e);
138         }
139         return null;
140     }
141 
142     @Override
refreshShortcut(String shortcutId, String oldExtraData)143     public SuggestionCursor refreshShortcut(String shortcutId, String oldExtraData) {
144         return null;
145     }
146 
isNetworkConnected()147     private boolean isNetworkConnected() {
148         NetworkInfo networkInfo = getActiveNetworkInfo();
149         return networkInfo != null && networkInfo.isConnected();
150     }
151 
getActiveNetworkInfo()152     private NetworkInfo getActiveNetworkInfo() {
153         ConnectivityManager connectivity =
154                 (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
155         if (connectivity == null) {
156             return null;
157         }
158         return connectivity.getActiveNetworkInfo();
159     }
160 
161     private static class GoogleSuggestCursor extends AbstractGoogleSourceResult {
162 
163         /* Contains the actual suggestions */
164         private final JSONArray mSuggestions;
165 
166         /* This contains the popularity of each suggestion
167          * i.e. 165,000 results. It's not related to sorting.
168          */
169         private final JSONArray mPopularity;
170 
GoogleSuggestCursor(Source source, String userQuery, JSONArray suggestions, JSONArray popularity)171         public GoogleSuggestCursor(Source source, String userQuery,
172                 JSONArray suggestions, JSONArray popularity) {
173             super(source, userQuery);
174             mSuggestions = suggestions;
175             mPopularity = popularity;
176         }
177 
178         @Override
getCount()179         public int getCount() {
180             return mSuggestions.length();
181         }
182 
183         @Override
getSuggestionQuery()184         public String getSuggestionQuery() {
185             try {
186                 return mSuggestions.getString(getPosition());
187             } catch (JSONException e) {
188                 Log.w(LOG_TAG, "Error parsing response: " + e);
189                 return null;
190             }
191         }
192 
193         @Override
getSuggestionText2()194         public String getSuggestionText2() {
195             try {
196                 return mPopularity.getString(getPosition());
197             } catch (JSONException e) {
198                 Log.w(LOG_TAG, "Error parsing response: " + e);
199                 return null;
200             }
201         }
202     }
203 }
204