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      */
PhotoManager()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      */
getInstance()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")
handleState(PhotoTask photoTask, int state)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      */
cancelAll()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      */
removeDownload(PhotoTask downloaderTask, URL pictureURL)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      */
startDownload( PhotoView imageView, boolean cacheFlag)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      */
recycleTask(PhotoTask downloadTask)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