Home | History | Annotate | Download | only in threadsample
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.threadsample;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.os.Handler;
     21 import android.os.Looper;
     22 import android.os.Message;
     23 import android.support.v4.util.LruCache;
     24 
     25 import java.net.URL;
     26 import java.util.Queue;
     27 import java.util.concurrent.BlockingQueue;
     28 import java.util.concurrent.LinkedBlockingQueue;
     29 import java.util.concurrent.ThreadPoolExecutor;
     30 import java.util.concurrent.TimeUnit;
     31 
     32 /**
     33  * This class creates pools of background threads for downloading
     34  * Picasa images from the web, based on URLs retrieved from Picasa's featured images RSS feed.
     35  * The class is implemented as a singleton; the only way to get an PhotoManager instance is to
     36  * call {@link #getInstance}.
     37  * <p>
     38  * The class sets the pool size and cache size based on the particular operation it's performing.
     39  * The algorithm doesn't apply to all situations, so if you re-use the code to implement a pool
     40  * of threads for your own app, you will have to come up with your choices for pool size, cache
     41  * size, and so forth. In many cases, you'll have to set some numbers arbitrarily and then
     42  * measure the impact on performance.
     43  * <p>
     44  * This class actually uses two threadpools in order to limit the number of
     45  * simultaneous image decoding threads to the number of available processor
     46  * cores.
     47  * <p>
     48  * Finally, this class defines a handler that communicates back to the UI
     49  * thread to change the bitmap to reflect the state.
     50  */
     51 @SuppressWarnings("unused")
     52 public class PhotoManager {
     53     /*
     54      * Status indicators
     55      */
     56     static final int DOWNLOAD_FAILED = -1;
     57     static final int DOWNLOAD_STARTED = 1;
     58     static final int DOWNLOAD_COMPLETE = 2;
     59     static final int DECODE_STARTED = 3;
     60     static final int TASK_COMPLETE = 4;
     61 
     62     // Sets the size of the storage that's used to cache images
     63     private static final int IMAGE_CACHE_SIZE = 1024 * 1024 * 4;
     64 
     65     // Sets the amount of time an idle thread will wait for a task before terminating
     66     private static final int KEEP_ALIVE_TIME = 1;
     67 
     68     // Sets the Time Unit to seconds
     69     private static final TimeUnit KEEP_ALIVE_TIME_UNIT;
     70 
     71     // Sets the initial threadpool size to 8
     72     private static final int CORE_POOL_SIZE = 8;
     73 
     74     // Sets the maximum threadpool size to 8
     75     private static final int MAXIMUM_POOL_SIZE = 8;
     76 
     77     /**
     78      * NOTE: This is the number of total available cores. On current versions of
     79      * Android, with devices that use plug-and-play cores, this will return less
     80      * than the total number of cores. The total number of cores is not
     81      * available in current Android implementations.
     82      */
     83     private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
     84 
     85     /*
     86      * Creates a cache of byte arrays indexed by image URLs. As new items are added to the
     87      * cache, the oldest items are ejected and subject to garbage collection.
     88      */
     89     private final LruCache<URL, byte[]> mPhotoCache;
     90 
     91     // A queue of Runnables for the image download pool
     92     private final BlockingQueue<Runnable> mDownloadWorkQueue;
     93 
     94     // A queue of Runnables for the image decoding pool
     95     private final BlockingQueue<Runnable> mDecodeWorkQueue;
     96 
     97     // A queue of PhotoManager tasks. Tasks are handed to a ThreadPool.
     98     private final Queue<PhotoTask> mPhotoTaskWorkQueue;
     99 
    100     // A managed pool of background download threads
    101     private final ThreadPoolExecutor mDownloadThreadPool;
    102 
    103     // A managed pool of background decoder threads
    104     private final ThreadPoolExecutor mDecodeThreadPool;
    105 
    106     // An object that manages Messages in a Thread
    107     private Handler mHandler;
    108 
    109     // A single instance of PhotoManager, used to implement the singleton pattern
    110     private static PhotoManager sInstance = null;
    111 
    112     // A static block that sets class fields
    113     static {
    114 
    115         // The time unit for "keep alive" is in seconds
    116         KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
    117 
    118         // Creates a single static instance of PhotoManager
    119         sInstance = new PhotoManager();
    120     }
    121     /**
    122      * Constructs the work queues and thread pools used to download and decode images.
    123      */
    124     private PhotoManager() {
    125 
    126         /*
    127          * Creates a work queue for the pool of Thread objects used for downloading, using a linked
    128          * list queue that blocks when the queue is empty.
    129          */
    130         mDownloadWorkQueue = new LinkedBlockingQueue<Runnable>();
    131 
    132         /*
    133          * Creates a work queue for the pool of Thread objects used for decoding, using a linked
    134          * list queue that blocks when the queue is empty.
    135          */
    136         mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>();
    137 
    138         /*
    139          * Creates a work queue for the set of of task objects that control downloading and
    140          * decoding, using a linked list queue that blocks when the queue is empty.
    141          */
    142         mPhotoTaskWorkQueue = new LinkedBlockingQueue<PhotoTask>();
    143 
    144         /*
    145          * Creates a new pool of Thread objects for the download work queue
    146          */
    147         mDownloadThreadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
    148                 KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDownloadWorkQueue);
    149 
    150         /*
    151          * Creates a new pool of Thread objects for the decoding work queue
    152          */
    153         mDecodeThreadPool = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES,
    154                 KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDecodeWorkQueue);
    155 
    156         // Instantiates a new cache based on the cache size estimate
    157         mPhotoCache = new LruCache<URL, byte[]>(IMAGE_CACHE_SIZE) {
    158 
    159             /*
    160              * This overrides the default sizeOf() implementation to return the
    161              * correct size of each cache entry.
    162              */
    163 
    164             @Override
    165             protected int sizeOf(URL paramURL, byte[] paramArrayOfByte) {
    166                 return paramArrayOfByte.length;
    167             }
    168         };
    169         /*
    170          * Instantiates a new anonymous Handler object and defines its
    171          * handleMessage() method. The Handler *must* run on the UI thread, because it moves photo
    172          * Bitmaps from the PhotoTask object to the View object.
    173          * To force the Handler to run on the UI thread, it's defined as part of the PhotoManager
    174          * constructor. The constructor is invoked when the class is first referenced, and that
    175          * happens when the View invokes startDownload. Since the View runs on the UI Thread, so
    176          * does the constructor and the Handler.
    177          */
    178         mHandler = new Handler(Looper.getMainLooper()) {
    179 
    180             /*
    181              * handleMessage() defines the operations to perform when the
    182              * Handler receives a new Message to process.
    183              */
    184             @Override
    185             public void handleMessage(Message inputMessage) {
    186 
    187                 // Gets the image task from the incoming Message object.
    188                 PhotoTask photoTask = (PhotoTask) inputMessage.obj;
    189 
    190                 // Sets an PhotoView that's a weak reference to the
    191                 // input ImageView
    192                 PhotoView localView = photoTask.getPhotoView();
    193 
    194                 // If this input view isn't null
    195                 if (localView != null) {
    196 
    197                     /*
    198                      * Gets the URL of the *weak reference* to the input
    199                      * ImageView. The weak reference won't have changed, even if
    200                      * the input ImageView has.
    201                      */
    202                     URL localURL = localView.getLocation();
    203 
    204                     /*
    205                      * Compares the URL of the input ImageView to the URL of the
    206                      * weak reference. Only updates the bitmap in the ImageView
    207                      * if this particular Thread is supposed to be serving the
    208                      * ImageView.
    209                      */
    210                     if (photoTask.getImageURL() == localURL)
    211 
    212                         /*
    213                          * Chooses the action to take, based on the incoming message
    214                          */
    215                         switch (inputMessage.what) {
    216 
    217                             // If the download has started, sets background color to dark green
    218                             case DOWNLOAD_STARTED:
    219                                 localView.setStatusResource(R.drawable.imagedownloading);
    220                                 break;
    221 
    222                             /*
    223                              * If the download is complete, but the decode is waiting, sets the
    224                              * background color to golden yellow
    225                              */
    226                             case DOWNLOAD_COMPLETE:
    227                                 // Sets background color to golden yellow
    228                                 localView.setStatusResource(R.drawable.decodequeued);
    229                                 break;
    230                             // If the decode has started, sets background color to orange
    231                             case DECODE_STARTED:
    232                                 localView.setStatusResource(R.drawable.decodedecoding);
    233                                 break;
    234                             /*
    235                              * The decoding is done, so this sets the
    236                              * ImageView's bitmap to the bitmap in the
    237                              * incoming message
    238                              */
    239                             case TASK_COMPLETE:
    240                                 localView.setImageBitmap(photoTask.getImage());
    241                                 recycleTask(photoTask);
    242                                 break;
    243                             // The download failed, sets the background color to dark red
    244                             case DOWNLOAD_FAILED:
    245                                 localView.setStatusResource(R.drawable.imagedownloadfailed);
    246 
    247                                 // Attempts to re-use the Task object
    248                                 recycleTask(photoTask);
    249                                 break;
    250                             default:
    251                                 // Otherwise, calls the super method
    252                                 super.handleMessage(inputMessage);
    253                         }
    254                 }
    255             }
    256         };
    257     }
    258 
    259     /**
    260      * Returns the PhotoManager object
    261      * @return The global PhotoManager object
    262      */
    263     public static PhotoManager getInstance() {
    264 
    265         return sInstance;
    266     }
    267 
    268     /**
    269      * Handles state messages for a particular task object
    270      * @param photoTask A task object
    271      * @param state The state of the task
    272      */
    273     @SuppressLint("HandlerLeak")
    274     public void handleState(PhotoTask photoTask, int state) {
    275         switch (state) {
    276 
    277             // The task finished downloading and decoding the image
    278             case TASK_COMPLETE:
    279 
    280                 // Puts the image into cache
    281                 if (photoTask.isCacheEnabled()) {
    282                     // If the task is set to cache the results, put the buffer
    283                     // that was
    284                     // successfully decoded into the cache
    285                     mPhotoCache.put(photoTask.getImageURL(), photoTask.getByteBuffer());
    286                 }
    287 
    288                 // Gets a Message object, stores the state in it, and sends it to the Handler
    289                 Message completeMessage = mHandler.obtainMessage(state, photoTask);
    290                 completeMessage.sendToTarget();
    291                 break;
    292 
    293             // The task finished downloading the image
    294             case DOWNLOAD_COMPLETE:
    295                 /*
    296                  * Decodes the image, by queuing the decoder object to run in the decoder
    297                  * thread pool
    298                  */
    299                 mDecodeThreadPool.execute(photoTask.getPhotoDecodeRunnable());
    300 
    301             // In all other cases, pass along the message without any other action.
    302             default:
    303                 mHandler.obtainMessage(state, photoTask).sendToTarget();
    304                 break;
    305         }
    306 
    307     }
    308 
    309     /**
    310      * Cancels all Threads in the ThreadPool
    311      */
    312     public static void cancelAll() {
    313 
    314         /*
    315          * Creates an array of tasks that's the same size as the task work queue
    316          */
    317         PhotoTask[] taskArray = new PhotoTask[sInstance.mDownloadWorkQueue.size()];
    318 
    319         // Populates the array with the task objects in the queue
    320         sInstance.mDownloadWorkQueue.toArray(taskArray);
    321 
    322         // Stores the array length in order to iterate over the array
    323         int taskArraylen = taskArray.length;
    324 
    325         /*
    326          * Locks on the singleton to ensure that other processes aren't mutating Threads, then
    327          * iterates over the array of tasks and interrupts the task's current Thread.
    328          */
    329         synchronized (sInstance) {
    330 
    331             // Iterates over the array of tasks
    332             for (int taskArrayIndex = 0; taskArrayIndex < taskArraylen; taskArrayIndex++) {
    333 
    334                 // Gets the task's current thread
    335                 Thread thread = taskArray[taskArrayIndex].mThreadThis;
    336 
    337                 // if the Thread exists, post an interrupt to it
    338                 if (null != thread) {
    339                     thread.interrupt();
    340                 }
    341             }
    342         }
    343     }
    344 
    345     /**
    346      * Stops a download Thread and removes it from the threadpool
    347      *
    348      * @param downloaderTask The download task associated with the Thread
    349      * @param pictureURL The URL being downloaded
    350      */
    351     static public void removeDownload(PhotoTask downloaderTask, URL pictureURL) {
    352 
    353         // If the Thread object still exists and the download matches the specified URL
    354         if (downloaderTask != null && downloaderTask.getImageURL().equals(pictureURL)) {
    355 
    356             /*
    357              * Locks on this class to ensure that other processes aren't mutating Threads.
    358              */
    359             synchronized (sInstance) {
    360 
    361                 // Gets the Thread that the downloader task is running on
    362                 Thread thread = downloaderTask.getCurrentThread();
    363 
    364                 // If the Thread exists, posts an interrupt to it
    365                 if (null != thread)
    366                     thread.interrupt();
    367             }
    368             /*
    369              * Removes the download Runnable from the ThreadPool. This opens a Thread in the
    370              * ThreadPool's work queue, allowing a task in the queue to start.
    371              */
    372             sInstance.mDownloadThreadPool.remove(downloaderTask.getHTTPDownloadRunnable());
    373         }
    374     }
    375 
    376     /**
    377      * Starts an image download and decode
    378      *
    379      * @param imageView The ImageView that will get the resulting Bitmap
    380      * @param cacheFlag Determines if caching should be used
    381      * @return The task instance that will handle the work
    382      */
    383     static public PhotoTask startDownload(
    384             PhotoView imageView,
    385             boolean cacheFlag) {
    386 
    387         /*
    388          * Gets a task from the pool of tasks, returning null if the pool is empty
    389          */
    390         PhotoTask downloadTask = sInstance.mPhotoTaskWorkQueue.poll();
    391 
    392         // If the queue was empty, create a new task instead.
    393         if (null == downloadTask) {
    394             downloadTask = new PhotoTask();
    395         }
    396 
    397         // Initializes the task
    398         downloadTask.initializeDownloaderTask(PhotoManager.sInstance, imageView, cacheFlag);
    399 
    400         /*
    401          * Provides the download task with the cache buffer corresponding to the URL to be
    402          * downloaded.
    403          */
    404         downloadTask.setByteBuffer(sInstance.mPhotoCache.get(downloadTask.getImageURL()));
    405 
    406         // If the byte buffer was empty, the image wasn't cached
    407         if (null == downloadTask.getByteBuffer()) {
    408 
    409             /*
    410              * "Executes" the tasks' download Runnable in order to download the image. If no
    411              * Threads are available in the thread pool, the Runnable waits in the queue.
    412              */
    413             sInstance.mDownloadThreadPool.execute(downloadTask.getHTTPDownloadRunnable());
    414 
    415             // Sets the display to show that the image is queued for downloading and decoding.
    416             imageView.setStatusResource(R.drawable.imagequeued);
    417 
    418         // The image was cached, so no download is required.
    419         } else {
    420 
    421             /*
    422              * Signals that the download is "complete", because the byte array already contains the
    423              * undecoded image. The decoding starts.
    424              */
    425 
    426             sInstance.handleState(downloadTask, DOWNLOAD_COMPLETE);
    427         }
    428 
    429         // Returns a task object, either newly-created or one from the task pool
    430         return downloadTask;
    431     }
    432 
    433     /**
    434      * Recycles tasks by calling their internal recycle() method and then putting them back into
    435      * the task queue.
    436      * @param downloadTask The task to recycle
    437      */
    438     void recycleTask(PhotoTask downloadTask) {
    439 
    440         // Frees up memory in the task
    441         downloadTask.recycle();
    442 
    443         // Puts the task object back into the queue for re-use.
    444         mPhotoTaskWorkQueue.offer(downloadTask);
    445     }
    446 }
    447