Messages and Message Handlers

Before you create a message, you need to understand what a Message is and the relationship it has with its Handler (often called its message handler).

Message anatomy

Let’s start by looking closely at messages. The messages that a Flash might put in an inbox (its own inbox or that of another Flash) are not supportive notes, like, “You run very fast, Flash.” They are tasks that need to be handled.

A message is an instance of Message and contains several fields. Three are relevant to your implementation:

what

a user-defined int that describes the message

obj

a user-specified object to be sent with the message

target

the Handler that will handle the message

The target of a Message is an instance of Handler. You can think of the name Handler as being short for message handler. When you create a Message, it will automatically be attached to a Handler. And when your Message is ready to be processed, Handler will be the object in charge of making it happen.

Handler anatomy

To do any real work with messages, you will need an instance of Handler first. A Handler is not just a target for processing your Messages. A Handler is your interface for creating and posting Messages, too. Take a look at Figure 26.4.

Figure 26.4  Looper, Handler, HandlerThread, and Messages

Figure shows a handler and a looper.

Messages must be posted and consumed on a Looper, because Looper owns the inbox of Message objects. So Handler always has a reference to its coworker, the Looper.

A Handler is attached to exactly one Looper, and a Message is attached to exactly one target Handler, called its target. A Looper has a whole queue of Messages. Multiple Messages can reference the same target Handler (Figure 26.4).

Multiple Handlers can be attached to one Looper (Figure 26.5). This means that your Handler’s Messages may be living side by side with another Handler’s messages.

Figure 26.5  Multiple Handlers, one Looper

Figure shows multiple handlers and one looper.

Using handlers

Usually, you do not set a message’s target Handler by hand. It is better to build the message by calling Handler.obtainMessage(…). You pass the other message fields into this method, and it automatically sets the target to the Handler object the method was called on for you.

Handler.obtainMessage(…) pulls from a common recycling pool to avoid creating new Message objects, so it is also more efficient than creating new instances.

Once you have obtained a Message, you call sendToTarget() to send the Message to its Handler. The Handler will then put the Message on the end of Looper’s message queue.

In this case, you are going to obtain a message and send it to its target within the implementation of queueThumbnail(). The message’s what will be a constant defined as MESSAGE_DOWNLOAD. The message’s obj will be an object of type T, which will be used to identify the download. In this case, obj will be the PhotoHolder that the adapter passed in to queueThumbnail().

When the looper pulls a Message from the queue, it gives the message to the message’s target Handler to handle. Typically, the message is handled in the target’s implementation of Handler.handleMessage(…).

Figure 26.6 shows the object relationships involved.

Figure 26.6  Creating a Message and sending it

Illustration shows creation and sending of messages.

In this case, your implementation of handleMessage(…) will use FlickrFetchr to download bytes from the URL and then turn these bytes into a bitmap.

First, add the constant and member variables as shown in Listing 26.7.

Listing 26.7  Adding constant and member variables (ThumbnailDownloader.java)

public class ThumbnailDownloader<T> extends HandlerThread {
    private static final String TAG = "ThumbnailDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;

    private boolean mHasQuit = false;
    private Handler mRequestHandler;
    private ConcurrentMap<T,String> mRequestMap = new ConcurrentHashMap<>();
    ...
}

MESSAGE_DOWNLOAD will be used to identify messages as download requests. (ThumbnailDownloader will set this as the what on any new download messages it creates.)

The newly added mRequestHandler will store a reference to the Handler responsible for queueing download requests as messages onto the ThumbnailDownloader background thread. This handler will also be in charge of processing download request messages when they are pulled off the queue.

The mRequestMap variable is a ConcurrentHashMap, a thread-safe version of HashMap. Here, using a download request’s identifying object of type T as a key, you can store and retrieve the URL associated with a particular request. (In this case, the identifying object is a PhotoHolder, so the request response can be easily routed back to the UI element where the downloaded image should be placed.)

Next, add code to queueThumbnail(…) to update mRequestMap and to post a new message to the background thread’s message queue.

