Cache and pre-cache challenges


#1

These are the things that I found out while working on these challenges.

  1. Sometimes I got null url, so I had to check for them because it would crash the app when checking the cache as I used the url as the key.
  2. I implemented the cache on a ‘singleton’ with a getInstance method. that way I can use it from both threads and any class.
  3. For the pre-cache, I added a couple methods to request the pre-cache to the thumbnailDownloader. I did that because I wanted to avoid downloading the same image and adding it to the cache more than once at a time. If adding and downloading to the cache is done on the same thread, that is easily avoided by checking the cache first.
  4. adding a message constant to request a pre-cache (MESSAGE_PRELOAD_CACHE). Instead of using the Token for the message object, I used the url, and it needs not go into the hashmap. Handling such message lets me deposit the bitmap on the cache, nut as there is no token (ImageView), nothing happens on the interface
  5. Doing things this way had the side effect that while processing the pre-cache messages, the thumbnails on the screen were not updated. I needed some kind of message priority. For this I used HandrlerThread’s sendMessageAtFrontOfQueue method. This allows me to get the MESSAGE_DOWNLOAD messages be processed before any MESSAGE_PRELOAD_CACHE pending message.

However, the documentation says about sendMessageAtFrontOfQueue that it puts a message in the front of the message queue but
"This method is only for use in very special circumstances – it can easily starve the message queue, cause ordering problems, or have other unexpected side-effects."

I think what it means is if I continually put messages for DOWNLOAD and the front, PRELOAD_CACHE messages may never be executed, but as MESSAGE_DOWNLOAD downloads and adds to the cache anyway, there is no much problem.
Not sure what other problems this may cause.

ThumbnailDownloader

package mx.org.dabicho.photogallery;


import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.util.Log;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Hilo que descarga las imágenes bajo demanda
 */
public class ThumbnailDownloader<Token> extends HandlerThread {
    private static final String TAG = "ThumbnailDownloader";
    private static final int MESSAGE_DOWNLOAD = 0;
    private static final int MESSAGE_PRELOAD_CACHE=1;

    public static final String PRELOAD_CACHE_URL="org.mx.dabicho.thumbnailUrl";


    Handler mHandler;
    Handler mResponseHandler;

    Map<Token, String> requestMap = Collections.synchronizedMap(new HashMap<Token, String>());

    Listener<Token> mListener;

    public interface Listener<Token> {
        void onThumbnailDownloaded(Token token, Bitmap thumbnail);
    }

    public void setListener(Listener<Token> listener) {
        mListener = listener;
    }

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

    }

    @Override
    protected void onLooperPrepared() {
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == MESSAGE_DOWNLOAD) {

                    Token token = (Token) msg.obj;
                    Log.i(TAG, "Got a request for url: " + requestMap.get(token));
                    handleRequest(token);
                } else if(msg.what == MESSAGE_PRELOAD_CACHE) {
                    String url =(String) msg.obj;
                    if(url!=null && BitmapCacheManager.getInstance().get(url)==null) {
                        Log.i(TAG, "Got a request to cache: " + url);
                        handleCacheRequest(url);
                    } else
                        Log.i(TAG, "Got a request to cache: "+url+" already present");
                }
            }
        };
        super.onLooperPrepared();
    }

    private void handleCacheRequest(String url){

            try {
                final byte[] bitmapBytes;
                final Bitmap lBitmap;

                bitmapBytes = new FlickrFetcher().getUrlBytes(url);
                lBitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);


                BitmapCacheManager.getInstance().put(url, lBitmap);
            } catch (IOException ioe) {
                Log.e(TAG, "Error downloading image", ioe);
            }

    }

    private void handleRequest(final Token token) {
        try {
            final String url = requestMap.get(token);
            if (url == null)
                return;
            final byte[] bitmapBytes;
            final Bitmap lBitmap;

            bitmapBytes = new FlickrFetcher().getUrlBytes(url);


            lBitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);


            BitmapCacheManager.getInstance().put(url, lBitmap);
            Log.i(TAG, "Bitmap created");


            mResponseHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (requestMap.get(token) != url) {
                        return;
                    }
                    requestMap.remove(token);
                    mListener.onThumbnailDownloaded(token, lBitmap);
                }
            });

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

    public void queuePreloadCache(String url) {
        Log.i(TAG,"Got a request for pre-cache: "+url);
        if (!mHandler.hasMessages(MESSAGE_PRELOAD_CACHE, url))
            mHandler.obtainMessage(MESSAGE_PRELOAD_CACHE, url).sendToTarget();
    }

    public void queueThumbnail(Token token, String url) {
        Log.i(TAG, "Got an URL: " + url);
        requestMap.put(token, url);
        if (!mHandler.hasMessages(MESSAGE_DOWNLOAD, token)) {
            Message message=mHandler.obtainMessage(MESSAGE_DOWNLOAD, token);
            mHandler.sendMessageAtFrontOfQueue(message);
        }

    }

    public void clearQueue() {
        mHandler.removeMessages(MESSAGE_DOWNLOAD);
        mHandler.removeMessages(MESSAGE_PRELOAD_CACHE);
        requestMap.clear();
    }
}

