2 * Copyright (C) 2008 ZXing authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.zxing.client.android.book;
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.apache.http.client.methods.HttpUriRequest;
41 import org.json.JSONArray;
42 import org.json.JSONException;
43 import org.json.JSONObject;
45 import java.io.ByteArrayOutputStream;
46 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.regex.Pattern;
52 import com.google.zxing.client.android.R;
53 import com.google.zxing.client.android.Intents;
54 import com.google.zxing.client.android.AndroidHttpClient;
57 * Uses Google Book Search to find a word or phrase in the requested book.
59 * @author dswitkin@google.com (Daniel Switkin)
61 public final class SearchBookContentsActivity extends Activity {
63 private static final String TAG = SearchBookContentsActivity.class.getSimpleName();
65 private static final String USER_AGENT = "ZXing (Android)";
66 private static final Pattern TAG_PATTERN = Pattern.compile("\\<.*?\\>");
67 private static final Pattern LT_ENTITY_PATTERN = Pattern.compile("<");
68 private static final Pattern GT_ENTITY_PATTERN = Pattern.compile(">");
69 private static final Pattern QUOTE_ENTITY_PATTERN = Pattern.compile("'");
70 private static final Pattern QUOT_ENTITY_PATTERN = Pattern.compile(""");
72 private NetworkThread networkThread;
74 private EditText queryTextView;
75 private Button queryButton;
76 private ListView resultListView;
77 private TextView headerView;
79 private final Handler handler = new Handler() {
81 public void handleMessage(Message message) {
82 switch (message.what) {
83 case R.id.search_book_contents_succeeded:
84 handleSearchResults((JSONObject) message.obj);
87 case R.id.search_book_contents_failed:
89 headerView.setText(R.string.msg_sbc_failed);
95 private final Button.OnClickListener buttonListener = new Button.OnClickListener() {
96 public void onClick(View view) {
101 private final View.OnKeyListener keyListener = new View.OnKeyListener() {
102 public boolean onKey(View view, int keyCode, KeyEvent event) {
103 if (keyCode == KeyEvent.KEYCODE_ENTER) {
116 public void onCreate(Bundle icicle) {
117 super.onCreate(icicle);
119 // Make sure that expired cookies are removed on launch.
120 CookieSyncManager.createInstance(this);
121 CookieManager.getInstance().removeExpiredCookie();
123 Intent intent = getIntent();
124 if (intent == null || (!intent.getAction().equals(Intents.SearchBookContents.ACTION))) {
129 isbn = intent.getStringExtra(Intents.SearchBookContents.ISBN);
130 if (isbn.startsWith("http://google.com/books?id=")) {
131 setTitle(getString(R.string.sbc_name));
133 setTitle(getString(R.string.sbc_name) + ": ISBN " + isbn);
136 setContentView(R.layout.search_book_contents);
137 queryTextView = (EditText) findViewById(R.id.query_text_view);
139 String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY);
140 if (initialQuery != null && initialQuery.length() > 0) {
141 // Populate the search box but don't trigger the search
142 queryTextView.setText(initialQuery);
144 queryTextView.setOnKeyListener(keyListener);
146 queryButton = (Button) findViewById(R.id.query_button);
147 queryButton.setOnClickListener(buttonListener);
149 resultListView = (ListView) findViewById(R.id.result_list_view);
150 LayoutInflater factory = LayoutInflater.from(this);
151 headerView = (TextView) factory.inflate(R.layout.search_book_contents_header,
152 resultListView, false);
153 resultListView.addHeaderView(headerView);
157 protected void onResume() {
159 queryTextView.selectAll();
163 public void onConfigurationChanged(Configuration config) {
164 // Do nothing, this is to prevent the activity from being restarted when the keyboard opens.
165 super.onConfigurationChanged(config);
168 private void resetForNewQuery() {
169 networkThread = null;
170 queryTextView.setEnabled(true);
171 queryTextView.selectAll();
172 queryButton.setEnabled(true);
175 private void launchSearch() {
176 if (networkThread == null) {
177 String query = queryTextView.getText().toString();
178 if (query != null && query.length() > 0) {
179 networkThread = new NetworkThread(isbn, query, handler);
180 networkThread.start();
181 headerView.setText(R.string.msg_sbc_searching_book);
182 resultListView.setAdapter(null);
183 queryTextView.setEnabled(false);
184 queryButton.setEnabled(false);
189 // Currently there is no way to distinguish between a query which had no results and a book
190 // which is not searchable - both return zero results.
191 private void handleSearchResults(JSONObject json) {
193 int count = json.getInt("number_of_results");
194 headerView.setText("Found " + (count == 1 ? "1 result" : count + " results"));
196 JSONArray results = json.getJSONArray("search_results");
197 SearchBookContentsResult.setQuery(queryTextView.getText().toString());
198 List<SearchBookContentsResult> items = new ArrayList<SearchBookContentsResult>(count);
199 for (int x = 0; x < count; x++) {
200 items.add(parseResult(results.getJSONObject(x)));
202 resultListView.setOnItemClickListener(new BrowseBookListener(this, items));
203 resultListView.setAdapter(new SearchBookContentsAdapter(this, items));
205 String searchable = json.optString("searchable");
206 if ("false".equals(searchable)) {
207 headerView.setText(R.string.msg_sbc_book_not_searchable);
209 resultListView.setAdapter(null);
211 } catch (JSONException e) {
212 Log.w(TAG, "Bad JSON from book search", e);
213 resultListView.setAdapter(null);
214 headerView.setText(R.string.msg_sbc_failed);
218 // Available fields: page_id, page_number, page_url, snippet_text
219 private SearchBookContentsResult parseResult(JSONObject json) {
221 String pageId = json.getString("page_id");
222 String pageNumber = json.getString("page_number");
223 if (pageNumber.length() > 0) {
224 pageNumber = getString(R.string.msg_sbc_page) + ' ' + pageNumber;
226 // This can happen for text on the jacket, and possibly other reasons.
227 pageNumber = getString(R.string.msg_sbc_unknown_page);
230 // Remove all HTML tags and encoded characters. Ideally the server would do this.
231 String snippet = json.optString("snippet_text");
232 boolean valid = true;
233 if (snippet.length() > 0) {
234 snippet = TAG_PATTERN.matcher(snippet).replaceAll("");
235 snippet = LT_ENTITY_PATTERN.matcher(snippet).replaceAll("<");
236 snippet = GT_ENTITY_PATTERN.matcher(snippet).replaceAll(">");
237 snippet = QUOTE_ENTITY_PATTERN.matcher(snippet).replaceAll("'");
238 snippet = QUOT_ENTITY_PATTERN.matcher(snippet).replaceAll("\"");
240 snippet = '(' + getString(R.string.msg_sbc_snippet_unavailable) + ')';
243 return new SearchBookContentsResult(pageId, pageNumber, snippet, valid);
244 } catch (JSONException e) {
245 // Never seen in the wild, just being complete.
246 return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", "", false);
250 private static final class NetworkThread extends Thread {
251 private final String isbn;
252 private final String query;
253 private final Handler handler;
255 NetworkThread(String isbn, String query, Handler handler) {
258 this.handler = handler;
263 AndroidHttpClient client = null;
265 // These return a JSON result which describes if and where the query was found. This API may
266 // break or disappear at any time in the future. Since this is an API call rather than a
267 // website, we don't use LocaleManager to change the TLD.
269 if (isbn.startsWith("http://google.com/books?id=")) {
270 int equals = isbn.indexOf('=');
271 String volumeId = isbn.substring(equals + 1);
272 uri = new URI("http", null, "www.google.com", -1, "/books", "id=" + volumeId +
273 "&jscmd=SearchWithinVolume2&q=" + query, null);
275 uri = new URI("http", null, "www.google.com", -1, "/books", "vid=isbn" + isbn +
276 "&jscmd=SearchWithinVolume2&q=" + query, null);
278 HttpUriRequest get = new HttpGet(uri);
279 get.setHeader("cookie", getCookie(uri.toString()));
280 client = AndroidHttpClient.newInstance(USER_AGENT);
281 HttpResponse response = client.execute(get);
282 if (response.getStatusLine().getStatusCode() == 200) {
283 HttpEntity entity = response.getEntity();
284 ByteArrayOutputStream jsonHolder = new ByteArrayOutputStream();
285 entity.writeTo(jsonHolder);
287 JSONObject json = new JSONObject(jsonHolder.toString(getEncoding(entity)));
290 Message message = Message.obtain(handler, R.id.search_book_contents_succeeded);
292 message.sendToTarget();
294 Log.w(TAG, "HTTP returned " + response.getStatusLine().getStatusCode() + " for " + uri);
295 Message message = Message.obtain(handler, R.id.search_book_contents_failed);
296 message.sendToTarget();
298 } catch (Exception e) {
299 Log.w(TAG, "Error accessing book search", e);
300 Message message = Message.obtain(handler, R.id.search_book_contents_failed);
301 message.sendToTarget();
303 if (client != null) {
309 // Book Search requires a cookie to work, which we store persistently. If the cookie does
310 // not exist, this could be the first search or it has expired. Either way, do a quick HEAD
311 // request to fetch it, save it via the CookieSyncManager to flash, then return it.
312 private static String getCookie(String url) {
313 String cookie = CookieManager.getInstance().getCookie(url);
314 if (cookie == null || cookie.length() == 0) {
315 Log.d(TAG, "Book Search cookie was missing or expired");
316 HttpUriRequest head = new HttpHead(url);
317 AndroidHttpClient client = AndroidHttpClient.newInstance(USER_AGENT);
319 HttpResponse response = client.execute(head);
320 if (response.getStatusLine().getStatusCode() == 200) {
321 Header[] cookies = response.getHeaders("set-cookie");
322 for (Header theCookie : cookies) {
323 CookieManager.getInstance().setCookie(url, theCookie.getValue());
325 CookieSyncManager.getInstance().sync();
326 cookie = CookieManager.getInstance().getCookie(url);
328 } catch (IOException e) {
329 Log.w(TAG, "Error setting book search cookie", e);
336 private static String getEncoding(HttpEntity entity) {
337 // FIXME: The server is returning ISO-8859-1 but the content is actually windows-1252.
338 // Once Jeff fixes the HTTP response, remove this hardcoded value and go back to getting
339 // the encoding dynamically.
340 return "windows-1252";
341 // HeaderElement[] elements = entity.getContentType().getElements();
342 // if (elements != null && elements.length > 0) {
343 // String encoding = elements[0].getParameterByName("charset").getValue();
344 // if (encoding != null && encoding.length() > 0) {