Changed the ChartServer and Search Book Contents URLs back to google.com instead...
[zxing.git] / android / src / com / google / zxing / client / android / SearchBookContentsActivity.java
1 /*
2  * Copyright (C) 2008 ZXing authors
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.google.zxing.client.android;
18
19 import android.app.Activity;
20 import android.content.Intent;
21 import android.content.res.Configuration;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.os.Message;
25 import android.util.Log;
26 import android.view.KeyEvent;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.webkit.CookieManager;
30 import android.webkit.CookieSyncManager;
31 import android.widget.Button;
32 import android.widget.EditText;
33 import android.widget.ListView;
34 import android.widget.TextView;
35 import org.apache.http.Header;
36 import org.apache.http.HttpEntity;
37 import org.apache.http.HttpResponse;
38 import org.apache.http.client.methods.HttpGet;
39 import org.apache.http.client.methods.HttpHead;
40 import org.json.JSONArray;
41 import org.json.JSONException;
42 import org.json.JSONObject;
43
44 import java.io.ByteArrayOutputStream;
45 import java.io.IOException;
46 import java.net.URI;
47 import java.net.URLEncoder;
48 import java.util.ArrayList;
49 import java.util.List;
50
51 public final class SearchBookContentsActivity extends Activity {
52
53   private static final String TAG = "SearchBookContents";
54
55   // These return a JSON result which describes if and where the query was found. This API may
56   // break or disappear at any time in the future. Since this is an API call rather than a website,
57   // we don't use LocaleManager to change the TLD.
58   private static final String BOOK_SEARCH_URL = "//www.google.com/books?vid=isbn";
59   private static final String BOOK_SEARCH_COMMAND = "&jscmd=SearchWithinVolume2&q=";
60
61   private NetworkThread mNetworkThread;
62   private String mISBN;
63   private EditText mQueryTextView;
64   private Button mQueryButton;
65   private ListView mResultListView;
66   private TextView mHeaderView;
67   private String mUserAgent;
68
69   @Override
70   public void onCreate(Bundle icicle) {
71     super.onCreate(icicle);
72
73     // Make sure that expired cookies are removed on launch.
74     CookieSyncManager.createInstance(this);
75     CookieManager.getInstance().removeExpiredCookie();
76
77     Intent intent = getIntent();
78     if (intent == null || (!intent.getAction().equals(Intents.SearchBookContents.ACTION) &&
79         !intent.getAction().equals(Intents.SearchBookContents.DEPRECATED_ACTION))) {
80       finish();
81       return;
82     }
83
84     mISBN = intent.getStringExtra(Intents.SearchBookContents.ISBN);
85     setTitle(getString(R.string.sbc_name) + ": ISBN " + mISBN);
86
87     setContentView(R.layout.search_book_contents);
88     mQueryTextView = (EditText) findViewById(R.id.query_text_view);
89
90     String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY);
91     if (initialQuery != null && initialQuery.length() > 0) {
92       // Populate the search box but don't trigger the search
93       mQueryTextView.setText(initialQuery);
94     }
95     mQueryTextView.setOnKeyListener(mKeyListener);
96
97     mQueryButton = (Button) findViewById(R.id.query_button);
98     mQueryButton.setOnClickListener(mButtonListener);
99
100     mResultListView = (ListView) findViewById(R.id.result_list_view);
101     LayoutInflater factory = LayoutInflater.from(this);
102     mHeaderView = (TextView) factory.inflate(R.layout.search_book_contents_header,
103         mResultListView, false);
104     mResultListView.addHeaderView(mHeaderView);
105
106     mUserAgent = getString(R.string.zxing_user_agent);
107   }
108
109   @Override
110   protected void onResume() {
111     super.onResume();
112     mQueryTextView.selectAll();
113   }
114
115   @Override
116   public void onConfigurationChanged(Configuration config) {
117     // Do nothing, this is to prevent the activity from being restarted when the keyboard opens.
118     super.onConfigurationChanged(config);
119   }
120
121   public final Handler mHandler = new Handler() {
122     public void handleMessage(Message message) {
123       switch (message.what) {
124         case R.id.search_book_contents_succeeded:
125           handleSearchResults((JSONObject) message.obj);
126           resetForNewQuery();
127           break;
128         case R.id.search_book_contents_failed:
129           resetForNewQuery();
130           mHeaderView.setText(R.string.msg_sbc_failed);
131           break;
132       }
133     }
134   };
135
136   private void resetForNewQuery() {
137     mNetworkThread = null;
138     mQueryTextView.setEnabled(true);
139     mQueryTextView.selectAll();
140     mQueryButton.setEnabled(true);
141   }
142
143   private final Button.OnClickListener mButtonListener = new Button.OnClickListener() {
144     public void onClick(View view) {
145       launchSearch();
146     }
147   };
148
149   private final View.OnKeyListener mKeyListener = new View.OnKeyListener() {
150     public boolean onKey(View view, int keyCode, KeyEvent event) {
151       if (keyCode == KeyEvent.KEYCODE_ENTER) {
152         launchSearch();
153         return true;
154       }
155       return false;
156     }
157   };
158
159   private void launchSearch() {
160     if (mNetworkThread == null) {
161       String query = mQueryTextView.getText().toString();
162       if (query != null && query.length() > 0) {
163         mNetworkThread = new NetworkThread(mISBN, query, mHandler, mUserAgent);
164         mNetworkThread.start();
165         mHeaderView.setText(R.string.msg_sbc_searching_book);
166         mResultListView.setAdapter(null);
167         mQueryTextView.setEnabled(false);
168         mQueryButton.setEnabled(false);
169       }
170     }
171   }
172
173   // Currently there is no way to distinguish between a query which had no results and a book
174   // which is not searchable - both return zero results.
175   private void handleSearchResults(JSONObject json) {
176     try {
177       int count = json.getInt("number_of_results");
178       mHeaderView.setText("Found " + ((count == 1) ? "1 result" : count + " results"));
179       if (count > 0) {
180         JSONArray results = json.getJSONArray("search_results");
181         SearchBookContentsResult.setQuery(mQueryTextView.getText().toString());
182         List<SearchBookContentsResult> items = new ArrayList<SearchBookContentsResult>(count);
183         for (int x = 0; x < count; x++) {
184           items.add(parseResult(results.getJSONObject(x)));
185         }
186         mResultListView.setAdapter(new SearchBookContentsAdapter(this, items));
187       } else {
188         mResultListView.setAdapter(null);
189       }
190     } catch (JSONException e) {
191       Log.e(TAG, e.toString());
192       mResultListView.setAdapter(null);
193       mHeaderView.setText(R.string.msg_sbc_failed);
194     }
195   }
196
197   // Available fields: page_number, page_id, page_url, snippet_text
198   private SearchBookContentsResult parseResult(JSONObject json) {
199     try {
200       String pageNumber = json.getString("page_number");
201       if (pageNumber.length() > 0) {
202         pageNumber = getString(R.string.msg_sbc_page) + " " + pageNumber;
203       } else {
204         // This can happen for text on the jacket, and possibly other reasons.
205         pageNumber = getString(R.string.msg_sbc_unknown_page);
206       }
207
208       // Remove all HTML tags and encoded characters. Ideally the server would do this.
209       String snippet = json.optString("snippet_text");
210       boolean valid = true;
211       if (snippet.length() > 0) {
212         snippet = snippet.replaceAll("\\<.*?\\>", "");
213         snippet = snippet.replaceAll("&lt;", "<");
214         snippet = snippet.replaceAll("&gt;", ">");
215         snippet = snippet.replaceAll("&#39;", "'");
216         snippet = snippet.replaceAll("&quot;", "\"");
217       } else {
218         snippet = "(" + getString(R.string.msg_sbc_snippet_unavailable) + ")";
219         valid = false;
220       }
221       return new SearchBookContentsResult(pageNumber, snippet, valid);
222     } catch (JSONException e) {
223       // Never seen in the wild, just being complete.
224       return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", false);
225     }
226   }
227
228   private static class NetworkThread extends Thread {
229
230     private final String mISBN;
231     private final String mQuery;
232     private final Handler mHandler;
233     private String mUserAgent;
234
235     public NetworkThread(String isbn, String query, Handler handler, String userAgent) {
236       mISBN = isbn;
237       mQuery = query;
238       mHandler = handler;
239       mUserAgent = userAgent;
240     }
241
242     public void run() {
243       AndroidHttpClient client = null;
244       try {
245         String url = BOOK_SEARCH_URL + mISBN + BOOK_SEARCH_COMMAND + URLEncoder.encode(mQuery, "UTF8");
246         URI uri = new URI("http", url, null);
247         HttpGet get = new HttpGet(uri);
248         get.setHeader("cookie", getCookie("http:" + url));
249         client = AndroidHttpClient.newInstance(mUserAgent);
250         HttpResponse response = client.execute(get);
251         if (response.getStatusLine().getStatusCode() == 200) {
252           HttpEntity entity = response.getEntity();
253           ByteArrayOutputStream jsonHolder = new ByteArrayOutputStream();
254           entity.writeTo(jsonHolder);
255           jsonHolder.flush();
256           JSONObject json = new JSONObject(jsonHolder.toString(getEncoding(entity)));
257           jsonHolder.close();
258
259           Message message = Message.obtain(mHandler, R.id.search_book_contents_succeeded);
260           message.obj = json;
261           message.sendToTarget();
262         } else {
263           Log.e(TAG, "HTTP returned " + response.getStatusLine().getStatusCode() +
264               " for http:" + url);
265           Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
266           message.sendToTarget();
267         }
268       } catch (Exception e) {
269         Log.e(TAG, e.toString());
270         Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
271         message.sendToTarget();
272       } finally {
273         if (client != null) {
274           client.close();
275         }
276       }
277     }
278
279     // Book Search requires a cookie to work, which we store persistently. If the cookie does
280     // not exist, this could be the first search or it has expired. Either way, do a quick HEAD
281     // request to fetch it, save it via the CookieSyncManager to flash, then return it.
282     private String getCookie(String url) {
283       String cookie = CookieManager.getInstance().getCookie(url);
284       if (cookie == null || cookie.length() == 0) {
285         Log.v(TAG, "Book Search cookie was missing or expired");
286         HttpHead head = new HttpHead(url);
287         AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
288         try {
289           HttpResponse response = client.execute(head);
290           if (response.getStatusLine().getStatusCode() == 200) {
291             Header[] cookies = response.getHeaders("set-cookie");
292             for (int x = 0; x < cookies.length; x++) {
293               CookieManager.getInstance().setCookie(url, cookies[x].getValue());
294             }
295             CookieSyncManager.getInstance().sync();
296             cookie = CookieManager.getInstance().getCookie(url);
297           }
298         } catch (IOException e) {
299           Log.e(TAG, e.toString());
300         }
301         client.close();
302       }
303       return cookie;
304     }
305
306     private String getEncoding(HttpEntity entity) {
307       // FIXME: The server is returning ISO-8859-1 but the content is actually windows-1252.
308       // Once Jeff fixes the HTTP response, remove this hardcoded value and go back to getting
309       // the encoding dynamically.
310       return "windows-1252";
311 //            HeaderElement[] elements = entity.getContentType().getElements();
312 //            if (elements != null && elements.length > 0) {
313 //                String encoding = elements[0].getParameterByName("charset").getValue();
314 //                if (encoding != null && encoding.length() > 0) {
315 //                    return encoding;
316 //                }
317 //            }
318 //            return "UTF-8";
319     }
320   }
321
322 }