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