Listing 26.8  Obtaining and sending a message (ThumbnailDownloader.java)

public class ThumbnailDownloader<T> extends HandlerThread {
    private static final String TAG = "ThumbnailDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;

    private boolean mHasQuit = false;
    private Handler mRequestHandler;
    private ConcurrentMap<T,String> mRequestMap = new ConcurrentHashMap<>();

    public ThumbnailDownloader() {
        super(TAG);
    }

    @Override
    public boolean quit() {
        mHasQuit = true;
        return super.quit();
    }

    public void queueThumbnail(T target, String url) {
        Log.i(TAG, "Got a URL: " + url);

        if (url == null) {
            mRequestMap.remove(target);
        } else {
            mRequestMap.put(target, url);
            mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
                    .sendToTarget();
        }
    }
}

You obtain a message directly from mRequestHandler, which automatically sets the new Message object’s target field to mRequestHandler. This means mRequestHandler will be in charge of processing the message when it is pulled off the message queue. The message’s what field is set to MESSAGE_DOWNLOAD. Its obj field is set to the T target value (a PhotoHolder in this case) that is passed to queueThumbnail(…).

The new message represents a download request for the specified T target (a PhotoHolder from the RecyclerView). Recall that PhotoGalleryFragment’s RecyclerView’s adapter implementation calls queueThumbnail(…) from onBindViewHolder(…), passing along the PhotoHolder the image is being downloaded for and the URL location of the image to download.

Notice that the message itself does not include the URL. Instead you update mRequestMap with a mapping between the request identifier (PhotoHolder) and the URL for the request. Later you will pull the URL from mRequestMap to ensure that you are always downloading the most recently requested URL for a given PhotoHolder instance. (This is important because ViewHolder objects in RecyclerViews are recycled and reused.)

Finally, initialize mRequestHandler and define what that Handler will do when downloaded messages are pulled off the queue and passed to it.

Listing 26.9  Handling a message (ThumbnailDownloader.java)

public class ThumbnailDownloader<T> extends HandlerThread {
    private static final String TAG = "ThumbnailDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;

    private boolean mHasQuit = false;
    private Handler mRequestHandler;
    private ConcurrentMap<T,String> mRequestMap = new ConcurrentHashMap<>();

    public ThumbnailDownloader() {
        super(TAG);
    }

    @Override
    protected void onLooperPrepared() {
        mRequestHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == MESSAGE_DOWNLOAD) {
                    T target = (T) msg.obj;
                    Log.i(TAG, "Got a request for URL: " + mRequestMap.get(target));
                    handleRequest(target);
                }
            }
        };
    }

    @Override
    public boolean quit() {
        mHasQuit = true;
        return super.quit();
    }

    public void queueThumbnail(T target, String url) {
        ...
    }

    private void handleRequest(final T target) {
        try {
            final String url = mRequestMap.get(target);

            if (url == null) {
                return;
            }

            byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);
            final Bitmap bitmap = BitmapFactory
                    .decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
            Log.i(TAG, "Bitmap created");

        } catch (IOException ioe) {
            Log.e(TAG, "Error downloading image", ioe);
        }
    }
}

You implemented Handler.handleMessage(…) in your Handler subclass within onLooperPrepared(). HandlerThread.onLooperPrepared() is called before the Looper checks the queue for the first time. This makes it a good place to create your Handler implementation.

Within Handler.handleMessage(…), you check the message type, retrieve the obj value (which will be of type T and serves as the identifier for the request), and then pass it to handleRequest(…). (Recall that Handler.handleMessage(…) will get called when a download message is pulled off the queue and ready to be processed.)

The handleRequest() method is a helper method where the downloading happens. Here you check for the existence of a URL. Then you pass the URL to a new instance of your old friend FlickrFetchr. In particular, you use the FlickrFetchr.getUrlBytes(…) method that you created with such foresight in the last chapter.

Finally, you use BitmapFactory to construct a bitmap with the array of bytes returned from getUrlBytes(…).

Run PhotoGallery and check Logcat for your confirming log statements.

