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;
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;
44 import java.io.ByteArrayOutputStream;
45 import java.io.IOException;
47 import java.util.ArrayList;
48 import java.util.List;
50 public class SearchBookContentsActivity extends Activity {
52 private static final String TAG = "SearchBookContents";
54 // These return a JSON result which describes if and where the query was found. This API may
55 // break or disappear at any time in the future.
56 private static final String BOOK_SEARCH_URL = "//www.google.com/books?vid=isbn";
57 private static final String BOOK_SEARCH_COMMAND = "&jscmd=SearchWithinVolume2&q=";
59 private NetworkThread mNetworkThread;
61 private EditText mQueryTextView;
62 private Button mQueryButton;
63 private ListView mResultListView;
64 private TextView mHeaderView;
65 private String mUserAgent;
68 public void onCreate(Bundle icicle) {
69 super.onCreate(icicle);
71 // Make sure that expired cookies are removed on launch.
72 CookieSyncManager.createInstance(this);
73 CookieManager.getInstance().removeExpiredCookie();
75 Intent intent = getIntent();
76 if (intent == null || (!intent.getAction().equals(Intents.SearchBookContents.ACTION) &&
77 !intent.getAction().equals(Intents.SearchBookContents.DEPRECATED_ACTION))) {
82 mISBN = intent.getStringExtra(Intents.SearchBookContents.ISBN);
83 setTitle(getString(R.string.sbc_name) + ": ISBN " + mISBN);
85 setContentView(R.layout.search_book_contents);
86 mQueryTextView = (EditText) findViewById(R.id.query_text_view);
88 String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY);
89 if (initialQuery != null && initialQuery.length() > 0) {
90 // Populate the search box but don't trigger the search
91 mQueryTextView.setText(initialQuery);
93 mQueryTextView.setOnKeyListener(mKeyListener);
95 mQueryButton = (Button) findViewById(R.id.query_button);
96 mQueryButton.setOnClickListener(mButtonListener);
98 mResultListView = (ListView) findViewById(R.id.result_list_view);
99 LayoutInflater factory = LayoutInflater.from(this);
100 mHeaderView = (TextView) factory.inflate(R.layout.search_book_contents_header,
101 mResultListView, false);
102 mResultListView.addHeaderView(mHeaderView);
104 mUserAgent = getString(R.string.zxing_user_agent);
108 protected void onResume() {
110 mQueryTextView.selectAll();
114 public void onConfigurationChanged(Configuration config) {
115 // Do nothing, this is to prevent the activity from being restarted when the keyboard opens.
116 super.onConfigurationChanged(config);
119 public Handler mHandler = new Handler() {
120 public void handleMessage(Message message) {
121 switch (message.what) {
122 case R.id.search_book_contents_succeeded:
123 handleSearchResults((JSONObject) message.obj);
126 case R.id.search_book_contents_failed:
128 mHeaderView.setText(R.string.msg_sbc_failed);
134 private void resetForNewQuery() {
135 mNetworkThread = null;
136 mQueryTextView.setEnabled(true);
137 mQueryTextView.selectAll();
138 mQueryButton.setEnabled(true);
141 private Button.OnClickListener mButtonListener = new Button.OnClickListener() {
142 public void onClick(View view) {
147 private View.OnKeyListener mKeyListener = new View.OnKeyListener() {
148 public boolean onKey(View view, int keyCode, KeyEvent event) {
149 if (keyCode == KeyEvent.KEYCODE_ENTER) {
157 private void launchSearch() {
158 if (mNetworkThread == null) {
159 String query = mQueryTextView.getText().toString();
160 if (query != null && query.length() > 0) {
161 mNetworkThread = new NetworkThread(mISBN, query, mHandler);
162 mNetworkThread.start();
163 mHeaderView.setText(R.string.msg_sbc_searching_book);
164 mResultListView.setAdapter(null);
165 mQueryTextView.setEnabled(false);
166 mQueryButton.setEnabled(false);
171 // Currently there is no way to distinguish between a query which had no results and a book
172 // which is not searchable - both return zero results.
173 private void handleSearchResults(JSONObject json) {
175 int count = json.getInt("number_of_results");
176 mHeaderView.setText("Found " + ((count == 1) ? "1 result" : count + " results"));
178 JSONArray results = json.getJSONArray("search_results");
179 SearchBookContentsResult.setQuery(mQueryTextView.getText().toString());
180 List<SearchBookContentsResult> items = new ArrayList<SearchBookContentsResult>(count);
181 for (int x = 0; x < count; x++) {
182 items.add(parseResult(results.getJSONObject(x)));
184 mResultListView.setAdapter(new SearchBookContentsAdapter(this, items));
186 mResultListView.setAdapter(null);
188 } catch (JSONException e) {
189 Log.e(TAG, e.toString());
190 mResultListView.setAdapter(null);
191 mHeaderView.setText(R.string.msg_sbc_failed);
195 // Available fields: page_number, page_id, page_url, snippet_text
196 private SearchBookContentsResult parseResult(JSONObject json) {
198 String pageNumber = json.getString("page_number");
199 if (pageNumber.length() > 0) {
200 pageNumber = getString(R.string.msg_sbc_page) + " " + pageNumber;
202 // This can happen for text on the jacket, and possibly other reasons.
203 pageNumber = getString(R.string.msg_sbc_unknown_page);
206 // Remove all HTML tags and encoded characters. Ideally the server would do this.
207 String snippet = json.optString("snippet_text");
208 boolean valid = true;
209 if (snippet.length() > 0) {
210 snippet = snippet.replaceAll("\\<.*?\\>", "");
211 snippet = snippet.replaceAll("<", "<");
212 snippet = snippet.replaceAll(">", ">");
213 snippet = snippet.replaceAll("'", "'");
214 snippet = snippet.replaceAll(""", "\"");
216 snippet = "(" + getString(R.string.msg_sbc_snippet_unavailable) + ")";
219 return new SearchBookContentsResult(pageNumber, snippet, valid);
220 } catch (JSONException e) {
221 // Never seen in the wild, just being complete.
222 return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", false);
226 private class NetworkThread extends Thread {
228 private String mISBN;
229 private String mQuery;
230 private Handler mHandler;
232 public NetworkThread(String isbn, String query, Handler handler) {
239 String url = BOOK_SEARCH_URL + mISBN + BOOK_SEARCH_COMMAND + mQuery;
240 AndroidHttpClient client = null;
242 URI uri = new URI("http", url, null);
243 HttpGet get = new HttpGet(uri);
244 get.setHeader("cookie", getCookie("http:" + url));
245 client = AndroidHttpClient.newInstance(mUserAgent);
246 HttpResponse response = client.execute(get);
247 if (response.getStatusLine().getStatusCode() == 200) {
248 HttpEntity entity = response.getEntity();
249 ByteArrayOutputStream jsonHolder = new ByteArrayOutputStream();
250 entity.writeTo(jsonHolder);
252 JSONObject json = new JSONObject(jsonHolder.toString(getEncoding(entity)));
255 Message message = Message.obtain(mHandler, R.id.search_book_contents_succeeded);
257 message.sendToTarget();
259 Log.e(TAG, "HTTP returned " + response.getStatusLine().getStatusCode() +
261 Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
262 message.sendToTarget();
264 } catch (Exception e) {
265 Log.e(TAG, e.toString());
266 Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
267 message.sendToTarget();
269 if (client != null) {
275 // Book Search requires a cookie to work, which we store persistently. If the cookie does
276 // not exist, this could be the first search or it has expired. Either way, do a quick HEAD
277 // request to fetch it, save it via the CookieSyncManager to flash, then return it.
278 private String getCookie(String url) {
279 String cookie = CookieManager.getInstance().getCookie(url);
280 if (cookie == null || cookie.length() == 0) {
281 Log.v(TAG, "Book Search cookie was missing or expired");
282 HttpHead head = new HttpHead(url);
283 AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
285 HttpResponse response = client.execute(head);
286 if (response.getStatusLine().getStatusCode() == 200) {
287 Header[] cookies = response.getHeaders("set-cookie");
288 for (int x = 0; x < cookies.length; x++) {
289 CookieManager.getInstance().setCookie(url, cookies[x].getValue());
291 CookieSyncManager.getInstance().sync();
292 cookie = CookieManager.getInstance().getCookie(url);
294 } catch (IOException e) {
295 Log.e(TAG, e.toString());
302 private String getEncoding(HttpEntity entity) {
303 // FIXME: The server is returning ISO-8859-1 but the content is actually windows-1252.
304 // Once Jeff fixes the HTTP response, remove this hardcoded value and go back to getting
305 // the encoding dynamically.
306 return "windows-1252";
307 // HeaderElement[] elements = entity.getContentType().getElements();
308 // if (elements != null && elements.length > 0) {
309 // String encoding = elements[0].getParameterByName("charset").getValue();
310 // if (encoding != null && encoding.length() > 0) {