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