androidmessage-queuebackground-threadandroid-looperandroid-handlerthread

NullPointerException in HandlerThread


This bug baffled me for hours. I am getting the NullPointerException. The problem is this error is not consistent. It happens when I launch the app, but only occasionally. So I am not sure what is causing it.

I apologize for the verbose question with the error log, but I could not find another way of asking.

The error log is as follows:

FATAL EXCEPTION: main
Process: com.myproject.android, PID: 22175
java.lang.NullPointerException
    at com.myproject.android.ImageDownloaderThread.queueImage(ImageDownloaderThread.java:74)
    at com.myproject.android.NewsItemPagerActivity$NewsItemFragmentStatePagerAdapter.getItem(NewsItemPagerActivity.java:325)
    at android.support.v13.app.FragmentStatePagerAdapter.instantiateItem(FragmentStatePagerAdapter.java:109)
    at android.support.v4.view.ViewPager.addNewItem(ViewPager.java:832)
    at android.support.v4.view.ViewPager.populate(ViewPager.java:982)
    at android.support.v4.view.ViewPager.populate(ViewPager.java:914)
    at android.support.v4.view.ViewPager.onMeasure(ViewPager.java:1436)
    at android.view.View.measure(View.java:16497)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5125)
    at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
    at android.view.View.measure(View.java:16497)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5125)
    at com.android.internal.widget.ActionBarOverlayLayout.onMeasure(ActionBarOverlayLayout.java:327)
    at android.view.View.measure(View.java:16497)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5125)
    at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
    at com.android.internal.policy.impl.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2291)
    at android.view.View.measure(View.java:16497)
    at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:1912)
    at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1109)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1291)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:996)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5600)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761)
    at android.view.Choreographer.doCallbacks(Choreographer.java:574)
    at android.view.Choreographer.doFrame(Choreographer.java:544)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:747)
    at android.os.Handler.handleCallback(Handler.java:733)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:136)
    at android.app.ActivityThread.main(ActivityThread.java:5001)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
    at dalvik.system.NativeStart.main(Native Method)

And the code where this is happening is shown below:

package com.myproject.android;

import java.io.IOException;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;

/*
 * This class is used to download images in the background thread
 */
public class ImageDownloaderThread<Token> extends HandlerThread {

    private static final String TAG = "ImageDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;

    // This is the handler attached to the looper
    Handler mHandler; 






    // The is used as a reference to the main UI thread's handler
    Handler mResponseHandler;

    // This is a listener object that is used to update the main UI thread with the image that is downloaded
    Listener mListener;

    // This is the interface needed when a listener is created. It forces an implementation of the callback in the main UI thread
    public interface Listener {
        void onImageDownloaded(Bitmap image, int pos);
    }

    // Set the listener
    public void setListener(Listener listener) {
        mListener = listener;
    }





    // Constructor
    public ImageDownloaderThread(Handler responseHandler) {
        super(TAG);
        mResponseHandler = responseHandler; // Set the response handler to the one passed from the main thread
    }


    // This method executes some setup before Looper loops for each message
    @Override
    protected void onLooperPrepared() {

        // Create a message handler to handle the message queue
        mHandler = new MessageHandler(ImageDownloaderThread.this);
    }


    // This method is used to add a message to the message queue, so that it can be handled later
    // ... this method is called by the main UI thread to add the message to the queue of the current thread to be handled later
    public void queueImage(String url, int pos) {

        mHandler
            .obtainMessage(MESSAGE_DOWNLOAD, pos, 0, url)
            .sendToTarget();
    }





    // This method is used to download the image  
    private void handleRequest(String url, int pos) {

        try {

            // first check if the url is empty. if it is, then return
            if (url == null) {
                return;
            }

            // Download the image
            byte[] bitmapBytes = new NewsItemsFetcher().getUrlBytes(url);

            // Generate a bitmap
            final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);

            // Set position as 'final'
            final int position = pos;


            // We are using mResponseHandler.post(Runnable) to send a message to the response handler
            // This message will eventually result in the main thread updating the UI with the image
            mResponseHandler.post(new Runnable() {
                @Override
                public void run() {                 
                    mListener.onImageDownloaded(bitmap, position);

                }
            });

        }

        catch (HttpResponseException httpe) {
            // TODO: Handle http response not OK
            Log.e(TAG, "Error in server response", httpe);
        }

        catch (IOException ioe) {
            // TODO: Handle download error
            Log.e(TAG, "Error downloading image", ioe);
        }

    }


    class MessageHandler extends Handler {

        private final ImageDownloaderThread<Token> mImageDownloader;

        MessageHandler(ImageDownloaderThread<Token> imageDownloader) {
            mImageDownloader = imageDownloader;
        }

        // This method is used to process the message that is waiting in the queue 
        @Override
        public void handleMessage(Message msg) {

            // First, check if the message is to download an image
            if (msg.what == MESSAGE_DOWNLOAD) {

                // Call the handleRequest() function which will eventually download the image
                String url = (String)msg.obj;
                int pos = msg.arg1;


                if (mImageDownloader != null) {
                    mImageDownloader.handleRequest(url, pos);
                }

            }
        }

    }

}

In case you are wondering, line 74 in the error log (more specifically, this at com.myproject.android.ImageDownloaderThread.queueImage(ImageDownloaderThread.java:74), references the .obtainMessage(MESSAGE_DOWNLOAD, pos, 0, url) line of code in queueImage()


EDIT

According to a suggestion in Loop's answer, mHandler is null when queueImage() is called. So, how can I guarantee mHandler to be intialized by onLooperPrepared() before executing any queueImage() call?


Solution

  • The only reason for me would be that queueImage() method is called before onLooperPrepared() so mHandler is not initialize.

    Update

    HandlerThread is simply a Thread with implementation of the run() method where onLooperPrepared() is called.

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();//It's HERE
        Looper.loop();
        mTid = -1;
    }
    

    So when it's called depends on starting this thread. If you start it and immediately call public method on the reference of this thread you may encounter a race condition and mHandler is not initialized on time.

    One solution would be a delay for start processing images or playing with synchronization techniques. However, I would use much simpler way.

    Just to be clear, you want your mHandler to be initialized just after HandlerThread is created and you don't want to do it explicitly from the Main Activity where HandlerThread is created.

    Update 2

    Just come up with the following solution.

    queueImage() provides simple and light data. You could check if mHandler is null, if it's true add parameters of queueImage() to that queue. When onLoopPrepared() is called check if there is anything is that queue and process that data.

    private LinkedBlockingQueue<Pair<String,Integer>> mQueue = new LinkedBlockingQueue<Pair<String,Integer>>();
    
    public void queueImage(String url, int pos) {
        if (mHandler == null) {
            mQueue.put(new Pair<String,Integer>(url, pos));
            return;
        }
        mHandler
            .obtainMessage(MESSAGE_DOWNLOAD, pos, 0, url)
            .sendToTarget();
    }
    
    @Override
    protected void onLooperPrepared() {
    
        // Create a message handler to handle the message queue
        mHandler = new MessageHandler(ImageDownloaderThread.this);
        //TODO check the queue here, if there is data take it and process
        //you can call queueImage() once again for each queue item
        Pair<String, Integer> pair = null;
        while((pair = mQueue.poll()) != null) {
            queueImage(pair.first, pair.second);
        }
    }