Back to project page android-image-management.
The source code is released under:
Apache License
If you think the Android project android-image-management listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * android-image-management for Android/* w w w.ja va 2s . c o m*/ * Copyright (C) 2013 Laurence Dawson <contact@laurencedawson.com> * * 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.laurencedawson.image_management; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Comparator; import java.util.Date; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.net.Uri; import android.os.Build; import android.support.v4.util.LruCache; import android.webkit.MimeTypeMap; public class ImageManager { public static final boolean DEBUG = false; public static final int QUEUE_SIZE = 30; public static final int LONG_DELAY = 160; public static final int SHORT_DELAY = 80; public static final int NO_DELAY = 0; public static final int LONG_CONNECTION_TIMEOUT = 10000; public static final int LONG_REQUEST_TIMEOUT = 10000; public static final int MAX_WIDTH = 480; public static final int MAX_HEIGHT = 720; public static final int UI_PRIORITY = 1; public static final int BACKGROUND_PRIORITY = 0; public static final String GIF_MIME = "image/gif"; private Context mContext; private final LruCache<String, Bitmap> mBitmapCache; private final ExecutorService mThreadPool; // Inspired by Volley, only allow one thread to decode a bitmap private static final Object DECODE_LOCK = new Object(); // Maintain 3 queues for image requests // The first queues requests to be processed immediately by the thread pool private BlockingQueue<Runnable> mTaskQueue; // The second maintains a queue of active requests private ConcurrentLinkedQueue<Runnable> mActiveTasks; // The third maintains a queue of blocked requests. A request can be blocked // if a duplicate exists in the task or active queue private ConcurrentLinkedQueue<Runnable> mBlockedTasks; /** * Initialize a newly created ImageManager * @param context The application ofinal r activity context * @param cacheSize The size of the LRU cache * @param threads The number of threads for the pools to use */ public ImageManager(final Context context, final int cacheSize, final int threads ){ // Instantiate the three queues. The task queue uses a custom comparator to // change the ordering from FIFO (using the internal comparator) to respect // request priorities. If two requests have equal priorities, they are // sorted according to creation date mTaskQueue = new PriorityBlockingQueue<Runnable>(QUEUE_SIZE, new ImageThreadComparator()); mActiveTasks = new ConcurrentLinkedQueue<Runnable>(); mBlockedTasks = new ConcurrentLinkedQueue<Runnable>(); // The application context mContext = context; // Create a new threadpool using the taskQueue mThreadPool = new ThreadPoolExecutor(threads, threads, Long.MAX_VALUE, TimeUnit.SECONDS, mTaskQueue){ @Override protected void beforeExecute(final Thread thread, final Runnable run) { // Before executing a request, place the request on the active queue // This prevents new duplicate requests being placed in the active queue mActiveTasks.add(run); super.beforeExecute(thread, run); } @Override protected void afterExecute(final Runnable r, final Throwable t) { // After a request has finished executing, remove the request from // the active queue, this allows new duplicate requests to be submitted mActiveTasks.remove(r); // Perform a quick check to see if there are any remaining requests in // the blocked queue. Peek the head and check for duplicates in the // active and task queues. If no duplicates exist, add the request to // the task queue. Repeat this until a duplicate is found synchronized (mBlockedTasks) { while(mBlockedTasks.peek()!=null && !mTaskQueue.contains(mBlockedTasks.peek()) && !mActiveTasks.contains(mBlockedTasks.peek())){ Runnable runnable = mBlockedTasks.poll(); if(runnable!=null){ mThreadPool.execute(runnable); } } } super.afterExecute(r, t); } }; // Calculate the cache size final int actualCacheSize = ((int) (Runtime.getRuntime().maxMemory() / 1024)) / cacheSize; // Create the LRU cache // http://developer.android.com/reference/android/util/LruCache.html // The items are no longer recycled as they leave the cache, turns out this wasn't the right // way to go about this and often resulted in recycled bitmaps being drawn // http://stackoverflow.com/questions/10743381/when-should-i-recycle-a-bitmap-using-lrucache mBitmapCache = new LruCache<String, Bitmap>(actualCacheSize){ protected int sizeOf(final String key, final Bitmap value) { return value.getByteCount() / 1024; } }; } /** * Remove all images from the LRU cache */ public void removeAll() { synchronized (mBitmapCache) { mBitmapCache.evictAll(); } } /** * Remove an Image from the LRU cache * @param url The URL of the element to remove */ public void removeEntry(final String url){ if(url==null){ return; } synchronized (mBitmapCache) { mBitmapCache.remove(url); } } /** * Add a bitmap to the cache * @param url URL of the image * @param bitmap The bitmap image */ private void addEntry(final String url, final Bitmap bitmap){ if(url == null || bitmap == null){ return; } synchronized (mBitmapCache) { if(mBitmapCache.get(url) == null){ mBitmapCache.put(url, bitmap); } } } /** * Given an image URL, check if the cached image is a GIF * @param url The URL of the image * @return True if the image is a GIF */ public boolean isGif(String url){ if(url==null){ return false; } // First try to grab the mime from the options Options options = new Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(getFullCacheFileName(mContext, url), options); if(options.outMimeType!=null && options.outMimeType.equals(ImageManager.GIF_MIME)){ return true; } // Next, try to grab the mime type from the url final String extension = MimeTypeMap.getFileExtensionFromUrl(url); if(extension!=null){ String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); if(mimeType!=null){ return mimeType.equals(ImageManager.GIF_MIME); } } return false; } /** * Get a specified image from the cache * @param url The URL of the image * @return A Bitmap of the image requested */ public Bitmap get(String url) { if(url!=null){ synchronized (mBitmapCache) { // Get the image from the cache final Bitmap bitmap = mBitmapCache.get(url); // Check if the bitmap is in the cache if (bitmap != null) { return bitmap; } } } return null; } /** * Request an image to be downloaded, cached and loaded into the LRU cache. * * @param url The URL of the image to grab * @param request The ImageRequest complete with image options */ public void requestImage(final ImageRequest request) { // If the request has no URL, abandon it if(request.mUrl==null){ return; } // Create the image download runnable final ImageDownloadThread imageDownloadThread = new ImageDownloadThread(){ public void run() { // Sleep the request for the specified time if(request!=null && request.mLoadDelay>0){ try { Thread.sleep(request.mLoadDelay); } catch (InterruptedException e){ if(ImageManager.DEBUG){ e.printStackTrace(); } } } File file = null; // If the URL is not a local reseource, grab the file if(!getUrl().startsWith("content://")){ // Grab a link to the file file = new File(getFullCacheFileName(mContext, getUrl())); // If the file doesn't exist, grab it from the network if (!file.exists()){ cacheImage( file, request); } // Otherwise let the callback know the image is cached else if(request!=null){ request.sendCachedCallback(getUrl(), true); } // Check if the file is a gif boolean isGif = isGif(getUrl()); // If the file downloaded was a gif, tell all the callbacks if( isGif && request!=null ){ request.sendGifCallback(getUrl()); } } // Check if we should cache the image and the dimens boolean shouldCache = false; int maxWidth = MAX_WIDTH; int maxHeight = MAX_HEIGHT; if(request!=null){ maxWidth = request.mMaxWidth; maxHeight = request.mMaxHeight; shouldCache = request.mCacheImage; } // If any of the callbacks request the image should be cached, cache it if(shouldCache && (request!=null&&request.mContext!=null)||request==null){ // First check the image isn't actually in the cache Bitmap bitmap = get(getUrl()); // If the bitmap isn't in the cache, try to grab it // Or the bitmap was in the cache, but is of no use if(bitmap == null){ if(!getUrl().startsWith("content://")){ bitmap = decodeBitmap(file, maxWidth, maxHeight); }else{ Uri uri = Uri.parse(getUrl()); try{ InputStream input = mContext.getContentResolver().openInputStream(uri); bitmap = BitmapFactory.decodeStream(input); input.close(); }catch(FileNotFoundException e){ if(DEBUG){ e.printStackTrace(); } }catch(IOException e){ if(DEBUG){ e.printStackTrace(); } } } // If we grabbed the image ok, add to the cache if(bitmap!=null){ addEntry(getUrl(), bitmap); } } // Send the cached callback if(request!=null){ request.sendCallback(getUrl(), bitmap); } } } }; // Set the url of the request imageDownloadThread.setUrl(request.mUrl); // Set the creation time of the request imageDownloadThread.setCreated(request.mCreated); // Assign a priority to the request if(request.mImageListener==null){ // If there is no image listener, assign it background priority imageDownloadThread.setPriority(BACKGROUND_PRIORITY); }else{ // If there is an image listener, assign it UI priority imageDownloadThread.setPriority(UI_PRIORITY); } // If the new request is not a duplicate of an entry of the active and // task queues, add the request to the task queue if(!mTaskQueue.contains(imageDownloadThread) && !mActiveTasks.contains(imageDownloadThread)){ mThreadPool.execute(imageDownloadThread); } // If the request is a duplicate, add it to the blocked tasks queue else{ mBlockedTasks.add(imageDownloadThread); } } /** * Grab and save an image directly to disk * @param file The Bitmap file * @param url The URL of the image * @param imageCallback The callback associated with the request */ public static void cacheImage(final File file, ImageRequest imageCallback) { HttpURLConnection urlConnection = null; FileOutputStream fileOutputStream = null; InputStream inputStream = null; boolean isGif = false; try{ // Setup the connection urlConnection = (HttpURLConnection) new URL(imageCallback.mUrl).openConnection(); urlConnection.setConnectTimeout(ImageManager.LONG_CONNECTION_TIMEOUT); urlConnection.setReadTimeout(ImageManager.LONG_REQUEST_TIMEOUT); urlConnection.setUseCaches(true); urlConnection.setInstanceFollowRedirects(true); // Set the progress to 0 imageCallback.sendProgressUpdate(imageCallback.mUrl, 0); // Connect inputStream = urlConnection.getInputStream(); // Do not proceed if the file wasn't downloaded if(urlConnection.getResponseCode()==404){ urlConnection.disconnect(); return; } // Check if the image is a GIF String contentType = urlConnection.getHeaderField("Content-Type"); if(contentType!=null){ isGif = contentType.equals(GIF_MIME); } // Grab the length of the image int length = 0; try{ String fileLength = urlConnection.getHeaderField("Content-Length"); if(fileLength!=null){ length = Integer.parseInt(fileLength); } }catch(NumberFormatException e){ if(ImageManager.DEBUG){ e.printStackTrace(); } } // Write the input stream to disk fileOutputStream = new FileOutputStream(file, true); int byteRead = 0; int totalRead = 0; final byte[] buffer = new byte[8192]; int frameCount = 0; // Download the image while ((byteRead = inputStream.read(buffer)) != -1) { // If the image is a gif, count the start of frames if(isGif){ for(int i=0;i<byteRead-3;i++){ if( buffer[i] == 33 && buffer[i+1] == -7 && buffer[i+2] == 4 ){ frameCount++; // Once we have at least one frame, stop the download if(frameCount>1){ fileOutputStream.write(buffer, 0, i); fileOutputStream.close(); imageCallback.sendProgressUpdate(imageCallback.mUrl, 100); imageCallback.sendCachedCallback(imageCallback.mUrl, true); urlConnection.disconnect(); return; } } } } // Write the buffer to the file and update the total number of bytes // read so far (used for the callback) fileOutputStream.write(buffer, 0, byteRead); totalRead+=byteRead; // Update the callback with the current progress if(length>0){ imageCallback.sendProgressUpdate(imageCallback.mUrl, (int) (((float)totalRead/(float)length)*100) ); } } // Tidy up after the download if (fileOutputStream != null){ fileOutputStream.close(); } // Sent the callback that the image has been downloaded imageCallback.sendCachedCallback(imageCallback.mUrl, true); if (inputStream != null){ inputStream.close(); } // Disconnect the connection urlConnection.disconnect(); } catch (final MalformedURLException e) { if (ImageManager.DEBUG){ e.printStackTrace(); } // If the file exists and an error occurred, delete the file if (file != null){ file.delete(); } } catch (final IOException e) { if (ImageManager.DEBUG){ e.printStackTrace(); } // If the file exists and an error occurred, delete the file if (file != null){ file.delete(); } } } /** * Decode a Bitmap with the default max width and height * @param file The Bitmap file * @return The Bitmap image */ public static Bitmap decodeBitmap(final File file){ return decodeBitmap(file, ImageManager.MAX_WIDTH, ImageManager.MAX_WIDTH); } /** * Decode a Bitmap with a given max width and height * @param file The Bitmap file * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @return The Bitmap image */ @SuppressLint("NewApi") public static Bitmap decodeBitmap(final File file, final int reqWidth, final int reqHeight) { // Serialize all decode on a global lock to reduce concurrent heap usage. synchronized (DECODE_LOCK) { // Check if the file doesn't exist or has no content if(!file.exists() || file.exists() && file.length()==0 ){ return null; } // Load a scaled version of the bitmap Options opts = null; opts = getOptions(file,reqWidth,reqHeight); // Set a few additional options for the bitmap opts opts.inPurgeable = true; opts.inInputShareable = true; opts.inDither = true; // Grab the bitmap Bitmap bitmap = BitmapFactory.decodeFile(file.getPath(), opts); // If on JellyBean attempt to draw with mipmaps enabled if(bitmap!=null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){ bitmap.setHasMipMap(true); } // return the decoded bitmap return bitmap; } } /** * Grab the Bitmap options for a given max width and height * https://code.google.com/p/iosched * * @param file The file to load options for * @param reqWidth The requested width of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap * @return The options to be used for loading bitmaps */ public static Options getOptions(final File file, final int reqWidth, final int reqHeight) { BitmapFactory.Options options = new Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file.getPath(), options); options.inJustDecodeBounds = false; int inSampleSize = 1; int height = options.outHeight; int width = options.outWidth; if (height > reqHeight || width > reqWidth) { // Calculate ratios of height and width to requested height and width final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // Choose the smallest ratio as inSampleSize value, this will guarantee // a final image with both dimensions larger than or equal to the // requested height and width. inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; // This offers some additional logic in case the image has a strange // aspect ratio. For example, a panorama may have a much larger // width than height. In these cases the total pixels might still // end up being too large to fit comfortably in memory, so we should // be more aggressive with sample down the image (=larger // inSampleSize). final float totalPixels = width * height; // Anything more than 2x the requested pixels we'll sample down // further. final float totalReqPixelsCap = reqWidth * reqHeight * 2; while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { inSampleSize++; } } options.inSampleSize = inSampleSize; return options; } /** * Grab the full cache name for an image * * Based upon the following project * https://code.google.com/p/iosched * @param context The calling {@link Context} * @param url The URL of the image */ public static String getFullCacheFileName(Context context, String url) { return getCacheDir(context) + "/" + getCacheFileName(url); } /** * A hashing method that changes a string (like a URL) into a hash suitable * for using as a disk filename. * @param url The URL of the image * @return A filename */ public static String getCacheFileName(final String url){ String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(url.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(url.hashCode()); } return cacheKey; } /** * Based upon the following project * https://code.google.com/p/iosched * @param bytes A byte array * @return A {@link String} to use for a filename */ private static String bytesToHexString(byte[] bytes) { final StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { final String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { stringBuilder.append('0'); } stringBuilder.append(hex); } return stringBuilder.toString(); } /** * Get the cache directory to store images in * @param context The calling {@link Context} * @return a {@link File} to save images in */ private static File getCacheDir( final Context context ){ if(context!=null){ // Try to grab a reference to the external cache dir final File directory = context.getExternalCacheDir(); // If the file is OK, return it if(directory!=null){ return directory; } // If that fails, try to get a reference to the internal cache // This is a rare edge case but occasionally users don't have external // storage / their SDCard is removed / the folder is corrupt else{ return context.getCacheDir(); } } return null; } /** * Clear the local image cache of images over n days old * @param context The calling {@link Context} * @param int Max age of the images in days */ public static void clearCache( Context context, final int maxDays ){ // Grab the time now long time = new Date().getTime(); // If we can access the external cache, empty that first if( context.getExternalCacheDir() != null ){ String[] children = context.getExternalCacheDir().list(); for (int i = children.length-1; i >= 0; i--){ final File file = new File(context.getExternalCacheDir(), children[i]); final Date lastModified = new Date( file.lastModified() ); final long difference = time - lastModified.getTime(); final int days = (int) (difference / (24 * 60 * 60 * 1000)); if(days>=maxDays){ file.delete(); } } } // If we can access the internal cache, empty that too if(context.getCacheDir() != null ){ String[] children = context.getCacheDir().list(); for (int i = children.length-1; i >= 0; i--){ final File file = new File(context.getCacheDir(), children[i]); final Date lastModified = new Date( file.lastModified() ); final long difference = time - lastModified.getTime(); final int days = (int) (difference / (24 * 60 * 60 * 1000)); if(days>=maxDays){ file.delete(); } } } } } /** * A simple comparator which favours ImageDownloadThreads with higher * priorities (such as UI requests over background requests) * @author Laurence Dawson * */ class ImageThreadComparator implements Comparator<Runnable>{ @Override public int compare(final Runnable lhs, final Runnable rhs) { if(lhs instanceof ImageDownloadThread && rhs instanceof ImageDownloadThread){ // Favour a higher priority if(((ImageDownloadThread)lhs).getPriority()>((ImageDownloadThread)rhs).getPriority()){ return -1; } else if(((ImageDownloadThread)lhs).getPriority()<((ImageDownloadThread)rhs).getPriority()){ return 1; } // Favour a lower creation time else if(((ImageDownloadThread)lhs).getCreated()>((ImageDownloadThread)rhs).getCreated()){ return 1; }else if(((ImageDownloadThread)lhs).getCreated()<((ImageDownloadThread)rhs).getCreated()){ return -1; } } return 0; } } /** * A simple Runnable object that can be given a priority, to be used with * ImageThreadComparator and PriorityBlockingQueue * @author Laurence Dawson * */ class ImageDownloadThread implements Runnable{ private int priority; private String url; private long created; @Override public void run() { // To be overridden } public int getPriority() { return priority; } public void setPriority(final int priority) { this.priority = priority; } public String getUrl(){ return url; } public void setUrl(String url){ this.url = url; } public long getCreated(){ return created; } public void setCreated(long created){ this.created = created; } @Override public boolean equals(final Object o) { if(getUrl()==null){ return false; } if(o==null){ return false; } if(o instanceof ImageDownloadThread && ((ImageDownloadThread)o).getUrl().equals(getUrl())){ return true; } return super.equals(o); } @Override public int hashCode() { return getUrl().hashCode(); } }