Of course, the request will not be completely handled until you set the bitmap on the PhotoHolder that originally came from PhotoAdapter. However, this is UI work, so it must be done on the main thread.

Everything you have seen so far uses handlers and messages on a single thread – ThumbnailDownloader putting messages in ThumbnailDownloader’s own inbox. In the next section, you will see how ThumbnailDownloader can use a Handler to post requests to a separate thread (namely, the main thread).

Passing handlers

So far you are able to schedule work on the background thread from the main thread using ThumbnailDownloader’s mRequestHandler. This flow is shown in Figure 26.7.

Figure 26.7  Scheduling work on ThumbnailDownloader from the main thread

Illustration shows scheduling work on ThumbnailDownloader's thread from main Thread.

You can also schedule work on the main thread from the background thread using a Handler attached to the main thread. This flow looks like Figure 26.8.

Figure 26.8  Scheduling work on the main thread from ThumbnailDownloader’s thread

Illustration shows scheduling work on ThumbnailDownloader's thread from main Thread.

The main thread is a message loop with handlers and a Looper. When you create a Handler in the main thread, it will be associated with the main thread’s Looper. You can then pass that Handler to another thread. The passed Handler maintains its loyalty to the Looper of the thread that created it. Any messages the Handler is responsible for will be handled on the main thread’s queue.

In ThumbnailDownloader.java, add the mResponseHandler variable seen above to hold a Handler passed from the main thread. Then replace the constructor with one that accepts a Handler and sets the variable. Also, add a listener interface that will be used to communicate the responses (downloaded images) with the requester (the main thread).

Listing 26.10  Handling a message (ThumbnailDownloader.java)

public class ThumbnailDownloader<T> extends HandlerThread {
    private static final String TAG = "ThumbnailDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;

    private boolean mHasQuit = false;
    private Handler mRequestHandler;
    private ConcurrentMap<T,String> mRequestMap = new ConcurrentHashMap<>();
    private Handler mResponseHandler;
    private ThumbnailDownloadListener<T> mThumbnailDownloadListener;

    public interface ThumbnailDownloadListener<T> {
        void onThumbnailDownloaded(T target, Bitmap thumbnail);
    }

    public void setThumbnailDownloadListener(ThumbnailDownloadListener<T> listener) {
        mThumbnailDownloadListener = listener;
    }

    public ThumbnailDownloader(Handler responseHandler) {
        super(TAG);
        mResponseHandler = responseHandler;
    }
    ...
}

The onThumbnailDownloaded(…) method defined in your new ThumbnailDownloadListener interface will eventually be called when an image has been fully downloaded and is ready to be added to the UI. Using this listener delegates the responsibility of what to do with the downloaded image to a class other than ThumbnailDownloader (in this case, to PhotoGalleryFragment). Doing so separates the downloading task from the UI updating task (putting the images into ImageViews), so that ThumbnailDownloader could be used for downloading into other kinds of View objects as needed.

Next, modify PhotoGalleryFragment to pass a Handler attached to the main thread to ThumbnailDownloader. Also, set a ThumbnailDownloadListener to handle the downloaded image once it is complete.

Listing 26.11  Hooking up to response Handler (PhotoGalleryFragment.java)

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    new FetchItemsTask().execute();

    Handler responseHandler = new Handler();
    mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler);
    mThumbnailDownloader.setThumbnailDownloadListener(
        new ThumbnailDownloader.ThumbnailDownloadListener<PhotoHolder>() {
            @Override
            public void onThumbnailDownloaded(PhotoHolder photoHolder,
                                              Bitmap bitmap) {
                Drawable drawable = new BitmapDrawable(getResources(), bitmap);
                photoHolder.bindDrawable(drawable);
            }
        }
    );
    mThumbnailDownloader.start();
    mThumbnailDownloader.getLooper();
    Log.i(TAG, "Background thread started");
}

Remember that by default, the Handler will attach itself to the Looper for the current thread. Because this Handler is created in onCreate(…), it will be attached to the main thread’s Looper.

