/*
* Copyright (C) 2009 Muthu Ramadoss. All rights reserved.
*
* Modified from Zxing project to suit Books-Exchange requirements.
* Original source from Zxing - http://code.google.com/p/zxing/
*/
/*
* Copyright (C) 2008 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidrocks.bex.zxing.client.android;
import com.androidrocks.bex.R;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
public final class SearchBookContentsActivity extends Activity {
private static final String TAG = "SearchBookContents";
private static final String USER_AGENT = "ZXing/1.5 (Android)";
private NetworkThread mNetworkThread;
private String mISBN;
private EditText mQueryTextView;
private Button mQueryButton;
private ListView mResultListView;
private TextView mHeaderView;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
// Make sure that expired cookies are removed on launch.
CookieSyncManager.createInstance(this);
CookieManager.getInstance().removeExpiredCookie();
Intent intent = getIntent();
if (intent == null || (!intent.getAction().equals(Intents.SearchBookContents.ACTION) &&
!intent.getAction().equals(Intents.SearchBookContents.DEPRECATED_ACTION))) {
finish();
return;
}
mISBN = intent.getStringExtra(Intents.SearchBookContents.ISBN);
setTitle(getString(R.string.sbc_name) + ": ISBN " + mISBN);
setContentView(R.layout.search_book_contents);
mQueryTextView = (EditText) findViewById(R.id.query_text_view);
String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY);
if (initialQuery != null && initialQuery.length() > 0) {
// Populate the search box but don't trigger the search
mQueryTextView.setText(initialQuery);
}
mQueryTextView.setOnKeyListener(mKeyListener);
mQueryButton = (Button) findViewById(R.id.query_button);
mQueryButton.setOnClickListener(mButtonListener);
mResultListView = (ListView) findViewById(R.id.result_list_view);
LayoutInflater factory = LayoutInflater.from(this);
mHeaderView = (TextView) factory.inflate(R.layout.search_book_contents_header,
mResultListView, false);
mResultListView.addHeaderView(mHeaderView);
}
@Override
protected void onResume() {
super.onResume();
mQueryTextView.selectAll();
}
@Override
public void onConfigurationChanged(Configuration config) {
// Do nothing, this is to prevent the activity from being restarted when the keyboard opens.
super.onConfigurationChanged(config);
}
public final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case R.id.search_book_contents_succeeded:
handleSearchResults((JSONObject) message.obj);
resetForNewQuery();
break;
case R.id.search_book_contents_failed:
resetForNewQuery();
mHeaderView.setText(R.string.msg_sbc_failed);
break;
}
}
};
private void resetForNewQuery() {
mNetworkThread = null;
mQueryTextView.setEnabled(true);
mQueryTextView.selectAll();
mQueryButton.setEnabled(true);
}
private final Button.OnClickListener mButtonListener = new Button.OnClickListener() {
public void onClick(View view) {
launchSearch();
}
};
private final View.OnKeyListener mKeyListener = new View.OnKeyListener() {
public boolean onKey(View view, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
launchSearch();
return true;
}
return false;
}
};
private void launchSearch() {
if (mNetworkThread == null) {
String query = mQueryTextView.getText().toString();
if (query != null && query.length() > 0) {
mNetworkThread = new NetworkThread(mISBN, query, mHandler);
mNetworkThread.start();
mHeaderView.setText(R.string.msg_sbc_searching_book);
mResultListView.setAdapter(null);
mQueryTextView.setEnabled(false);
mQueryButton.setEnabled(false);
}
}
}
// Currently there is no way to distinguish between a query which had no results and a book
// which is not searchable - both return zero results.
private void handleSearchResults(JSONObject json) {
try {
int count = json.getInt("number_of_results");
mHeaderView.setText("Found " + (count == 1 ? "1 result" : count + " results"));
if (count > 0) {
JSONArray results = json.getJSONArray("search_results");
SearchBookContentsResult.setQuery(mQueryTextView.getText().toString());
List<SearchBookContentsResult> items = new ArrayList<SearchBookContentsResult>(count);
for (int x = 0; x < count; x++) {
items.add(parseResult(results.getJSONObject(x)));
}
mResultListView.setAdapter(new SearchBookContentsAdapter(this, items));
} else {
String searchable = json.optString("searchable");
if ("false".equals(searchable)) {
mHeaderView.setText(R.string.msg_sbc_book_not_searchable);
}
mResultListView.setAdapter(null);
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
mResultListView.setAdapter(null);
mHeaderView.setText(R.string.msg_sbc_failed);
}
}
// Available fields: page_number, page_id, page_url, snippet_text
private SearchBookContentsResult parseResult(JSONObject json) {
try {
String pageNumber = json.getString("page_number");
if (pageNumber.length() > 0) {
pageNumber = getString(R.string.msg_sbc_page) + ' ' + pageNumber;
} else {
// This can happen for text on the jacket, and possibly other reasons.
pageNumber = getString(R.string.msg_sbc_unknown_page);
}
// Remove all HTML tags and encoded characters. Ideally the server would do this.
String snippet = json.optString("snippet_text");
boolean valid = true;
if (snippet.length() > 0) {
snippet = snippet.replaceAll("\\<.*?\\>", "");
snippet = snippet.replaceAll("<", "<");
snippet = snippet.replaceAll(">", ">");
snippet = snippet.replaceAll("'", "'");
snippet = snippet.replaceAll(""", "\"");
} else {
snippet = '(' + getString(R.string.msg_sbc_snippet_unavailable) + ')';
valid = false;
}
return new SearchBookContentsResult(pageNumber, snippet, valid);
} catch (JSONException e) {
// Never seen in the wild, just being complete.
return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", false);
}
}
private static final class NetworkThread extends Thread {
private final String mISBN;
private final String mQuery;
private final Handler mHandler;
NetworkThread(String isbn, String query, Handler handler) {
mISBN = isbn;
mQuery = query;
mHandler = handler;
}
@Override
public void run() {
AndroidHttpClient client = null;
try {
// These return a JSON result which describes if and where the query was found. This API may
// break or disappear at any time in the future. Since this is an API call rather than a
// website, we don't use LocaleManager to change the TLD.
URI uri = new URI("http", null, "www.google.com", -1, "/books", "vid=isbn" + mISBN +
"&jscmd=SearchWithinVolume2&q=" + mQuery, null);
HttpUriRequest get = new HttpGet(uri);
get.setHeader("cookie", getCookie(uri.toString()));
client = AndroidHttpClient.newInstance(USER_AGENT);
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
ByteArrayOutputStream jsonHolder = new ByteArrayOutputStream();
entity.writeTo(jsonHolder);
jsonHolder.flush();
JSONObject json = new JSONObject(jsonHolder.toString(getEncoding(entity)));
jsonHolder.close();
Message message = Message.obtain(mHandler, R.id.search_book_contents_succeeded);
message.obj = json;
message.sendToTarget();
} else {
Log.e(TAG, "HTTP returned " + response.getStatusLine().getStatusCode() + " for " + uri);
Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
message.sendToTarget();
}
} catch (Exception e) {
Log.e(TAG, e.toString());
Message message = Message.obtain(mHandler, R.id.search_book_contents_failed);
message.sendToTarget();
} finally {
if (client != null) {
client.close();
}
}
}
// Book Search requires a cookie to work, which we store persistently. If the cookie does
// not exist, this could be the first search or it has expired. Either way, do a quick HEAD
// request to fetch it, save it via the CookieSyncManager to flash, then return it.
private String getCookie(String url) {
String cookie = CookieManager.getInstance().getCookie(url);
if (cookie == null || cookie.length() == 0) {
Log.v(TAG, "Book Search cookie was missing or expired");
HttpUriRequest head = new HttpHead(url);
AndroidHttpClient client = AndroidHttpClient.newInstance(USER_AGENT);
try {
HttpResponse response = client.execute(head);
if (response.getStatusLine().getStatusCode() == 200) {
Header[] cookies = response.getHeaders("set-cookie");
for (Header theCookie : cookies) {
CookieManager.getInstance().setCookie(url, theCookie.getValue());
}
CookieSyncManager.getInstance().sync();
cookie = CookieManager.getInstance().getCookie(url);
}
} catch (IOException e) {
Log.e(TAG, e.toString());
}
client.close();
}
return cookie;
}
private static String getEncoding(HttpEntity entity) {
// FIXME: The server is returning ISO-8859-1 but the content is actually windows-1252.
// Once Jeff fixes the HTTP response, remove this hardcoded value and go back to getting
// the encoding dynamically.
return "windows-1252";
// HeaderElement[] elements = entity.getContentType().getElements();
// if (elements != null && elements.length > 0) {
// String encoding = elements[0].getParameterByName("charset").getValue();
// if (encoding != null && encoding.length() > 0) {
// return encoding;
// }
// }
// return "UTF-8";
}
}
}