Sunday, November 22, 2009

Displaying Images from SD Card In Android - Part 2

In one of my previous posts, I wrote about how to fetch and display images from the SD card. The problem with the previous post is that one would have to wait until the first couple of images are available and shown on the screen. This implies that when the user wants to see the images, he will wait a couple of seconds until the first screen of images is available. The code that I'm going to post here works more like the Gallery application, meaning that one image at a time will be displayed on the screen. To achieve this effect, I used an AsyncTask, which fetches one image at a time in the background, and adds that image to the grid view during the progress update.

package blog.android.sdcard2;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.AdapterView.OnItemClickListener;

/**
 * Loads images from SD card. 
 * 
 * @author Mihai Fonoage
 *
 */
public class LoadImagesFromSDCardActivity extends Activity implements
OnItemClickListener {
    
    /**
     * Grid view holding the images.
     */
    private GridView sdcardImages;
    /**
     * Image adapter for the grid view.
     */
    private ImageAdapter imageAdapter;
    /**
     * Display used for getting the width of the screen. 
     */
    private Display display;

    /**
     * Creates the content view, sets up the grid, the adapter, and the click listener.
     * 
     * @see android.app.Activity#onCreate(android.os.Bundle)
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);        
        // Request progress bar
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.sdcard);

        display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

        setupViews();
        setProgressBarIndeterminateVisibility(true); 
        loadImages();
    }

    /**
     * Free up bitmap related resources.
     */
    protected void onDestroy() {
        super.onDestroy();
        final GridView grid = sdcardImages;
        final int count = grid.getChildCount();
        ImageView v = null;
        for (int i = 0; i < count; i++) {
            v = (ImageView) grid.getChildAt(i);
            ((BitmapDrawable) v.getDrawable()).setCallback(null);
        }
    }
    /**
     * Setup the grid view.
     */
    private void setupViews() {
        sdcardImages = (GridView) findViewById(R.id.sdcard);
        sdcardImages.setNumColumns(display.getWidth()/95);
        sdcardImages.setClipToPadding(false);
        sdcardImages.setOnItemClickListener(LoadImagesFromSDCardActivity.this);
        imageAdapter = new ImageAdapter(getApplicationContext()); 
        sdcardImages.setAdapter(imageAdapter);
    }
    /**
     * Load images.
     */
    private void loadImages() {
        final Object data = getLastNonConfigurationInstance();
        if (data == null) {
            new LoadImagesFromSDCard().execute();
        } else {
            final LoadedImage[] photos = (LoadedImage[]) data;
            if (photos.length == 0) {
                new LoadImagesFromSDCard().execute();
            }
            for (LoadedImage photo : photos) {
                addImage(photo);
            }
        }
    }
    /**
     * Add image(s) to the grid view adapter.
     * 
     * @param value Array of LoadedImages references
     */
    private void addImage(LoadedImage... value) {
        for (LoadedImage image : value) {
            imageAdapter.addPhoto(image);
            imageAdapter.notifyDataSetChanged();
        }
    }
    
    /**
     * Save bitmap images into a list and return that list. 
     * 
     * @see android.app.Activity#onRetainNonConfigurationInstance()
     */
    @Override
    public Object onRetainNonConfigurationInstance() {
        final GridView grid = sdcardImages;
        final int count = grid.getChildCount();
        final LoadedImage[] list = new LoadedImage[count];

        for (int i = 0; i < count; i++) {
            final ImageView v = (ImageView) grid.getChildAt(i);
            list[i] = new LoadedImage(((BitmapDrawable) v.getDrawable()).getBitmap());
        }

        return list;
    }
    /**
     * Async task for loading the images from the SD card. 
     * 
     * @author Mihai Fonoage
     *
     */
    class LoadImagesFromSDCard extends AsyncTask<Object, LoadedImage, Object> {
        
        /**
         * Load images from SD Card in the background, and display each image on the screen. 
         *  
         * @see android.os.AsyncTask#doInBackground(Params[])
         */
        @Override
        protected Object doInBackground(Object... params) {
            //setProgressBarIndeterminateVisibility(true); 
            Bitmap bitmap = null;
            Bitmap newBitmap = null;
            Uri uri = null;            

            // Set up an array of the Thumbnail Image ID column we want
            String[] projection = {MediaStore.Images.Thumbnails._ID};
            // Create the cursor pointing to the SDCard
            Cursor cursor = managedQuery( MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,
                    projection, // Which columns to return
                    null,       // Return all rows
                    null,       
                    null); 
            int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Thumbnails._ID);
            int size = cursor.getCount();
            // If size is 0, there are no images on the SD Card.
            if (size == 0) {
                //No Images available, post some message to the user
            }
            int imageID = 0;
            for (int i = 0; i < size; i++) {
                cursor.moveToPosition(i);
                imageID = cursor.getInt(columnIndex);
                uri = Uri.withAppendedPath(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, "" + imageID);
                try {
                    bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
                    if (bitmap != null) {
                        newBitmap = Bitmap.createScaledBitmap(bitmap, 70, 70, true);
                        bitmap.recycle();
                        if (newBitmap != null) {
                            publishProgress(new LoadedImage(newBitmap));
                        }
                    }
                } catch (IOException e) {
                    //Error fetching image, try to recover
                }
            }
            cursor.close();
            return null;
        }
        /**
         * Add a new LoadedImage in the images grid.
         *
         * @param value The image.
         */
        @Override
        public void onProgressUpdate(LoadedImage... value) {
            addImage(value);
        }
        /**
         * Set the visibility of the progress bar to false.
         * 
         * @see android.os.AsyncTask#onPostExecute(java.lang.Object)
         */
        @Override
        protected void onPostExecute(Object result) {
            setProgressBarIndeterminateVisibility(false);
        }
    }

    /**
     * Adapter for our image files. 
     * 
     * @author Mihai Fonoage
     *
     */
    class ImageAdapter extends BaseAdapter {

        private Context mContext; 
        private ArrayList<LoadedImage> photos = new ArrayList<LoadedImage>();

        public ImageAdapter(Context context) { 
            mContext = context; 
        } 

        public void addPhoto(LoadedImage photo) { 
            photos.add(photo); 
        } 

        public int getCount() { 
            return photos.size(); 
        } 

        public Object getItem(int position) { 
            return photos.get(position); 
        } 

        public long getItemId(int position) { 
            return position; 
        } 

        public View getView(int position, View convertView, ViewGroup parent) { 
            final ImageView imageView; 
            if (convertView == null) { 
                imageView = new ImageView(mContext); 
            } else { 
                imageView = (ImageView) convertView; 
            } 
            imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
            imageView.setPadding(8, 8, 8, 8);
            imageView.setImageBitmap(photos.get(position).getBitmap());
            return imageView; 
        } 
    }

    /**
     * A LoadedImage contains the Bitmap loaded for the image.
     */
    private static class LoadedImage {
        Bitmap mBitmap;

        LoadedImage(Bitmap bitmap) {
            mBitmap = bitmap;
        }

        public Bitmap getBitmap() {
            return mBitmap;
        }
    }
    /**
     * When an image is clicked, load that image as a puzzle. 
     */
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {        
        int columnIndex = 0;
        String[] projection = {MediaStore.Images.Media.DATA};
        Cursor cursor = managedQuery( MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,
                projection,
                null, 
                null, 
                null);
        if (cursor != null) {
            columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            cursor.moveToPosition(position);
            String imagePath = cursor.getString(columnIndex); 

            FileInputStream is = null;
            BufferedInputStream bis = null;
            try {
                is = new FileInputStream(new File(imagePath));
                bis = new BufferedInputStream(is);
                Bitmap bitmap = BitmapFactory.decodeStream(bis);
                Bitmap useThisBitmap = Bitmap.createScaledBitmap(bitmap, parent.getWidth(), parent.getHeight(), true);
                bitmap.recycle();
                //Display bitmap (useThisBitmap)
            } 
            catch (Exception e) {
                //Try to recover
            }
            finally {
                try {
                    if (bis != null) {
                        bis.close();
                    }
                    if (is != null) {
                        is.close();
                    }
                    cursor.close();
                    projection = null;
                } catch (Exception e) {
                }
            }
        }
    }

}

