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.book; 18 19 import android.app.Activity; 20 import android.content.Intent; 21 import android.os.AsyncTask; 22 import android.os.Bundle; 23 import android.util.Log; 24 import android.view.KeyEvent; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.widget.EditText; 28 import android.widget.ListView; 29 import android.widget.TextView; 30 31 import org.json.JSONArray; 32 import org.json.JSONException; 33 import org.json.JSONObject; 34 35 import java.io.IOException; 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.regex.Pattern; 39 40 import com.google.zxing.client.android.Intents; 41 import com.google.zxing.client.android.HttpHelper; 42 import com.google.zxing.client.android.LocaleManager; 43 import com.google.zxing.client.android.R; 44 45 /** 46 * Uses Google Book Search to find a word or phrase in the requested book. 47 * 48 * @author [email protected] (Daniel Switkin) 49 */ 50 public final class SearchBookContentsActivity extends Activity { 51 52 private static final String TAG = SearchBookContentsActivity.class.getSimpleName(); 53 54 private static final Pattern TAG_PATTERN = Pattern.compile("<.*?>"); 55 private static final Pattern LT_ENTITY_PATTERN = Pattern.compile("<"); 56 private static final Pattern GT_ENTITY_PATTERN = Pattern.compile(">"); 57 private static final Pattern QUOTE_ENTITY_PATTERN = Pattern.compile("'"); 58 private static final Pattern QUOT_ENTITY_PATTERN = Pattern.compile("""); 59 60 private String isbn; 61 private EditText queryTextView; 62 private View queryButton; 63 private ListView resultListView; 64 private TextView headerView; 65 private AsyncTask<String,?,?> networkTask; 66 67 private final View.OnClickListener buttonListener = new View.OnClickListener() { 68 @Override 69 public void onClick(View view) { 70 launchSearch(); 71 } 72 }; 73 74 private final View.OnKeyListener keyListener = new View.OnKeyListener() { 75 @Override 76 public boolean onKey(View view, int keyCode, KeyEvent event) { 77 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) { 78 launchSearch(); 79 return true; 80 } 81 return false; 82 } 83 }; 84 getISBN()85 String getISBN() { 86 return isbn; 87 } 88 89 @Override onCreate(Bundle icicle)90 public void onCreate(Bundle icicle) { 91 super.onCreate(icicle); 92 93 Intent intent = getIntent(); 94 if (intent == null || !Intents.SearchBookContents.ACTION.equals(intent.getAction())) { 95 finish(); 96 return; 97 } 98 99 isbn = intent.getStringExtra(Intents.SearchBookContents.ISBN); 100 if (isbn == null) { 101 finish(); 102 return; 103 } 104 105 if (LocaleManager.isBookSearchUrl(isbn)) { 106 setTitle(getString(R.string.sbc_name)); 107 } else { 108 setTitle(getString(R.string.sbc_name) + ": ISBN " + isbn); 109 } 110 111 setContentView(R.layout.search_book_contents); 112 queryTextView = (EditText) findViewById(R.id.query_text_view); 113 114 String initialQuery = intent.getStringExtra(Intents.SearchBookContents.QUERY); 115 if (initialQuery != null && !initialQuery.isEmpty()) { 116 // Populate the search box but don't trigger the search 117 queryTextView.setText(initialQuery); 118 } 119 queryTextView.setOnKeyListener(keyListener); 120 121 queryButton = findViewById(R.id.query_button); 122 queryButton.setOnClickListener(buttonListener); 123 124 resultListView = (ListView) findViewById(R.id.result_list_view); 125 LayoutInflater factory = LayoutInflater.from(this); 126 headerView = (TextView) factory.inflate(R.layout.search_book_contents_header, 127 resultListView, false); 128 resultListView.addHeaderView(headerView); 129 } 130 131 @Override onResume()132 protected void onResume() { 133 super.onResume(); 134 queryTextView.selectAll(); 135 } 136 137 @Override onPause()138 protected void onPause() { 139 AsyncTask<?,?,?> oldTask = networkTask; 140 if (oldTask != null) { 141 oldTask.cancel(true); 142 networkTask = null; 143 } 144 super.onPause(); 145 } 146 launchSearch()147 private void launchSearch() { 148 String query = queryTextView.getText().toString(); 149 if (query != null && !query.isEmpty()) { 150 AsyncTask<?,?,?> oldTask = networkTask; 151 if (oldTask != null) { 152 oldTask.cancel(true); 153 } 154 networkTask = new NetworkTask(); 155 networkTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, query, isbn); 156 headerView.setText(R.string.msg_sbc_searching_book); 157 resultListView.setAdapter(null); 158 queryTextView.setEnabled(false); 159 queryButton.setEnabled(false); 160 } 161 } 162 163 private final class NetworkTask extends AsyncTask<String,Object,JSONObject> { 164 165 @Override doInBackground(String... args)166 protected JSONObject doInBackground(String... args) { 167 try { 168 // These return a JSON result which describes if and where the query was found. This API may 169 // break or disappear at any time in the future. Since this is an API call rather than a 170 // website, we don't use LocaleManager to change the TLD. 171 String theQuery = args[0]; 172 String theIsbn = args[1]; 173 String uri; 174 if (LocaleManager.isBookSearchUrl(theIsbn)) { 175 int equals = theIsbn.indexOf('='); 176 String volumeId = theIsbn.substring(equals + 1); 177 uri = "http://www.google.com/books?id=" + volumeId + "&jscmd=SearchWithinVolume2&q=" + theQuery; 178 } else { 179 uri = "http://www.google.com/books?vid=isbn" + theIsbn + "&jscmd=SearchWithinVolume2&q=" + theQuery; 180 } 181 CharSequence content = HttpHelper.downloadViaHttp(uri, HttpHelper.ContentType.JSON); 182 return new JSONObject(content.toString()); 183 } catch (IOException | JSONException ioe) { 184 Log.w(TAG, "Error accessing book search", ioe); 185 return null; 186 } 187 } 188 189 @Override onPostExecute(JSONObject result)190 protected void onPostExecute(JSONObject result) { 191 if (result == null) { 192 headerView.setText(R.string.msg_sbc_failed); 193 } else { 194 handleSearchResults(result); 195 } 196 queryTextView.setEnabled(true); 197 queryTextView.selectAll(); 198 queryButton.setEnabled(true); 199 } 200 201 // Currently there is no way to distinguish between a query which had no results and a book 202 // which is not searchable - both return zero results. handleSearchResults(JSONObject json)203 private void handleSearchResults(JSONObject json) { 204 try { 205 int count = json.getInt("number_of_results"); 206 headerView.setText(getString(R.string.msg_sbc_results) + " : " + count); 207 if (count > 0) { 208 JSONArray results = json.getJSONArray("search_results"); 209 SearchBookContentsResult.setQuery(queryTextView.getText().toString()); 210 List<SearchBookContentsResult> items = new ArrayList<>(count); 211 for (int x = 0; x < count; x++) { 212 items.add(parseResult(results.getJSONObject(x))); 213 } 214 resultListView.setOnItemClickListener(new BrowseBookListener(SearchBookContentsActivity.this, items)); 215 resultListView.setAdapter(new SearchBookContentsAdapter(SearchBookContentsActivity.this, items)); 216 } else { 217 String searchable = json.optString("searchable"); 218 if ("false".equals(searchable)) { 219 headerView.setText(R.string.msg_sbc_book_not_searchable); 220 } 221 resultListView.setAdapter(null); 222 } 223 } catch (JSONException e) { 224 Log.w(TAG, "Bad JSON from book search", e); 225 resultListView.setAdapter(null); 226 headerView.setText(R.string.msg_sbc_failed); 227 } 228 } 229 230 // Available fields: page_id, page_number, snippet_text parseResult(JSONObject json)231 private SearchBookContentsResult parseResult(JSONObject json) { 232 233 String pageId; 234 String pageNumber; 235 String snippet; 236 try { 237 pageId = json.getString("page_id"); 238 pageNumber = json.optString("page_number"); 239 snippet = json.optString("snippet_text"); 240 } catch (JSONException e) { 241 Log.w(TAG, e); 242 // Never seen in the wild, just being complete. 243 return new SearchBookContentsResult(getString(R.string.msg_sbc_no_page_returned), "", "", false); 244 } 245 246 if (pageNumber == null || pageNumber.isEmpty()) { 247 // This can happen for text on the jacket, and possibly other reasons. 248 pageNumber = ""; 249 } else { 250 pageNumber = getString(R.string.msg_sbc_page) + ' ' + pageNumber; 251 } 252 253 boolean valid = snippet != null && !snippet.isEmpty(); 254 if (valid) { 255 // Remove all HTML tags and encoded characters. 256 snippet = TAG_PATTERN.matcher(snippet).replaceAll(""); 257 snippet = LT_ENTITY_PATTERN.matcher(snippet).replaceAll("<"); 258 snippet = GT_ENTITY_PATTERN.matcher(snippet).replaceAll(">"); 259 snippet = QUOTE_ENTITY_PATTERN.matcher(snippet).replaceAll("'"); 260 snippet = QUOT_ENTITY_PATTERN.matcher(snippet).replaceAll("\""); 261 } else { 262 snippet = '(' + getString(R.string.msg_sbc_snippet_unavailable) + ')'; 263 } 264 265 return new SearchBookContentsResult(pageId, pageNumber, snippet, valid); 266 } 267 268 } 269 270 } 271