Preloading challenge


#1

Hey guys,

I’m trying to do the preloading challenge from chapter 27. I did the first part okay (saving images to a cache) using the code from the android developers website. developer.android.com/training/d … itmap.html

In ThumbnailDownloader, I put the LruCache creation code in the onLooperPrepared section, then the addBitmapToMemoryCache part in handleRequest after first checking if the url was already in the cache, and that seemed to work fine.

However, I am stuck on the second part, where it asks me to preload the image. What I tried to do was put a for loop right before queueThumbnail in the getView method. [code]

	@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);
		imageView.setImageResource(R.drawable.brian_up_close);
		for (int i = 0; i < 10; i ++) {
		GalleryItem item = getItem(position+i);
		mThumbnailThread.queueThumbnail(imageView, item.getUrl()); 	
		}
		
		
		return convertView;
	} [/code]

I knew that this wasn’t very elegant but I thought it would at least work. However, it did not. The queueThumbnail method is being iterated, and the requests are being sent through: however, the bitmaps are not being created for url’s that aren’t in the imageView. So I was just wondering why this didn’t work, and was wondering what would work. Thanks for reading this!

Sincerely,
Mark


#2

It isn’t working because requests are tied to a specific ImageView. If you make ten requests using the same ImageView, only the last one will go through.

ThumbnailDownloader does this to limit the number of useless requests being made - imagine flinging your GridView really fast. It would immediately queue up a request for every single image, and you’d have to wait a long time to see the images for the views you’re actually seeing.


#3

That makes a lot of sense. I’m pretty stuck here: any hints on how to do this challenge?


#4

Well, I don’t want to spoil the solution for you. :slight_smile: I’ll say that I’d start by adding two things:

1- A separate prefetch interface.
2- A way to make prefetch downloads lower priority, or otherwise prevent them from hogging the thread.


#5

How about setting up a Prefetch AsyncTask like the FlickrFetchr, give it the mList, and let it chug away adding bitmaps to the cache in the background? Or if you want it to persist another HandlerThread?

I have found unlike Rick that keeping all the Cache management in the DL thread much simpler and straightforward than the trouble he has created for himself doing it on the main thread… Generally disassociating preloading of the cache as much as possible from UI activity is probably a good thing other than giving the Preloader a hint as to where in the mList to start filling the cache when you start it or having it get tips from the UI via messages that steer it on its preloading tasks.


#6

I am also stuck and feeling blue. Can you give us more hint?

Thanks!


#7

Here is my attempt at it. Code could be cleaned up but I think it is a working draft.

The following is a new ThumbnailCacheDownloader

[code]public class ThumbnailCacheDownloader extends HandlerThread {
private static final String TAG = “ThumbnailCacheDownloader”;
private static final int MESSAGE_CACHING = 1;
Map<String, String> requestMapCache = Collections
.synchronizedMap(new HashMap<String, String>());
Handler mHandler;

public ThumbnailCacheDownloader() {
	super(TAG);
	// TODO Auto-generated constructor stub
}

public void queueThumbnailCache(String id, String url) {
	// TODO Auto-generated method stub
	requestMapCache.put(id, url);
	mHandler.obtainMessage(MESSAGE_CACHING, id).sendToTarget();
}

@SuppressLint("HandlerLeak")
@Override
protected void onLooperPrepared() {
	// TODO Auto-generated method stub
	// super.onLooperPrepared();
	mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			// TODO Auto-generated method stub
			// super.handleMessage(msg);
			if (msg.what == MESSAGE_CACHING) {
				String id = (String) msg.obj;
				Log.i(TAG,
						"got to request for url @ id:" + requestMapCache.get(id)
								+ "-" + id);
				handleCachingRequest(id);
			}
		}
	};
}