The sdcard.xml file:

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    
    <GridView  
        android:id="@+id/sdcard"
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent"
        android:verticalSpacing="10dp"
        android:horizontalSpacing="10dp" 
        android:stretchMode="columnWidth"
        android:gravity="center" />
        
</FrameLayout>    

That's it. Let me know if you have any questions.

Enjoy!

Wednesday, November 18, 2009

Gartner's Top 10 Mobile Apps for 2012

Gartner has released its top 10 applications for 2012.

"Consumer mobile applications and services are no longer the prerogative of mobile carriers,” said Sandy Shen, research director at Gartner. “The increasing consumer interest in smartphones, the participation of Internet players in the mobile space, and the emergence of application stores and cross-industry services are reducing the dominance of mobile carriers. Each player will influence how the application is delivered and experienced by consumers, who ultimately vote with their attention and spending power".

LBS applications are at number 2. The LBS user base is predicted to grow to 526 million by 2012 (from 96 million in 2009). NFC is at number 7. I read here that some new version of iPhone is supposed to include NFC support (actually support for proximity), and I do believe it's about time for the mobile industry to get behind this technology.

You can read the entire report at Gartner.

Friday, November 13, 2009

Engineering Achievements and Challenges

The National Academy of Engineering (NAE) put together two lists: one that ranks the 20 greatest engineering achievements of the 20th century, and one with engineering's challenges to be accomplished:


