Homework 2 – Zebrary book scanner
Zebrary is a simple book-cataloging application that uses the camera of a device and an open-source barcode image processing library called ZXing (pronounced “Zebra Crossing”). The camera is to scan the barcode of a book, ZXing reads the ISBN number, and the Google Books API is used to gather bibliographic information about the volume, including a cover image URL.
Books are kept in an alphabetical list in the main Activity, and touching a list item drills down into a book detail view Activity that displays the author, title, cover image, a couple of links to Worldcat and Amazon for that specific book, and a button to scan a new book. Users can delete a book from the detail view or by long-touching an item in the list.
Using the ZXing Library
The setup for this took a few steps. First, you have to checkout source for /core and /android from the ZXing svn repository, build the /core project using ant, import the /android project into Eclipse and mark it as a Library project. After all this, you can use the ZXing /android project as a library in your own project. Damian Flannery has put together a very nice step-by-step blog post for doing this.
Once the environment is set up, you can launch an Intent based on Activities defined in the ZXing source. For example, the following launches a new SCAN Activity, brining up the camera
Intent intent = new Intent("com.google.zxing.client.android.SCAN");
intent.putExtra("SCAN_MODE", "PRODUCT_MODE");
startActivityForResult(intent, REQUEST_SCAN_NEW_BOOK);
Then, using onActivityResult(), grab the results of the scan and do whatever you like with them.
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_SCAN_NEW_BOOK && resultCode == RESULT_OK) {
String isbn = data.getStringExtra("SCAN_RESULT");
Toast.makeText(this, isbn, Toast.LENGTH_SHORT).show();
}
}
This sample code only displays a Toast message. The real code first checks the local SQLite database for a book matching that ISBN. If it finds a match, the user is taken to that book’s detail view. If not, we put together a URL string and issue an HTTP GET request:
String url = "https://www.googleapis.com/books/v1/volumes?q=isbn:" + isbn + "&key=" + GOOGLE_BOOKS_API_KEY;
InputStream input = null;
try {
HttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet(url);
HttpResponse response = client.execute(get);
HttpEntity result = response.getEntity();
input = result.getContent();
}
The results are used to create a new Book object and add its details to the local database.
Parsing JSON results from a web service
Once you have an API key, the Google Books API provides conveniently-wrapped bibliographic results in JSON format. If you’re never used JSON, it stands for JavaScript Object Notation and is a handy way of bundling up simple data into hierarchical key-value pairs, where the values can be strings, JSON objects, or lists of strings or JSON objects. Java has a built-in JSONObject class that provides methods for easy parsing. A sample JSON result from Google Books might look like this:
{
"publisher": "O'Reilly Media",
"authors": ["Ben Fry"],
"title": "Visualizing data",
"imageLinks": {
"smallThumbnail": "http:\/\/bks7.books.google.com\/books?id=6jsVAiULQBgC&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api",
"thumbnail": "http:\/\/bks7.books.google.com\/books?id=6jsVAiULQBgC&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api"
}
}
The real results have more layers of hierarchy and a lot more data, but this shows enough to get the idea. If we read the full result of the HTTP GET request into a string, we can use JSONObjects as follows:
JSONObject jObject = null;
String title, author, coverImageURLString;
try {
// Create a JSONObject from the text returned by the HTTP GET request.
jObject = new JSONObject(text);
// "title" is a key in the main, first-level JSONObject
title = jObject.getString("title"); // title = "Visualizing Data"
// "authors" is an array (i.e., enclosed in []), and we want the 0th element of that array
author = jObject.getJSONArray("authors").getString(0); // author = "Ben Fry"
// "imageLinks" is the key for a JSONObject inside the main JSONObject, and "thumbnail" is a key inside that
coverImageURLString = jObject.getJSONObject("imageLinks").getString("thumbnail");
}
Reading and writing images
With that, I create a Book object, insert it into the database, and write the cover image to a file. I do this in a lazy way, and BitmapFactory comes in handy.
String imageFileName = "/cover-image-" + book.getIsbn() + ".png";
Bitmap bitmap = null;
try {
File filesDirectory = getFilesDir();
String imageFilePath = filesDirectory.getPath() + imageFileName;
// Try decoding the file at that path
bitmap = BitmapFactory.decodeFile(imageFilePath);
// If there was no file, then we probably need to download it.
if (bitmap == null) {
// Grab the URL and use BitmapFactory.decodeStream() to get the image
URL url = new URL(book.getCoverImageURL());
bitmap = BitmapFactory.decodeStream((InputStream) url.getContent());
// Now that we have it, write it to a file
FileOutputStream outputStream = openFileOutput(imageFileName, MODE_PRIVATE);
bitmap.compress(CompressFormat.PNG, 100, outputStream);
outputStream.close();
}
// Finally, set the ImageView's bitmap
image.setImageBitmap(bitmap);
}
Controlling the flow of Intents
One problem I came across was the difference in flow between a new book scan originated from the list view and one originated from a book detail view. In both cases, when the user taps back from the new book’s detail view Activity, I wanted them to return to the list view. A number of flags can be set on an Intent using Intent.setFlags(), including FLAG_ACTIVITY_CLEAR_TOP, which pops the launching Activity off the stack, so to speak, before starting the new intent.
Intent intent = new Intent(this, BookDetailViewActivity.class);
intent.putExtra("id", book.getId());
// Setting the FLAG_ACTIVITY_CLEAR_TOP flag means the user won't return to
// this Activity, but rather, to whatever Activity started this one
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);