| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.threadsample; |
| |
| import android.annotation.SuppressLint; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.support.v4.util.LruCache; |
| |
| import java.net.URL; |
| import java.util.Queue; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * This class creates pools of background threads for downloading |
| * Picasa images from the web, based on URLs retrieved from Picasa's featured images RSS feed. |
| * The class is implemented as a singleton; the only way to get an PhotoManager instance is to |
| * call {@link #getInstance}. |
| * <p> |
| * The class sets the pool size and cache size based on the particular operation it's performing. |
| * The algorithm doesn't apply to all situations, so if you re-use the code to implement a pool |
| * of threads for your own app, you will have to come up with your choices for pool size, cache |
| * size, and so forth. In many cases, you'll have to set some numbers arbitrarily and then |
| * measure the impact on performance. |
| * <p> |
| * This class actually uses two threadpools in order to limit the number of |
| * simultaneous image decoding threads to the number of available processor |
| * cores. |
| * <p> |
| * Finally, this class defines a handler that communicates back to the UI |
| * thread to change the bitmap to reflect the state. |
| */ |
| @SuppressWarnings("unused") |
| public class PhotoManager { |
| /* |
| * Status indicators |
| */ |
| static final int DOWNLOAD_FAILED = -1; |
| static final int DOWNLOAD_STARTED = 1; |
| static final int DOWNLOAD_COMPLETE = 2; |
| static final int DECODE_STARTED = 3; |
| static final int TASK_COMPLETE = 4; |
| |
| // Sets the size of the storage that's used to cache images |
| private static final int IMAGE_CACHE_SIZE = 1024 * 1024 * 4; |
| |
| // Sets the amount of time an idle thread will wait for a task before terminating |
| private static final int KEEP_ALIVE_TIME = 1; |
| |
| // Sets the Time Unit to seconds |
| private static final TimeUnit KEEP_ALIVE_TIME_UNIT; |
| |
| // Sets the initial threadpool size to 8 |
| private static final int CORE_POOL_SIZE = 8; |
| |
| // Sets the maximum threadpool size to 8 |
| private static final int MAXIMUM_POOL_SIZE = 8; |
| |
| /** |
| * NOTE: This is the number of total available cores. On current versions of |
| * Android, with devices that use plug-and-play cores, this will return less |
| * than the total number of cores. The total number of cores is not |
| * available in current Android implementations. |
| */ |
| private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); |
| |
| /* |
| * Creates a cache of byte arrays indexed by image URLs. As new items are added to the |
| * cache, the oldest items are ejected and subject to garbage collection. |
| */ |
| private final LruCache<URL, byte[]> mPhotoCache; |
| |
| // A queue of Runnables for the image download pool |
| private final BlockingQueue<Runnable> mDownloadWorkQueue; |
| |
| // A queue of Runnables for the image decoding pool |
| private final BlockingQueue<Runnable> mDecodeWorkQueue; |
| |
| // A queue of PhotoManager tasks. Tasks are handed to a ThreadPool. |
| private final Queue<PhotoTask> mPhotoTaskWorkQueue; |
| |
| // A managed pool of background download threads |
| private final ThreadPoolExecutor mDownloadThreadPool; |
| |
| // A managed pool of background decoder threads |
| private final ThreadPoolExecutor mDecodeThreadPool; |
| |
| // An object that manages Messages in a Thread |
| private Handler mHandler; |
| |
| // A single instance of PhotoManager, used to implement the singleton pattern |
| private static PhotoManager sInstance = null; |
| |
| // A static block that sets class fields |
| static { |
| |
| // The time unit for "keep alive" is in seconds |
| KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; |
| |
| // Creates a single static instance of PhotoManager |
| sInstance = new PhotoManager(); |
| } |
| /** |
| * Constructs the work queues and thread pools used to download and decode images. |
| */ |
| private PhotoManager() { |
| |
| /* |
| * Creates a work queue for the pool of Thread objects used for downloading, using a linked |
| * list queue that blocks when the queue is empty. |
| */ |
| mDownloadWorkQueue = new LinkedBlockingQueue<Runnable>(); |
| |
| /* |
| * Creates a work queue for the pool of Thread objects used for decoding, using a linked |
| * list queue that blocks when the queue is empty. |
| */ |
| mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>(); |
| |
| /* |
| * Creates a work queue for the set of of task objects that control downloading and |
| * decoding, using a linked list queue that blocks when the queue is empty. |
| */ |
| mPhotoTaskWorkQueue = new LinkedBlockingQueue<PhotoTask>(); |
| |
| /* |
| * Creates a new pool of Thread objects for the download work queue |
| */ |
| mDownloadThreadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, |
| KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDownloadWorkQueue); |
| |
| /* |
| * Creates a new pool of Thread objects for the decoding work queue |
| */ |
| mDecodeThreadPool = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES, |
| KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDecodeWorkQueue); |
| |
| // Instantiates a new cache based on the cache size estimate |
| mPhotoCache = new LruCache<URL, byte[]>(IMAGE_CACHE_SIZE) { |
| |
| /* |
| * This overrides the default sizeOf() implementation to return the |
| * correct size of each cache entry. |
| */ |
| |
| @Override |
| protected int sizeOf(URL paramURL, byte[] paramArrayOfByte) { |
| return paramArrayOfByte.length; |
| } |
| }; |
| /* |
| * Instantiates a new anonymous Handler object and defines its |
| * handleMessage() method. The Handler *must* run on the UI thread, because it moves photo |
| * Bitmaps from the PhotoTask object to the View object. |
| * To force the Handler to run on the UI thread, it's defined as part of the PhotoManager |
| * constructor. The constructor is invoked when the class is first referenced, and that |
| * happens when the View invokes startDownload. Since the View runs on the UI Thread, so |
| * does the constructor and the Handler. |
| */ |
| mHandler = new Handler(Looper.getMainLooper()) { |
| |
| /* |
| * handleMessage() defines the operations to perform when the |
| * Handler receives a new Message to process. |
| */ |
| @Override |
| public void handleMessage(Message inputMessage) { |
| |
| // Gets the image task from the incoming Message object. |
| PhotoTask photoTask = (PhotoTask) inputMessage.obj; |
| |
| // Sets an PhotoView that's a weak reference to the |
| // input ImageView |
| PhotoView localView = photoTask.getPhotoView(); |
| |
| // If this input view isn't null |
| if (localView != null) { |
| |
| /* |
| * Gets the URL of the *weak reference* to the input |
| * ImageView. The weak reference won't have changed, even if |
| * the input ImageView has. |
| */ |
| URL localURL = localView.getLocation(); |
| |
| /* |
| * Compares the URL of the input ImageView to the URL of the |
| * weak reference. Only updates the bitmap in the ImageView |
| * if this particular Thread is supposed to be serving the |
| * ImageView. |
| */ |
| if (photoTask.getImageURL() == localURL) |
| |
| /* |
| * Chooses the action to take, based on the incoming message |
| */ |
| switch (inputMessage.what) { |
| |
| // If the download has started, sets background color to dark green |
| case DOWNLOAD_STARTED: |
| localView.setStatusResource(R.drawable.imagedownloading); |
| break; |
| |
| /* |
| * If the download is complete, but the decode is waiting, sets the |
| * background color to golden yellow |
| */ |
| case DOWNLOAD_COMPLETE: |
| // Sets background color to golden yellow |
| localView.setStatusResource(R.drawable.decodequeued); |
| break; |
| // If the decode has started, sets background color to orange |
| case DECODE_STARTED: |
| localView.setStatusResource(R.drawable.decodedecoding); |
| break; |
| /* |
| * The decoding is done, so this sets the |
| * ImageView's bitmap to the bitmap in the |
| * incoming message |
| */ |
| case TASK_COMPLETE: |
| localView.setImageBitmap(photoTask.getImage()); |
| recycleTask(photoTask); |
| break; |
| // The download failed, sets the background color to dark red |
| case DOWNLOAD_FAILED: |
| localView.setStatusResource(R.drawable.imagedownloadfailed); |
| |
| // Attempts to re-use the Task object |
| recycleTask(photoTask); |
| break; |
| default: |
| // Otherwise, calls the super method |
| super.handleMessage(inputMessage); |
| } |
| } |
| } |
| }; |
| } |
| |
| /** |
| * Returns the PhotoManager object |
| * @return The global PhotoManager object |
| */ |
| public static PhotoManager getInstance() { |
| |
| return sInstance; |
| } |
| |
| /** |
| * Handles state messages for a particular task object |
| * @param photoTask A task object |
| * @param state The state of the task |
| */ |
| @SuppressLint("HandlerLeak") |
| public void handleState(PhotoTask photoTask, int state) { |
| switch (state) { |
| |
| // The task finished downloading and decoding the image |
| case TASK_COMPLETE: |
| |
| // Puts the image into cache |
| if (photoTask.isCacheEnabled()) { |
| // If the task is set to cache the results, put the buffer |
| // that was |
| // successfully decoded into the cache |
| mPhotoCache.put(photoTask.getImageURL(), photoTask.getByteBuffer()); |
| } |
| |
| // Gets a Message object, stores the state in it, and sends it to the Handler |
| Message completeMessage = mHandler.obtainMessage(state, photoTask); |
| completeMessage.sendToTarget(); |
| break; |
| |
| // The task finished downloading the image |
| case DOWNLOAD_COMPLETE: |
| /* |
| * Decodes the image, by queuing the decoder object to run in the decoder |
| * thread pool |
| */ |
| mDecodeThreadPool.execute(photoTask.getPhotoDecodeRunnable()); |
| |
| // In all other cases, pass along the message without any other action. |
| default: |
| mHandler.obtainMessage(state, photoTask).sendToTarget(); |
| break; |
| } |
| |
| } |
| |
| /** |
| * Cancels all Threads in the ThreadPool |
| */ |
| public static void cancelAll() { |
| |
| /* |
| * Creates an array of tasks that's the same size as the task work queue |
| */ |
| PhotoTask[] taskArray = new PhotoTask[sInstance.mDownloadWorkQueue.size()]; |
| |
| // Populates the array with the task objects in the queue |
| sInstance.mDownloadWorkQueue.toArray(taskArray); |
| |
| // Stores the array length in order to iterate over the array |
| int taskArraylen = taskArray.length; |
| |
| /* |
| * Locks on the singleton to ensure that other processes aren't mutating Threads, then |
| * iterates over the array of tasks and interrupts the task's current Thread. |
| */ |
| synchronized (sInstance) { |
| |
| // Iterates over the array of tasks |
| for (int taskArrayIndex = 0; taskArrayIndex < taskArraylen; taskArrayIndex++) { |
| |
| // Gets the task's current thread |
| Thread thread = taskArray[taskArrayIndex].mThreadThis; |
| |
| // if the Thread exists, post an interrupt to it |
| if (null != thread) { |
| thread.interrupt(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Stops a download Thread and removes it from the threadpool |
| * |
| * @param downloaderTask The download task associated with the Thread |
| * @param pictureURL The URL being downloaded |
| */ |
| static public void removeDownload(PhotoTask downloaderTask, URL pictureURL) { |
| |
| // If the Thread object still exists and the download matches the specified URL |
| if (downloaderTask != null && downloaderTask.getImageURL().equals(pictureURL)) { |
| |
| /* |
| * Locks on this class to ensure that other processes aren't mutating Threads. |
| */ |
| synchronized (sInstance) { |
| |
| // Gets the Thread that the downloader task is running on |
| Thread thread = downloaderTask.getCurrentThread(); |
| |
| // If the Thread exists, posts an interrupt to it |
| if (null != thread) |
| thread.interrupt(); |
| } |
| /* |
| * Removes the download Runnable from the ThreadPool. This opens a Thread in the |
| * ThreadPool's work queue, allowing a task in the queue to start. |
| */ |
| sInstance.mDownloadThreadPool.remove(downloaderTask.getHTTPDownloadRunnable()); |
| } |
| } |
| |
| /** |
| * Starts an image download and decode |
| * |
| * @param imageView The ImageView that will get the resulting Bitmap |
| * @param cacheFlag Determines if caching should be used |
| * @return The task instance that will handle the work |
| */ |
| static public PhotoTask startDownload( |
| PhotoView imageView, |
| boolean cacheFlag) { |
| |
| /* |
| * Gets a task from the pool of tasks, returning null if the pool is empty |
| */ |
| PhotoTask downloadTask = sInstance.mPhotoTaskWorkQueue.poll(); |
| |
| // If the queue was empty, create a new task instead. |
| if (null == downloadTask) { |
| downloadTask = new PhotoTask(); |
| } |
| |
| // Initializes the task |
| downloadTask.initializeDownloaderTask(PhotoManager.sInstance, imageView, cacheFlag); |
| |
| /* |
| * Provides the download task with the cache buffer corresponding to the URL to be |
| * downloaded. |
| */ |
| downloadTask.setByteBuffer(sInstance.mPhotoCache.get(downloadTask.getImageURL())); |
| |
| // If the byte buffer was empty, the image wasn't cached |
| if (null == downloadTask.getByteBuffer()) { |
| |
| /* |
| * "Executes" the tasks' download Runnable in order to download the image. If no |
| * Threads are available in the thread pool, the Runnable waits in the queue. |
| */ |
| sInstance.mDownloadThreadPool.execute(downloadTask.getHTTPDownloadRunnable()); |
| |
| // Sets the display to show that the image is queued for downloading and decoding. |
| imageView.setStatusResource(R.drawable.imagequeued); |
| |
| // The image was cached, so no download is required. |
| } else { |
| |
| /* |
| * Signals that the download is "complete", because the byte array already contains the |
| * undecoded image. The decoding starts. |
| */ |
| |
| sInstance.handleState(downloadTask, DOWNLOAD_COMPLETE); |
| } |
| |
| // Returns a task object, either newly-created or one from the task pool |
| return downloadTask; |
| } |
| |
| /** |
| * Recycles tasks by calling their internal recycle() method and then putting them back into |
| * the task queue. |
| * @param downloadTask The task to recycle |
| */ |
| void recycleTask(PhotoTask downloadTask) { |
| |
| // Frees up memory in the task |
| downloadTask.recycle(); |
| |
| // Puts the task object back into the queue for re-use. |
| mPhotoTaskWorkQueue.offer(downloadTask); |
| } |
| } |