Appear to have fixed the URL escaping issues this time without regression
[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
55   private NetworkThread mNetworkThread;
56   private String mISBN;
57   private EditText mQueryTextView;
58   private Button mQueryButton;
59   private ListView mResultListView;
60   private TextView mHeaderView;
61   private String mUserAgent;
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     mUserAgent = getString(R.string.zxing_user_agent);
101   }
102
103   @Override
104   protected void onResume() {
105     super.onResume();
106     mQueryTextView.selectAll();
107   }
108
109   @Override
110   public void onConfigurationChanged(Configuration config) {
111     // Do nothing, this is to prevent the activity from being restarted when the keyboard opens.
112     super.onConfigurationChanged(config);
113   }
114
115   public final Handler mHandler = new Handler() {
116     public void handleMessage(Message message) {
117       switch (message.what) {
118         case R.id.search_book_contents_succeeded:
119           handleSearchResults((JSONObject) message.obj);
120           resetForNewQuery();
121           break;
122         case R.id.search_book_contents_failed:
123           resetForNewQuery();
124           mHeaderView.setText(R.string.msg_sbc_failed);
125           break;
126       }
127     }
128   };
129
130   private void resetForNewQuery() {
131     mNetworkThread = null;
132     mQueryTextView.setEnabled(true);
133     mQueryTextView.selectAll();
134     mQueryButton.setEnabled(true);
135   }
136
137   private final Button.OnClickListener mButtonListener = new Button.OnClickListener() {
138     public void onClick(View view) {
139       launchSearch();
140     }
141   };
142
143   private final View.OnKeyListener mKeyListener = new View.OnKeyListener() {
144     public boolean onKey(View view, int keyCode, KeyEvent event) {
145       if (keyCode == KeyEvent.KEYCODE_ENTER) {
146         launchSearch();
147         return true;
148       }
149       return false;
150     }
151   };
152
153   private void launchSearch() {
154     if (mNetworkThread == null) {
155       String query = mQueryTextView.getText().toString();
156       if (query != null && query.length() > 0) {
157         mNetworkThread = new NetworkThread(mISBN, query, mHandler, mUserAgent);
158         mNetworkThread.start();
159         mHeaderView.setText(R.string.msg_sbc_searching_book);
160         mResultListView.setAdapter(null);
161         mQueryTextView.setEnabled(false);
162         mQueryButton.setEnabled(false);
163       }
164     }
165   }
166
167   // Currently there is no way to distinguish between a query which had no results and a book
168   // which is not searchable - both return zero results.
169   private void handleSearchResults(JSONObject json) {
170     try {
171       int count = json.getInt("number_of_results");
172       mHeaderView.setText("Found " + ((count == 1) ? "1 result" : count + " results"));
173       if (count > 0) {
174         JSONArray results = json.getJSONArray("search_results");
175         SearchBookContentsResult.setQuery(mQueryTextView.getText().toString());
176         List<SearchBookContentsResult> items = new ArrayList<SearchBookContentsResult>(count);
177         for (int x = 0; x < count; x++) {
178           items.add(parseResult(results.getJSONObject(x)));
179         }
180         mResultListView.setAdapter(new SearchBookContentsAdapter(this, items));
181       } else {
182         mResultListView.setAdapter(null);
183       }
184     } catch (JSONException e) {
185       Log.e(TAG, e.toString());
186       mResultListView.setAdapter(null);
187       mHeaderView.setText(R.string.msg_sbc_failed);
188     }
189   }
190
191   // Available fields: page_number, page_id, page_url, snippet_text
192   private SearchBookContentsResult parseResult(JSONObject json) {
193     try {
194       String pageNumber = json.getString("page_number");
195       if (pageNumber.length() > 0) {
196         pageNumber = getString(R.string.msg_sbc_page) + " " + pageNumber;
197       } else {
198         // This can happen for text on the jacket, and possibly other reasons.
199         pageNumber = getString(R.string.msg_sbc_unknown_page);
200       }
201
202       // Remove all HTML tags and encoded characters. Ideally the server would do this.
203       String snippet = json.optString("snippet_text");
204       boolean valid = true;
205       if (snippet.length() > 0) {
206         snippet = snippet.replaceAll("\\<.*?\\>", "");
207         snippet = snippet.replaceAll("&lt;", "<");
208         snippet = snippet.replaceAll("&gt;", ">");
209         snippet = snippet.replaceAll("&#39;", "'");
210         snippet = snippet.replaceAll("&quot;", "\"");
211       } else {
212         snippet = "(" + getString(R.string.msg_sbc_snippet_unavailable) + ")";
213         valid = false;
214       }
215       return new SearchBookContentsResult(pageNumber, snippet, valid);
216     } catch (JSONException e) {
217       // Never seen in the wild, just being complete.
218       return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", false);
219     }
220   }
221
222   private static final class NetworkThread extends Thread {
223
224     private final String mISBN;
225     private final String mQuery;
226     private final Handler mHandler;
227     private final String mUserAgent;
228
229     public NetworkThread(String isbn, String query, Handler handler, String userAgent) {
230       mISBN = isbn;
231       mQuery = query;
232       mHandler = handler;
233       mUserAgent = userAgent;
234     }
235
236     public final void run() {
237       AndroidHttpClient client = null;
238       try {
239         // These return a JSON result which describes if and where the query was found. This API may
240         // break or disappear at any time in the future. Since this is an API call rather than a website,
241         // we don't use LocaleManager to change the TLD.
242         URI uri = new URI("http", null, "www.google.com", -1, "/books", "vid=isbn" + mISBN + "&jscmd=SearchWithinVolume2&q=" + mQuery, null);
243         HttpUriRequest get = new HttpGet(uri);
244         get.setHeader("cookie", getCookie(uri.toString()));
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() + " for " + uri);
260           Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
261           message.sendToTarget();
262         }
263       } catch (Exception e) {
264         Log.e(TAG, e.toString());
265         Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
266         message.sendToTarget();
267       } finally {
268         if (client != null) {
269           client.close();
270         }
271       }
272     }
273
274     // Book Search requires a cookie to work, which we store persistently. If the cookie does
275     // not exist, this could be the first search or it has expired. Either way, do a quick HEAD
276     // request to fetch it, save it via the CookieSyncManager to flash, then return it.
277     private String getCookie(String url) {
278       String cookie = CookieManager.getInstance().getCookie(url);
279       if (cookie == null || cookie.length() == 0) {
280         Log.v(TAG, "Book Search cookie was missing or expired");
281         HttpUriRequest head = new HttpHead(url);
282         AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
283         try {
284           HttpResponse response = client.execute(head);
285           if (response.getStatusLine().getStatusCode() == 200) {
286             Header[] cookies = response.getHeaders("set-cookie");
287             for (int x = 0; x < cookies.length; x++) {
288               CookieManager.getInstance().setCookie(url, cookies[x].getValue());
289             }
290             CookieSyncManager.getInstance().sync();
291             cookie = CookieManager.getInstance().getCookie(url);
292           }
293         } catch (IOException e) {
294           Log.e(TAG, e.toString());
295         }
296         client.close();
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 }