private void handleCachingRequest(String id) {
	// TODO Auto-generated method stub
	try {
		final String url = requestMapCache.get(id);
		if (url == null)
			return;

		final Bitmap bitmap;

		if (myLruCache.getBitmapFromMemCache(url) != null) {
			bitmap = myLruCache.getBitmapFromMemCache(url);
			Log.i("Cache prefetch Test:", "found in cache");
		} else {
			byte[] bitMapByte = new FlickrFetchr().getUrlBytes(url);
			bitmap = BitmapFactory.decodeByteArray(bitMapByte, 0,
					bitMapByte.length);
			myLruCache.addBitmapToMemoryCache(url, bitmap);
			Log.i("Cache prefetch Test:", "put in cache");
			Log.i(TAG, "bitmap created");
		}
	} catch (IOException e) {
		Log.e(TAG, "Error loading image", e);
	}
}

public void clearQueue() {
	mHandler.removeMessages(MESSAGE_CACHING);
	requestMapCache.clear();
}

}[/code]

In PhotoGalleryFragment, created a new thread mThumbnailCacheThread and set it to a lower priority. Added the some “prefetching” code in getView of GalleryItem’s ArrayAdapter.

[code]public class PhotoGalleryFragment extends Fragment {
private String TAG = “PhotoGalleryFragment”;
GridView mGridView;
ArrayList mItems;

ThumbnailDownloader<ImageView> mThumbnailThread;
ThumbnailCacheDownloader mThumbnailCacheThread;
Handler mHandler;

@Override
public void onCreate(Bundle savedInstanceState) {
	// TODO Auto-generated method stub
	super.onCreate(savedInstanceState);
	setRetainInstance(true);
	new FetchItemsTask().execute();

	// Get max available VM memory, exceeding this amount will throw an
	// OutOfMemory exception. Stored in kilobytes as LruCache takes an
	// int in its constructor.
	final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

	// Use 1/8th of the available memory for this memory cache.
	final int cacheSize = maxMemory / 8;
	new myLruCache(cacheSize);

	mHandler = new Handler();
	mThumbnailThread = new ThumbnailDownloader<ImageView>(mHandler);
	mThumbnailThread.setPriority(5);
	mThumbnailThread.setListener(new Listener<ImageView>() {

		@Override
		public void onThumbnailLoaded(ImageView token, Bitmap thumbnail) {
			// TODO Auto-generated method stub
			if (isVisible()) {
				token.setImageBitmap(thumbnail);
			}
		}
	});
	mThumbnailThread.start();
	mThumbnailThread.getLooper();
	
	mThumbnailCacheThread = new ThumbnailCacheDownloader();
	mThumbnailCacheThread.setPriority(1);
	mThumbnailCacheThread.start();
	mThumbnailCacheThread.getLooper();
	Log.i(TAG, "Background thread started");
}

@Override
public void onDestroy() {
	// TODO Auto-generated method stub
	mThumbnailThread.quit();
	mThumbnailCacheThread.quit();
	Log.i(TAG, "Background thread destroyed");
	super.onDestroy();
}

@Override
public void onDestroyView() {
	// TODO Auto-generated method stub
	super.onDestroyView();
	mThumbnailThread.clearQueue();
	mThumbnailCacheThread.clearQueue();
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
		Bundle savedInstanceState) {
	// TODO Auto-generated method stub
	// return super.onCreateView(inflater, container, savedInstanceState);
	View v = inflater.inflate(R.layout.fragment_photo_gallery, container,
			false);
	mGridView = (GridView) v.findViewById(R.id.gridview);
	mGridView.setOnScrollListener(new OnScrollListener() {

		@Override
		public void onScrollStateChanged(AbsListView view, int scrollState) {
			// TODO Auto-generated method stub

		}

		@Override
		public void onScroll(AbsListView view, int firstVisibleItem,
				int visibleItemCount, int totalItemCount) {
			// TODO Auto-generated method stub
			Log.i(TAG, "Scrolled: 1st Item: " + firstVisibleItem
					+ " - visible Items: " + visibleItemCount
					+ " - totalItemCount: " + totalItemCount);
			if (firstVisibleItem + visibleItemCount == totalItemCount
					&& totalItemCount > 0 ) {
				new FetchItemsTask().execute();
			}
		}
	});
	setupAdapter();
	return v;
}

private class FetchItemsTask extends
		AsyncTask<Void, Void, ArrayList<GalleryItem>> {
	@Override
	protected ArrayList<GalleryItem> doInBackground(Void... params) {
		// TODO Auto-generated method stub
		return new FlickrFetchr().fetchItems();
	}

	// Because this is background task. Setup adapter here to ensure the
	// fetching of items are completed.
	@Override
	protected void onPostExecute(ArrayList<GalleryItem> result) {
		// TODO Auto-generated method stub
		// super.onPostExecute(result);
		// mItems = result;
		if (mItems != null) {
			mItems.addAll(result);
		} else {
			mItems = result;
		}
		setupAdapter();
	}
}

void setupAdapter() {
	if (getActivity() == null || mGridView == null)
		return;

	if (mItems != null) {
		ArrayAdapter<GalleryItem> adapter = new ArrayAdapter<GalleryItem>(
				getActivity(), android.R.layout.simple_gallery_item, mItems) {
			@Override
			public View getView(int position, View convertView,
					ViewGroup parent) {
				// TODO Auto-generated method stub
				// return super.getView(position, convertView, parent);
				if (convertView == null) {
					convertView = getActivity().getLayoutInflater()
							.inflate(R.layout.gallery_item, parent, false);
				}
				ImageView iv = (ImageView) convertView
						.findViewById(R.id.gallery_item_imageview);
				iv.setImageResource(R.drawable.pink_dino);
				GalleryItem item = getItem(position);
				String url = item.getUrl();
				mThumbnailThread.queueThumbnail(iv, url);

				if (mItems.size() > 1) {
					int endPos = position - 10;
					if (endPos <= 0)
						endPos = 0;
					if (endPos > 0) {
						for (int i = position - 1; i >= endPos; i--) {
							if (i < mItems.size()) {
								url = mItems.get(i).getUrl();
								String id = mItems.get(i).getId();
								if (url != null) {
									mThumbnailCacheThread.queueThumbnailCache(
											id, url);
								}
							}
						}
					}
					for (int i = position + 1; i <= position + 10; i++) {
						if (i < mItems.size()) {
							url = mItems.get(i).getUrl();
							String id = mItems.get(i).getId();
							if (url != null) {
								mThumbnailCacheThread.queueThumbnailCache(id,
										url);
							}
						}

					}
				}
				return convertView;
			}
		};
		mGridView.setAdapter(adapter);
	} else {
		mGridView.setAdapter(null);
	}
}

}[/code]

