With the ZXing library, you can create Android applications with barcode scanning functionality. In Android SDK: Create a Barcode Reader, we implemented basic barcode reading with the library in a simple app. In this tutorial series, we will build on what we learned to create an app that will scan books and retrieve related information about them from Google Books API.
This series on Creating a Book Scanner App is in three parts:
- Google Books API and ZXing Setup
- Interface Creation and Book Search
- Retrieving and Displaying Book Information
Here is a preview of the app we are working toward:
When the final app runs, the user will be able to scan book barcodes. The app will present a selection of data about the scanned books, including where they're available for purchase. The app will also provide a link to the page for a scanned book on Google Books.
1. Parse the Search Results
Step 1
In the last section, we executed the Google Books search query using the scanned ISBN (EAN) number, then brought the returned data into the app as a string. Now let's parse the returned string, which is a JSON feed. We will do this in the onPostExecute method of the inner AsyncTask class we created. Add the method outline after the doInBackground method we completed last time.
protected void onPostExecute(String result) { //parse search results }
When you parse a JSON feed or any other incoming data from an external data source, you need to first familiarize yourself with the structure of the incoming data. Have a look at the Search and Volume sections on the Google Books API documentation to see the structure of what the search query will return.
We will be using the Java JSON libraries to process the search results. Add the following imports to your main activity class.
import java.io.BufferedInputStream; import java.net.URL; import java.net.URLConnection; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.net.Uri; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
Step 2
Let's start working through the search result JSON. In your onPostExecute method, add try and catch blocks to cope with any input exceptions.
try{ //parse results } catch (Exception e) { //no result }
In the catch block, respond to cases where there has not been a valid search result.
e.printStackTrace(); titleText.setText("NOT FOUND"); authorText.setText(""); descriptionText.setText(""); dateText.setText(""); starLayout.removeAllViews(); ratingCountText.setText(""); thumbView.setImageBitmap(null); previewBtn.setVisibility(View.GONE);
We simply set all of the user interface elements to indicate that a valid search result was not returned. This may happen if a scanned book is not listed on Google Books or if something else goes wrong when inputting the data.
Step 3
Back in the try block, set the preview button to be visible, assuming we have a valid result:
previewBtn.setVisibility(View.VISIBLE);
Now start to retrieve the JSON objects from the returned string.
JSONObject resultObject = new JSONObject(result); JSONArray bookArray = resultObject.getJSONArray("items");
As you can see from the structure outlined in the search instructions on the Google Books API documentation, the query result will include an array named "items" containing any results matching the passed ISBN number. Since we are only expecting a single result for the book scanned, let's get the first item in the array.
JSONObject bookObject = bookArray.getJSONObject(0);
Now we need the "volumeInfo" object which contains some of the data we want to display.
JSONObject volumeObject = bookObject.getJSONObject("volumeInfo");
This object will give us access to most of the book data items we are looking for.
Step 4
In some cases you'll find that the Google Books search result will return some but not all of the data items, so we'll use repeated try and catch blocks to handle cases where individual data values are missing. Start with the title.
try{ titleText.setText("TITLE: "+volumeObject.getString("title")); } catch(JSONException jse){ titleText.setText(""); jse.printStackTrace(); }
If the string can be retrieved, we'll display it in the relevant Text View. Otherwise, we'll set it to display an empty string, with the catch block allowing the app to continue processing the search results in spite of this missing item.
Next let's process the author, which is represented as an array in case there is more than one. We'll build the entire string using a loop.
StringBuilder authorBuild = new StringBuilder(""); try{ JSONArray authorArray = volumeObject.getJSONArray("authors"); for(int a=0; a<authorArray.length(); a++){ if(a>0) authorBuild.append(", "); authorBuild.append(authorArray.getString(a)); } authorText.setText("AUTHOR(S): "+authorBuild.toString()); } catch(JSONException jse){ authorText.setText(""); jse.printStackTrace(); }
Authors will be separated by commas if there's more than one, otherwise the single author name will appear. Now parse the publication date.
try{ dateText.setText("PUBLISHED: "+volumeObject.getString("publishedDate")); } catch(JSONException jse){ dateText.setText(""); jse.printStackTrace(); }
Next, we'll process the description.
try{ descriptionText.setText("DESCRIPTION: "+volumeObject.getString("description")); } catch(JSONException jse){ descriptionText.setText(""); jse.printStackTrace(); }
Now let's deal with the star rating. Remember that we created a layout and declared an array to hold the star Image Views. Go back to your onCreate method for a moment and instantiate the array after the existing code.
starViews=new ImageView[5]; for(int s=0; s<starViews.length; s++){ starViews[s]=new ImageView(this); }
There can be a maximum of five stars. Back in onPostExecute after the last catch block we added for the description, add try and catch blocks for the stars.
try{ //set stars } catch(JSONException jse){ starLayout.removeAllViews(); jse.printStackTrace(); }
When we retrieve the number of stars awarded to the book, we'll add the Image Views dynamically to the layout we created for the star rating. If the star rating number cannot be retrieved, we'll remove any previously added views in the catch block. In this try block, retrieve the number of stars from the returned JSON and cast it to an integer.
double decNumStars = Double.parseDouble(volumeObject.getString("averageRating")); int numStars = (int)decNumStars;
Set the number of stars currently displayed as the layout object tag. We will use this later when dealing with saving the app state. Remove any existing views from the layout for repeated scans.
starLayout.setTag(numStars); starLayout.removeAllViews();
Now we can simply loop to add the relevant number of stars.
for(int s=0; s<numStars; s++){ starViews[s].setImageResource(R.drawable.star); starLayout.addView(starViews[s]); }
Remember that we added the star image to the application drawables last time. After the star section catch block, we can deal with the rating count.
try{ ratingCountText.setText(" - "+volumeObject.getString("ratingsCount")+" ratings"); } catch(JSONException jse){ ratingCountText.setText(""); jse.printStackTrace(); }
Now let's check to see if the book has a preview on Google Books. Set the preview button to be enabled or disabled accordingly.
try{ boolean isEmbeddable = Boolean.parseBoolean (bookObject.getJSONObject("accessInfo").getString("embeddable")); if(isEmbeddable) previewBtn.setEnabled(true); else previewBtn.setEnabled(false); } catch(JSONException jse){ previewBtn.setEnabled(false); jse.printStackTrace(); }
We'll deal with what happens when pressing the preview button later. Next get the URL for the book's page on Google Books and set it as the tag for the link button.
try{ linkBtn.setTag(volumeObject.getString("infoLink")); linkBtn.setVisibility(View.VISIBLE); } catch(JSONException jse){ linkBtn.setVisibility(View.GONE); jse.printStackTrace(); }
We'll implement clicks on this later. The only remaining piece of information we still want to process is the thumbnail image, which is a little more complex. We want to use an additional AsyncTask class to retrieve it in the background.
2. Retrieve the Thumbnail Image
Step 1
After the "GetBookInfo" class, add another for retrieving the book thumbnail.
private class GetBookThumb extends AsyncTask<String, Void, String> { //get thumbnail }
We will pass the thumbnail URL string from the other AsyncTask when we retrieve it from the JSON. Inside this new AsyncTask class, add the doInBackground method.
@Override protected String doInBackground(String... thumbURLs) { //attempt to download image }
Step 2
Inside doInBackground, add try and catch blocks in case of I/O exceptions.
try{ //try to download } catch(Exception e) { e.printStackTrace(); }
In the try block, attempt to make a connection using the passed thumbnail URL.
URL thumbURL = new URL(thumbURLs[0]); URLConnection thumbConn = thumbURL.openConnection(); thumbConn.connect();
Now get input and buffered streams.
InputStream thumbIn = thumbConn.getInputStream(); BufferedInputStream thumbBuff = new BufferedInputStream(thumbIn);
We want to bring the image into the app as a bitmap, so add a new instance variable at the top of the activity class declaration.
private Bitmap thumbImg;
Back in doInBackground for the "GetBookThumb" class, after creating the buffered input stream, read the image into this bitmap.
thumbImg = BitmapFactory.decodeStream(thumbBuff);
Close the streams.
thumbBuff.close(); thumbIn.close();
After the catch block, return an empty string to complete the method.
return "";
After doInBackground, add the onPostExecute method to show the downloaded image.
protected void onPostExecute(String result) { thumbView.setImageBitmap(thumbImg); }
3. Execute the Retrieving and Parsing Classes
Step 1
Now let's pull these strands together. In your onActivityResult method, after the line in which you created the search query string, create and execute an instance of the AsyncTask class to fetch the search results.
new GetBookInfo().execute(bookSearchString);
In the onPostExecute method of the "GetBookInfo" class, after the catch block for the link button, add a final section for fetching the thumbnail using the JSON data and the other AsyncTask class.
try{ JSONObject imageInfo = volumeObject.getJSONObject("imageLinks"); new GetBookThumb().execute(imageInfo.getString("smallThumbnail")); } catch(JSONException jse){ thumbView.setImageBitmap(null); jse.printStackTrace(); }
4. Handle the Remaining Clicks
Step 1
Let's deal with the preview and link buttons. Start with the link button. We've already set the class as click listener for the buttons, so add a new section to your onClick method after the conditional for the scan button.
else if(v.getId()==R.id.link_btn){ //get the url tag String tag = (String)v.getTag(); //launch the url Intent webIntent = new Intent(Intent.ACTION_VIEW); webIntent.setData(Uri.parse(tag)); startActivity(webIntent); }
This code retrieves the URL from the button tag, which we set when we processed the JSON. The code then launches the link in the browser.
In order to launch the preview, we need the book ISBN number. Go back to the onActivityResult method for a moment. Add a line before the line in which you built the search query string. Set the scanned ISBN as a tag for the preview button.
previewBtn.setTag(scanContent);
Now back in onClick, add a conditional for the preview button in which you retrieve the tag.
else if(v.getId()==R.id.preview_btn){ String tag = (String)v.getTag(); //launch preview }
Step 2
We are going to create a new activity for the book preview. Add a new class to your project in the same package as the main activity, naming it "EmbeddedBook". Give your class the following outline.
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import android.app.Activity; import android.os.Bundle; import android.webkit.WebView; public class EmbeddedBook extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }
Create a new layout file in your app's "res/layout" folder, giving it the name "embedded_book.xml". Enter the following layout.
<WebView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/embedded_book_view" android:layout_width="fill_parent" android:layout_height="fill_parent" />
We will present the book preview in the WebView. Back in the new class, set this as content view in onCreate after the existing code.
setContentView(R.layout.embedded_book);
Add an instance variable at the top of the class.
private WebView embedView;
In onCreate, get a reference to the WebView we included in the layout.
embedView = (WebView)findViewById(R.id.embedded_book_view);
Set JavaScript to be enabled.
embedView.getSettings().setJavaScriptEnabled(true);
When we launch the activity, we will pass the ISBN. Retrieve this passed data next in onCreate.
Bundle extras = getIntent().getExtras();
Now check to see whether we have any extras.
if(extras !=null) { //get isbn }
Inside the conditional, attempt to retrieve the ISBN.
String isbn = extras.getString("isbn");
We are going to include the WebView content in an HTML formatted asset file. Add try and catch blocks for loading it.
try{ //load asset } catch(IOException ioe){ embedView.loadData ("<html><head></head><body>Whoops! Something went wrong.</body></html>", "text/html", "utf-8"); ioe.printStackTrace(); }
If the file cannot be loaded, we'll output an error message. We'll return to the try block soon. First, create a new file in your app's "assets" folder, naming it "embedded_book_page.html". Locate the new file in your Package Explorer, right-click and select "Open With" and then "Text Editor". Enter the following code.
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>Google Books Embedded Viewer API Example</title> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script type="text/javascript"> google.load("books", "0"); function initialize() { var viewer = new google.books.DefaultViewer(document.getElementById('viewerCanvas')); viewer.load('ISBN:$ISBN$'); } google.setOnLoadCallback(initialize); </script> </head> <body> <div id="viewerCanvas" style="width: 600px; height: 500px"></div> </body> </html>
This is the standard markup code used to display a Google Books Preview with the Embedded Viewer API. Notice that the page includes "$ISBN$". We'll use this to pass the ISBN number in from the activity.
Back to the try block in your "EmbeddedBook" class. Try to load the page using the following algorithm.
InputStream pageIn = getAssets().open("embedded_book_page.html"); BufferedReader htmlIn = new BufferedReader(new InputStreamReader(pageIn)); StringBuilder htmlBuild = new StringBuilder(""); String lineIn; while ((lineIn = htmlIn.readLine()) != null) { htmlBuild.append(lineIn); } htmlIn.close();
Next, pass the ISBN, replacing the section we included for it in the HTML.
String embeddedPage = htmlBuild.toString().replace("$ISBN$", isbn);
Complete the new activity by loading the page.
embedView.loadData(embeddedPage, "text/html", "utf-8");
Step 3
Back in the main activity class onClick method, in the conditional block you added for the preview button, after the line in which you retrieved the tag, launch the new activity, passing the ISBN.
Intent intent = new Intent(this, EmbeddedBook.class); intent.putExtra("isbn", tag); startActivity(intent);
Add the new class to your application manifest file, after the main activity element.
<activity android:name="com.example.barcodescanningapp.EmbeddedBook" />
Alter the package name to suit your own if necessary.
5. Preserve the Application State
Step 1
You can run the app at this stage and it should function. However, let's take care of the application state changing first. In your main activity class, add the following method.
protected void onSaveInstanceState(Bundle savedBundle) { savedBundle.putString("title", ""+titleText.getText()); savedBundle.putString("author", ""+authorText.getText()); savedBundle.putString("description", ""+descriptionText.getText()); savedBundle.putString("date", ""+dateText.getText()); savedBundle.putString("ratings", ""+ratingCountText.getText()); savedBundle.putParcelable("thumbPic", thumbImg); if(starLayout.getTag()!=null) savedBundle.putInt("stars", Integer.parseInt(starLayout.getTag().toString())); savedBundle.putBoolean("isEmbed", previewBtn.isEnabled()); savedBundle.putInt("isLink", linkBtn.getVisibility()); if(previewBtn.getTag()!=null) savedBundle.putString("isbn", previewBtn.getTag().toString()); }
We won't go into detail about saving the state because it's not the focal point of this tutorial. Look over the code to ensure you understand it. We are simply saving the displayed data so that we can preserve it when the application state changes. In onCreate after the existing code, retrieve this information.
if (savedInstanceState != null){ authorText.setText(savedInstanceState.getString("author")); titleText.setText(savedInstanceState.getString("title")); descriptionText.setText(savedInstanceState.getString("description")); dateText.setText(savedInstanceState.getString("date")); ratingCountText.setText(savedInstanceState.getString("ratings")); int numStars = savedInstanceState.getInt("stars");//zero if null for(int s=0; s<numStars; s++){ starViews[s].setImageResource(R.drawable.star); starLayout.addView(starViews[s]); } starLayout.setTag(numStars); thumbImg = (Bitmap)savedInstanceState.getParcelable("thumbPic"); thumbView.setImageBitmap(thumbImg); previewBtn.setTag(savedInstanceState.getString("isbn")); if(savedInstanceState.getBoolean("isEmbed")) previewBtn.setEnabled(true); else previewBtn.setEnabled(false); if(savedInstanceState.getInt("isLink")==View.VISIBLE) linkBtn.setVisibility(View.VISIBLE); else linkBtn.setVisibility(View.GONE); previewBtn.setVisibility(View.VISIBLE); }
Again, we will not go into detail on this. The code simply carries out what the code already does immediately after scanning. In this case, it's following a state change. Extend the opening tag of your main activity element in the project manifest to handle configuration changes.
android:configChanges="orientation|keyboardHidden|screenSize"
That completes the app! You should be able to run it at this point. When it launches, press the button and scan a book. If the book is listed in Google Books, the relevant information should appear within the interface. If certain data items are missing, the app should still display the others. If there is a preview available, the preview button should be enabled and should launch the embedded book activity.
Conclusion
In this series we built on the foundation from Android SDK: Create a Barcode Scanner to create a book scanner app in conjunction with the ZXing scanning library and Google Books API. Experiment with the app by trying to retrieve and display different items of data from the returned JSON feed or by responding to scans of different barcode types, rather than just EANs. Using external resources via APIs is a key skill in any development discipline, as it allows you to make the best use of existing data and functionality in order to focus on the unique details of your own apps.
Comments