teaching machines

Homework 2 – Zebrary book scanner

October 17, 2011 by . Filed under cs491 mobile, fall 2011, postmortems.

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);