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