Now ThumbnailDownloader has access via mResponseHandler to a Handler that is tied to the main thread’s Looper. It also has your ThumbnailDownloadListener to do the UI work with the returning Bitmaps. Specifically, the onThumbnailDownloaded implementation sets the Drawable of the originally requested PhotoHolder to the newly downloaded Bitmap.

You could send a custom Message back to the main thread requesting to add the image to the UI, similar to how you queued a request on the background thread to download the image. However, this would require another subclass of Handler, with an override of handleMessage(…).

Instead, let’s use another handy Handler method – post(Runnable).

Handler.post(Runnable) is a convenience method for posting Messages that look like this:

Runnable myRunnable = new Runnable() {
    @Override
    public void run() {
        /* Your code here */
    }
};
Message m = mHandler.obtainMessage();
m.callback = myRunnable;

When a Message has its callback field set, it is not routed to its target Handler when pulled off the message queue. Instead, the run() method of the Runnable stored in callback is executed directly.

In ThumbnailDownloader.handleRequest(), add the following code.

Listing 26.12  Downloading and displaying images (ThumbnailDownloader.java)

public class ThumbnailDownloader<T> extends HandlerThread {
    ...
    private Handler mResponseHandler;
    private ThumbnailDownloadListener<T> mThumbnailDownloadListener;
    ...
    private void handleRequest(final T target) {
        try {
            final String url = mRequestMap.get(target);

            if (url == null) {
                return;
            }

            byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);
            final Bitmap bitmap = BitmapFactory
                    .decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
            Log.i(TAG, "Bitmap created");

            mResponseHandler.post(new Runnable() {
                public void run() {
                    if (mRequestMap.get(target) != url ||
                            mHasQuit) {
                        return;
                    }

                    mRequestMap.remove(target);
                    mThumbnailDownloadListener.onThumbnailDownloaded(target, bitmap);
                }
            });

        } catch (IOException ioe) {
            Log.e(TAG, "Error downloading image", ioe);
        }
    }
}

Because mResponseHandler is associated with the main thread’s Looper, all of the code inside of run() will be executed on the main thread.

So what does this code do? First, you double-check the requestMap. This is necessary because the RecyclerView recycles its views. By the time ThumbnailDownloader finishes downloading the Bitmap, RecyclerView may have recycled the PhotoHolder and requested a different URL for it. This check ensures that each PhotoHolder gets the correct image, even if another request has been made in the meantime.

Next, you check mHasQuit. If ThumbnailDownloader has already quit, it may be unsafe to run any callbacks.

Finally, you remove the PhotoHolder-URL mapping from the requestMap and set the bitmap on the target PhotoHolder.

Before running PhotoGallery and seeing your hard-won images, there is one last danger you need to account for. If the user rotates the screen, ThumbnailDownloader may be hanging on to invalid PhotoHolders. Bad things will happen if the corresponding ImageViews get pressed.

Write a clearQueue() method to clean all the requests out of your queue.

Listing 26.13  Adding cleanup method (ThumbnailDownloader.java)

public class ThumbnailDownloader<T> extends HandlerThread {
    ...
    public void queueThumbnail(T target, String url) {
        ...
    }

    public void clearQueue() {
        mRequestHandler.removeMessages(MESSAGE_DOWNLOAD);
        mRequestMap.clear();
    }

    private void handleRequest(final T target) {
        ...
    }
}

Then clean out your downloader in PhotoGalleryFragment when your view is destroyed.

Listing 26.14  Calling cleanup method (PhotoGalleryFragment.java)

public class PhotoGalleryFragment extends Fragment {
  ...
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
      ...
  }

  @Override
  public void onDestroyView() {
      super.onDestroyView();
      mThumbnailDownloader.clearQueue();
  }

  @Override
  public void onDestroy() {
      ...
  }
  ...
}

With that, your work for this chapter is complete. Run PhotoGallery. Scroll around to see images dynamically loading.

PhotoGallery has achieved its basic goal of displaying images from Flickr. In the next few chapters, you will add more functionality, like searching for photos and opening each photo’s Flickr page in a web view.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.64.132