PhotoGalleryFragment:

package mx.org.dabicho.photogallery;

import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import java.io.IOException;
import java.util.ArrayList;

import mx.org.dabicho.photogallery.model.GalleryItem;

/**
 * Fragmento principal de la galer+ía
 */
public class PhotoGalleryFragment extends Fragment {
    private static final String TAG = "PhotoGalleryFragment";
    private static final int MINIMUM_REMAINING_IMAGES=30;

    private ThumbnailDownloader<ImageView> mViewThumbnailDownloader;
    private int lastPageSize = 0;


    GridView mGridView;
    ArrayList<GalleryItem> mItems;

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        mViewThumbnailDownloader = new ThumbnailDownloader<ImageView>(new Handler());
        mViewThumbnailDownloader.setListener(new ThumbnailDownloader.Listener<ImageView>() {

            @Override
            public void onThumbnailDownloaded(ImageView imageView, Bitmap thumbnail) {
                if (isVisible())
                    imageView.setImageBitmap(thumbnail);
            }
        });
        mViewThumbnailDownloader.start();
        mViewThumbnailDownloader.getLooper();

        Log.i(TAG, "ThumbnailDownloader thread started");
        new FetchItemsTask().execute();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.fragment_photo_gallery, container, false);
        mGridView = (GridView) v.findViewById(R.id.gridView);

        setUpAdapter();

        return v;
    }

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

    @Override
    public void onDestroy() {
        super.onDestroy();

        mViewThumbnailDownloader.quit();
        Log.i(TAG, "Background thumbnailDownloader thread destroyed");
    }

    private class FetchItemsTask extends AsyncTask<Void, Void, ArrayList<GalleryItem>> {

        @Override
        protected ArrayList<GalleryItem> doInBackground(Void... params) {

            return new FlickrFetcher().fetchItems();

        }

        @Override
        protected void onPostExecute(ArrayList<GalleryItem> galleryItems) {
            lastPageSize = galleryItems.size();
            if (mItems == null) {
                mItems = galleryItems;
                setUpAdapter();
            } else {
                mItems.addAll(galleryItems);
                ((ArrayAdapter) mGridView.getAdapter()).notifyDataSetChanged();
            }

        }
    }

    void setUpAdapter() {

        if (getActivity() == null || mGridView == null) {
            return;
        }

        if (mItems != null) {

            mGridView.setAdapter(new GalleryItemAdapter(mItems));

        } else {

            mGridView.setAdapter(null);
        }


    }

    private class GalleryItemAdapter extends ArrayAdapter<GalleryItem> {
        public GalleryItemAdapter(ArrayList<GalleryItem> items) {
            super(getActivity(), 0, items);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = getActivity().getLayoutInflater().inflate(R.layout.gallery_item, parent, false);
            }

            ImageView imageView = (ImageView) convertView.findViewById(R.id.gallery_item_imageView);
            if(MINIMUM_REMAINING_IMAGES==mItems.size()-position)
                new FetchItemsTask().execute();
            GalleryItem lItem = getItem(position);
            if(lItem.getUrl()==null)
                return convertView;
            if (BitmapCacheManager.getInstance().get(lItem.getUrl()) == null) {
                imageView.setImageResource(R.drawable.brian_up_close);
                mViewThumbnailDownloader.queueThumbnail(imageView, lItem.getUrl());
            } else {
                imageView.setImageBitmap(BitmapCacheManager.getInstance().get(lItem.getUrl()));
            }
            int limit= getCount()>=position+19?position+19:getCount()-1;
            for(int i=position+1; i<limit; i++) {


                if(getItem(i).getUrl()==null)
                    continue;

                mViewThumbnailDownloader.queuePreloadCache(getItem(i).getUrl());
            }
            return convertView;
        }
    }

}

Edit: Fixed array out of bounds exception when pre-caching