Just in case, here is myLruCache

[code]public class myLruCache extends LruCache<String, Bitmap> {

public static LruCache<String, Bitmap> mMemoryCache;
public myLruCache(int maxSize) {
	super(maxSize);
	mMemoryCache = new LruCache<String, Bitmap>(maxSize);
	// TODO Auto-generated constructor stub
}

public static void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public static Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

}[/code]


#8

Thanks for the tips guys. I managed to get a working version loosely based on your draft @bikergeekco.
I just wanted to ask, have you optimized it since? Scrolling seems to lag a little for me.I thought about trying the view placeholder method
but I don’t think it would help in this case.


#9

Hey bikergeekco, does not your solution download the things twice?

Suppose you queue the first photo url, so then you queue the second to the other thread. When the adapter asks for the second position, it will queue the second item url’s to fetch it, in order to download it, again.

Or am I understanding incorrectly the priority? Lower priority means that it will get less CPU than the other one, but still downloading twice if you do not cache in ThumbnailDownloader (if this is the case).

Even though you use cache, it could happen (and it’s even probable) that both threads ask if a certain url is cached, so they both start to download it and then add the entry to the cache.

Please tell me if I am misunderstanding something.


#10

Thank you for supplying the solution and I have edited it to have the code more clear.

First, same as you, I use a Singleton class instance to represent the cache.

public class SingletonLruCache extends LruCache<String, Bitmap> {

    private static SingletonLruCache sSinglettonLruCache;

    private SingletonLruCache(int maxSize) {
        super(maxSize);
    }

    public static SingletonLruCache getInstance(int maxSize) {
        if (sSinglettonLruCache == null) {
            synchronized (SingletonLruCache.class) {
                if (sSinglettonLruCache == null) {
                    sSinglettonLruCache = new SingletonLruCache(maxSize);
                }
            }
        }
        return sSinglettonLruCache;
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024;
    }

