xref: /aosp_15_r20/external/zxing/android/src/com/google/zxing/client/android/book/SearchBookContentsActivity.java (revision 513427e33d61bc67fc40bc261642ac0b2a686b45)
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("&lt;");
56   private static final Pattern GT_ENTITY_PATTERN = Pattern.compile("&gt;");
57   private static final Pattern QUOTE_ENTITY_PATTERN = Pattern.compile("&#39;");
58   private static final Pattern QUOT_ENTITY_PATTERN = Pattern.compile("&quot;");
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