The new Android client, featuring:
[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.util.ArrayList;
48 import java.util.List;
49
50 public class SearchBookContentsActivity extends Activity {
51
52   private static final String TAG = "SearchBookContents";
53
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=";
58
59   private NetworkThread mNetworkThread;
60   private String mISBN;
61   private EditText mQueryTextView;
62   private Button mQueryButton;
63   private ListView mResultListView;
64   private TextView mHeaderView;
65   private String mUserAgent;
66
67   @Override
68   public void onCreate(Bundle icicle) {
69     super.onCreate(icicle);
70
71     // Make sure that expired cookies are removed on launch.
72     CookieSyncManager.createInstance(this);
73     CookieManager.getInstance().removeExpiredCookie();
74
75     Intent intent = getIntent();
76     if (intent == null || (!intent.getAction().equals(Intents.SearchBookContents.ACTION) &&
77         !intent.getAction().equals(Intents.SearchBookContents.DEPRECATED_ACTION))) {
78       finish();
79       return;
80     }
81
82     mISBN = intent.getStringExtra(Intents.SearchBookContents.ISBN);
83     setTitle(getString(R.string.sbc_name) + ": ISBN " + mISBN);
84
85     setContentView(R.layout.search_book_contents);
86     mQueryTextView = (EditText) findViewById(R.id.query_text_view);
87
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);
92     }
93     mQueryTextView.setOnKeyListener(mKeyListener);
94
95     mQueryButton = (Button) findViewById(R.id.query_button);
96     mQueryButton.setOnClickListener(mButtonListener);
97
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);
103
104     mUserAgent = getString(R.string.zxing_user_agent);
105   }
106
107   @Override
108   protected void onResume() {
109     super.onResume();
110     mQueryTextView.selectAll();
111   }
112
113   @Override
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);
117   }
118
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);
124           resetForNewQuery();
125           break;
126         case R.id.search_book_contents_failed:
127           resetForNewQuery();
128           mHeaderView.setText(R.string.msg_sbc_failed);
129           break;
130       }
131     }
132   };
133
134   private void resetForNewQuery() {
135     mNetworkThread = null;
136     mQueryTextView.setEnabled(true);
137     mQueryTextView.selectAll();
138     mQueryButton.setEnabled(true);
139   }
140
141   private Button.OnClickListener mButtonListener = new Button.OnClickListener() {
142     public void onClick(View view) {
143       launchSearch();
144     }
145   };
146
147   private View.OnKeyListener mKeyListener = new View.OnKeyListener() {
148     public boolean onKey(View view, int keyCode, KeyEvent event) {
149       if (keyCode == KeyEvent.KEYCODE_ENTER) {
150         launchSearch();
151         return true;
152       }
153       return false;
154     }
155   };
156
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_searching_book);
164         mResultListView.setAdapter(null);
165         mQueryTextView.setEnabled(false);
166         mQueryButton.setEnabled(false);
167       }
168     }
169   }
170
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) {
174     try {
175       int count = json.getInt("number_of_results");
176       mHeaderView.setText("Found " + ((count == 1) ? "1 result" : count + " results"));
177       if (count > 0) {
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)));
183         }
184         mResultListView.setAdapter(new SearchBookContentsAdapter(this, items));
185       } else {
186         mResultListView.setAdapter(null);
187       }
188     } catch (JSONException e) {
189       Log.e(TAG, e.toString());
190       mResultListView.setAdapter(null);
191       mHeaderView.setText(R.string.msg_sbc_failed);
192     }
193   }
194
195   // Available fields: page_number, page_id, page_url, snippet_text
196   private SearchBookContentsResult parseResult(JSONObject json) {
197     try {
198       String pageNumber = json.getString("page_number");
199       if (pageNumber.length() > 0) {
200         pageNumber = getString(R.string.msg_sbc_page) + " " + pageNumber;
201       } else {
202         // This can happen for text on the jacket, and possibly other reasons.
203         pageNumber = getString(R.string.msg_sbc_unknown_page);
204       }
205
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("&lt;", "<");
212         snippet = snippet.replaceAll("&gt;", ">");
213         snippet = snippet.replaceAll("&#39;", "'");
214         snippet = snippet.replaceAll("&quot;", "\"");
215       } else {
216         snippet = "(" + getString(R.string.msg_sbc_snippet_unavailable) + ")";
217         valid = false;
218       }
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);
223     }
224   }
225
226   private class NetworkThread extends Thread {
227
228     private String mISBN;
229     private String mQuery;
230     private Handler mHandler;
231
232     public NetworkThread(String isbn, String query, Handler handler) {
233       mISBN = isbn;
234       mQuery = query;
235       mHandler = handler;
236     }
237
238     public void run() {
239       String url = BOOK_SEARCH_URL + mISBN + BOOK_SEARCH_COMMAND + mQuery;
240       AndroidHttpClient client = null;
241       try {
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);
251           jsonHolder.flush();
252           JSONObject json = new JSONObject(jsonHolder.toString(getEncoding(entity)));
253           jsonHolder.close();
254
255           Message message = Message.obtain(mHandler, R.id.search_book_contents_succeeded);
256           message.obj = json;
257           message.sendToTarget();
258         } else {
259           Log.e(TAG, "HTTP returned " + response.getStatusLine().getStatusCode() +
260               " for http:" + url);
261           Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
262           message.sendToTarget();
263         }
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();
268       } finally {
269         if (client != null) {
270           client.close();
271         }
272       }
273     }
274
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);
284         try {
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());
290             }
291             CookieSyncManager.getInstance().sync();
292             cookie = CookieManager.getInstance().getCookie(url);
293           }
294         } catch (IOException e) {
295           Log.e(TAG, e.toString());
296         }
297       }
298       return cookie;
299     }
300
301     private String getEncoding(HttpEntity entity) {
302       // FIXME: The server is returning ISO-8859-1 but the content is actually windows-1252.
303       // Once Jeff fixes the HTTP response, remove this hardcoded value and go back to getting
304       // the encoding dynamically.
305       return "windows-1252";
306 //            HeaderElement[] elements = entity.getContentType().getElements();
307 //            if (elements != null && elements.length > 0) {
308 //                String encoding = elements[0].getParameterByName("charset").getValue();
309 //                if (encoding != null && encoding.length() > 0) {
310 //                    return encoding;
311 //                }
312 //            }
313 //            return "UTF-8";
314     }
315   }
316
317 }