    public static Bitmap getBitmapFromMemoryCache(String key) {
        if (key == null) {
            return null;
        }
        return sSinglettonLruCache.get(key);
    }

    public static void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            sSinglettonLruCache.put(key, bitmap);
        }
    }
}

Next, I moved the getBitmapFromMemoryCache() method to the PhotoGalleryFragment.java. If the pic has been cached, you can skip the duration of queueThumbnail() method. If you use getBitmapFromMemoryCache() in the handleRequest() method, even though the pic has been cached, you will also see the placeholder pic for a very little time because the queueThumbnail() without downloading the pic will also take a very little time.

public class PhotoGalleryFragment extends Fragment {

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

            imageView.setImageResource(R.drawable.brian_up_close);
            GalleryItem item = getItem(position);

            Bitmap bitmap = SingletonLruCache.getBitmapFromMemoryCache(item.getUrl());

            if (bitmap == null) {
                mThumbnailThread.queueThumbnail(imageView, item.getUrl());
            } else {
                if (isVisible()) {
                    imageView.setImageBitmap(bitmap);
                }
            }

            for (int i = position - 10; i <= position + 10; i++) {
                if (i >= 0 && i < mItems.size()) {
                    String url = mItems.get(i).getUrl();
                    if (SingletonLruCache.getBitmapFromMemoryCache(url) == null) {
                        mThumbnailCacheThread.queueThumbnailCache(url);
                    }
                }
            }

            return convertView;
        }
    }
}

and use a for loop to preload the pic, also use the new ThumbnailCacheThread, however, I only use the url as the variable to pass in.

           for (int i = position - 10; i <= position + 10; i++) {
                if (i >= 0 && i < mItems.size()) {
                    String url = mItems.get(i).getUrl();
                    if (SingletonLruCache.getBitmapFromMemoryCache(url) == null) {
                        mThumbnailCacheThread.queueThumbnailCache(url);
                    }
                }
            }

ThumbnailCacheDownloader.java
There’s no need to use requestMapCache/pic id(the url is enough)

public class ThumbnailCacheDownloader extends HandlerThread {

    private static final String TAG = "ThumbnailCacheDownloader";

    private static final int MESSAGE_CACHE_DOWNLOAD = 1;

    private Handler mHandler;

    public ThumbnailCacheDownloader() {
        super(TAG);
    }

    @Override
    protected void onLooperPrepared() {
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == MESSAGE_CACHE_DOWNLOAD) {
                    String url = (String) msg.obj;
                    Log.i(TAG, "Got a request for url: " + url);
                    handleCacheRequest(url);
                }
            }
        };
    }

    public void queueThumbnailCache(String url) {
        Log.i(TAG, "Got an URL: " + url);

        mHandler.obtainMessage(MESSAGE_CACHE_DOWNLOAD, url).sendToTarget();
    }

    private void handleCacheRequest(String url) {
        try {
            if ((url != null) &&
                    (SingletonLruCache.getBitmapFromMemoryCache(url) == null)) {
                byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);
                Bitmap bitmap = BitmapFactory.decodeByteArray(
                        bitmapBytes, 0, bitmapBytes.length);
                SingletonLruCache.addBitmapToMemoryCache(url, bitmap);
                Log.i(TAG, "Bitmap created");
            }
        } catch (IOException e) {
            Log.e(TAG, "Error downloading image", e);
            e.printStackTrace();
        }
    }

    public void clearCacheQueue() {
        mHandler.removeMessages(MESSAGE_CACHE_DOWNLOAD);
    }
}

The class LruCache is thread-safe, and in the same time, both the mThumbnailThread and mThumbnailCacheThread will update the cache, even though I have made the check SingletonLruCache.getBitmapFromMemoryCache(url) before queueThumbnail/queueThumbnailCache in UI thread, I checked it again in the handleRequest/handleCacheRequest to avoid during the time, the cache has been updated by the other downloading thread. However, I can not avoid downloading the same pic in the two threads at same time. I can only reduce the probability.