SOURCE: IEEE Spectrum

Thursday, November 12, 2009

Android Sliding Puzzle

Yesterday I released my first Android application into the Android Market. The application is a sliding puzzle game, and comes into two flavors, a free (lite) version (called "Sliding Puzzle Lite"), and a full version ("Sliding Puzzle Full") at $0.99. The lite version has only the basic features, such as 9 pre-loaded images and the ability to view the full (final) image during game play. The full version has the following features:
  • pre-loaded with 9 images
  • 4 difficulty levels (3x3, 4x4, 5x5, 6x6)
  • shuffle puzzle image
  • view final image
  • load image from SD Card
  • take a picture using the Camera and load it as a puzzle
  • displays a timer during game play 
  • displays how much percentage of the puzzle is done
  • displays the number of moves made
Below are some screen shots of the application:



I want to make some observations about the entire process.

First, the design was far from optimum in the first iteration; it was too complex for a one-screen game (one main screen, namely the puzzle screen). I started refactoring when I had to add some features and I noticed how hard it was to do just that. I ended up getting rid of some classes, methods, and instance variables all together, simplifying the design. Efficiency is crucial when developing for mobile devices. My background is in Java ME, where the restrictions are even bigger than on Android-based smartphones. For example, because I had to work with key-value pairs, instinctively I first went with a HashMap. I notice that the key-value pairs where actually pairs of integers, so I switched to using a SparseIntArray, which is more efficient that a HashMap. An even better approach would have been to use to parallel arrays of ints, but that's exactly how the SparseIntArray class is implemented, thank you for that! Overall, I tried to follow the guidelines form Designing for Performance document. Hence, the entire process was an iterative/incremental one, based on add feature <=> test feature <=> fix bugs <=> refactor (I hope the model does not look like a waterfall one because it wasn't :)). I noticed that testing and fixing bugs took the most time (and I am sure I still have bugs left unnoticed by me). I have been testing for the past month (and I've been doing all this in my spare time, being that I am working on my PhD also). Testing was still done manually (I hate that I do not use some kind of unit testing in Android). My biggest problem was that I was getting out of memory exceptions when loading bitmaps (especially when loading from SD card). I ended up using Bitmap.createScaledBitmap method to scale the bitmaps down. Furthermore, having the bitmaps displayed on the screen was initially done in a 'bulk' manner, where the screen would freeze until the first couple of images could be displayed. That's not user friendly at all; reading some blogs from smarter people, I ended up using a AsyncTask, where each image I would load, I would display on the screen, one at a time. With that, you would images being loaded on the display, similar to how the Gallery application (that comes pre-loaded on the phone) works.

Second, nothing beats testing on a real device. Emulators do come close, but having a device made quite a difference. For example, on some G1 phones, the camera software has a known bug, namely that the camera preview does not work in portrait mode. This issue is of course non-existent in the emulator.

I keep telling fellow students that I work with (or teach to), that nothing beats writing applications. Sure, you can read books (which of course you have to do), run examples, but if you do not start coding, you will never get to the next level as a programmer.

There are other lessons I have learned, and maybe I will post them in a later post.

Overall, I am looking forward to adding new features to the full version of the game (such as speech recognition, integration with Facebook, and many more), and also developing the next game from the list of ideas that I have.

I hope you enjoy the sliding puzzle, at least as much as I enjoyed developing it!

Saturday, November 7, 2009