/* * 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.google.zxing.client.android.book; import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; 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 com.google.zxing.FakeR; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import com.google.zxing.client.android.Intents; import com.google.zxing.client.android.HttpHelper; import com.google.zxing.client.android.LocaleManager; import com.google.zxing.client.android.R; import com.google.zxing.client.android.common.executor.AsyncTaskExecInterface; import com.google.zxing.client.android.common.executor.AsyncTaskExecManager; /** * Uses Google Book Search to find a word or phrase in the requested book. * * @author dswitkin@google.com (Daniel Switkin) */ public final class SearchBookContentsActivity extends Activity { private static final String TAG = SearchBookContentsActivity.class.getSimpleName(); private static final Pattern TAG_PATTERN = Pattern.compile("\\<.*?\\>"); private static final Pattern LT_ENTITY_PATTERN = Pattern.compile("<"); private static final Pattern GT_ENTITY_PATTERN = Pattern.compile(">"); private static final Pattern QUOTE_ENTITY_PATTERN = Pattern.compile("'"); private static final Pattern QUOT_ENTITY_PATTERN = Pattern.compile("""); private String isbn; private EditText queryTextView; private Button queryButton; private ListView resultListView; private TextView headerView; private NetworkTask networkTask; private final AsyncTaskExecInterface taskExec; public SearchBookContentsActivity() { taskExec = new AsyncTaskExecManager().build(); } private final Button.OnClickListener buttonListener = new Button.OnClickListener() { @Override public void onClick(View view) { launchSearch(); } }; private final View.OnKeyListener keyListener = new View.OnKeyListener() { @Override public boolean onKey(View view, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) { launchSearch(); return true; } return false; } }; String getISBN() { return isbn; } private static FakeR fakeR; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); fakeR = new FakeR(this); // 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)) { finish(); return; } isbn = intent.getStringExtra(Intents.SearchBookContents.ISBN); if (LocaleManager.isBookSearchUrl(isbn)) { setTitle(getString(fakeR.getId("string", "sbc_name"))); } else { setTitle(getString(fakeR.getId("string", "sbc_name")) + ": ISBN " + isbn); } setContentView(fakeR.getId("layout", "search_book_contents")); queryTextView = (EditText) findViewById(fakeR.getId("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 queryTextView.setText(initialQuery); } queryTextView.setOnKeyListener(keyListener); queryButton = (Button) findViewById(fakeR.getId("id", "query_button")); queryButton.setOnClickListener(buttonListener); resultListView = (ListView) findViewById(fakeR.getId("id", "result_list_view")); LayoutInflater factory = LayoutInflater.from(this); headerView = (TextView) factory.inflate(fakeR.getId("layout", "search_book_contents_header"), resultListView, false); resultListView.addHeaderView(headerView); } @Override protected void onResume() { super.onResume(); queryTextView.selectAll(); } @Override protected void onPause() { NetworkTask oldTask = networkTask; if (oldTask != null) { oldTask.cancel(true); networkTask = null; } super.onPause(); } private void launchSearch() { String query = queryTextView.getText().toString(); if (query != null && query.length() > 0) { NetworkTask oldTask = networkTask; if (oldTask != null) { oldTask.cancel(true); } networkTask = new NetworkTask(); taskExec.execute(networkTask, query, isbn); headerView.setText(fakeR.getId("string", "msg_sbc_searching_book")); resultListView.setAdapter(null); queryTextView.setEnabled(false); queryButton.setEnabled(false); } } private final class NetworkTask extends AsyncTask<String,Object,JSONObject> { @Override protected JSONObject doInBackground(String... args) { 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. String theQuery = args[0]; String theIsbn = args[1]; String uri; if (LocaleManager.isBookSearchUrl(theIsbn)) { int equals = theIsbn.indexOf('='); String volumeId = theIsbn.substring(equals + 1); uri = "http://www.google.com/books?id=" + volumeId + "&jscmd=SearchWithinVolume2&q=" + theQuery; } else { uri = "http://www.google.com/books?vid=isbn" + theIsbn + "&jscmd=SearchWithinVolume2&q=" + theQuery; } CharSequence content = HttpHelper.downloadViaHttp(uri, HttpHelper.ContentType.JSON); return new JSONObject(content.toString()); } catch (IOException ioe) { Log.w(TAG, "Error accessing book search", ioe); return null; } catch (JSONException je) { Log.w(TAG, "Error accessing book search", je); return null; } } @Override protected void onPostExecute(JSONObject result) { if (result == null) { headerView.setText(fakeR.getId("string", "msg_sbc_failed")); } else { handleSearchResults(result); } queryTextView.setEnabled(true); queryTextView.selectAll(); queryButton.setEnabled(true); } // 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"); headerView.setText(getString(fakeR.getId("string", "msg_sbc_results")) + " : " + count); if (count > 0) { JSONArray results = json.getJSONArray("search_results"); SearchBookContentsResult.setQuery(queryTextView.getText().toString()); List<SearchBookContentsResult> items = new ArrayList<SearchBookContentsResult>(count); for (int x = 0; x < count; x++) { items.add(parseResult(results.getJSONObject(x))); } resultListView.setOnItemClickListener(new BrowseBookListener(SearchBookContentsActivity.this, items)); resultListView.setAdapter(new SearchBookContentsAdapter(SearchBookContentsActivity.this, items)); } else { String searchable = json.optString("searchable"); if ("false".equals(searchable)) { headerView.setText(fakeR.getId("string", "msg_sbc_book_not_searchable")); } resultListView.setAdapter(null); } } catch (JSONException e) { Log.w(TAG, "Bad JSON from book search", e); resultListView.setAdapter(null); headerView.setText(fakeR.getId("string", "msg_sbc_failed")); } } // Available fields: page_id, page_number, page_url, snippet_text private SearchBookContentsResult parseResult(JSONObject json) { try { String pageId = json.getString("page_id"); String pageNumber = json.getString("page_number"); if (pageNumber.length() > 0) { pageNumber = getString(fakeR.getId("string", "msg_sbc_page")) + ' ' + pageNumber; } else { // This can happen for text on the jacket, and possibly other reasons. pageNumber = getString(fakeR.getId("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 = TAG_PATTERN.matcher(snippet).replaceAll(""); snippet = LT_ENTITY_PATTERN.matcher(snippet).replaceAll("<"); snippet = GT_ENTITY_PATTERN.matcher(snippet).replaceAll(">"); snippet = QUOTE_ENTITY_PATTERN.matcher(snippet).replaceAll("'"); snippet = QUOT_ENTITY_PATTERN.matcher(snippet).replaceAll("\""); } else { snippet = '(' + getString(fakeR.getId("string", "msg_sbc_snippet_unavailable")) + ')'; valid = false; } return new SearchBookContentsResult(pageId, pageNumber, snippet, valid); } catch (JSONException e) { // Never seen in the wild, just being complete. return new SearchBookContentsResult(getString(fakeR.getId("string", "msg_sbc_no_page_returned")), "", "", false); } } } }