Java tutorial
/******************************************************************************* * This file is part of the C4MAndroidImageManager project. * * Copyright (c) 2012 C4M PROD. * * C4MAndroidImageManager is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * C4MAndroidImageManager is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with C4MAndroidImageManager. If not, see <http://www.gnu.org/licenses/lgpl.html>. * * Contributors: * C4M PROD - initial API and implementation ******************************************************************************/ package com.c4mprod.utils; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.net.URL; import java.net.URLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Comparator; import java.util.Locale; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.http.AndroidHttpClient; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.v4.util.LruCache; import android.text.TextUtils; import android.util.FloatMath; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; public class ImageManager { private static final int CACHE_SIZE = 4 * 1024 * 1024; // 4Mib private static final long SD_CACHE_SIZE = 50 * 1024 * 1024; // 50Mib public static final int BITMAP_MAX_HEIGHT = 480; public static final int BITMAP_MAX_WIDTH = 800; public static final int THUMB_MAX_HEIGHT = 200; public static final int FLAG_ROUNDED_CORNERS = 1; public static final int FLAG_GET_THUMBNAIL = 1 << 1; public static final int FLAG_NO_THUMBNAIL = 1 << 2; public static final int FLAG_IN_BACKGROUND = 1 << 3; private static final int MSG_LOAD_FROM_SD = 0; private static final int MSG_DOWNLOAD = 1; private static final int MSG_STOP = 2; private static final String THUMB_FOLDER = "/thumb"; private static ImageManager instance; private LooperThread mDowloadLooper; private final LruCache<String, Bitmap> mImageLiveCache = new LruCache<String, Bitmap>(CACHE_SIZE); private final Handler mUiHandler; private boolean mStopped = false; SimpleDateFormat mDateFormater = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.UK); static Context mContext; private final SparseArray<Bitmap> mDefaultImageCache = new SparseArray<Bitmap>(2); // private ImageDownloaderListener mListener; public static interface ImageDownloaderListener { void onImageDownloaded(View v, Bitmap bmp); } private ImageManager(final Context ctx) { instance = this; mDowloadLooper = new LooperThread(); mDowloadLooper.start(); mUiHandler = new Handler() { @Override public void handleMessage(Message msg) { if (mStopped) { return; } ImageDownloadMessageData messageData = (ImageDownloadMessageData) msg.obj; View v = messageData.viewRef.get(); if (v != null && messageData.bitmap != null && messageData == getImageDownloadData(v, messageData.flags)) { if (messageData.listerner != null) { messageData.listerner.onImageDownloaded(v, messageData.bitmap); } else { if (v instanceof ImageView) { if ((messageData.flags & FLAG_IN_BACKGROUND) != 0) { BitmapDrawable bd = new BitmapDrawable(ctx.getResources(), messageData.bitmap); v.setBackgroundDrawable(bd); } else { ((ImageView) v).setImageBitmap(messageData.bitmap); } } else if (v instanceof ImageButton) { if ((messageData.flags & FLAG_IN_BACKGROUND) != 0) { BitmapDrawable bd = new BitmapDrawable(ctx.getResources(), messageData.bitmap); v.setBackgroundDrawable(bd); } else { ((ImageButton) v).setImageBitmap(messageData.bitmap); } } else { // no src BitmapDrawable bd = new BitmapDrawable(ctx.getResources(), messageData.bitmap); v.setBackgroundDrawable(bd); } } } }; }; // create cache dir if needed new File(ctx.getExternalCacheDir() + THUMB_FOLDER).mkdirs(); } public static boolean hasInstance() { return instance != null; } // public void clearListener() { // mListener = null; // } /** * this method don't change the default image (transparent if never change) * * @param ctx * @return the instance of ImageManager */ public static ImageManager getInstance(Context ctx) { mContext = ctx; if (instance == null) { instance = new ImageManager(ctx); } return instance; } /** * Get a the default image from a resource ID. * * @param defaultDrawableId * @return */ public Bitmap getDefaultImage(int defaultDrawableId) { Bitmap bitmap = mDefaultImageCache.get(defaultDrawableId); if (bitmap == null) { bitmap = BitmapFactory.decodeResource(mContext.getResources(), defaultDrawableId); mDefaultImageCache.put(defaultDrawableId, bitmap); } return bitmap; } /** * * @param url * url of the image to download * @param view * image container (put in background if is not an ImageView) * @param flags * @param checkNewVersionDelay * delay between two checks of the image in milliseconds * @param defaultImageId * TODO */ public void download(String url, View view, int flags, long checkNewVersionDelay, int defaultImageId) { download(null, url, view, flags, checkNewVersionDelay, defaultImageId); } /** * * @param listener * call when download is finished * @param url * url of the image to download * @param view * image container (put in background if is not an ImageView) * @param flags * @param checkNewVersionDelay * delay between two checks of the image in milliseconds * @param defaultImageId * TODO * @param defaultDrawableId */ public void download(ImageDownloaderListener listener, final String url, View view, int flags, long checkNewVersionDelay, int defaultImageId) { if (mStopped || TextUtils.isEmpty(url)) { return; } // mListener = listener; // get image from cache Bitmap cachedBitmap = null; if ((flags & FLAG_GET_THUMBNAIL) != 0) { cachedBitmap = mImageLiveCache.get(url + THUMB_FOLDER); } else { cachedBitmap = mImageLiveCache.get(url); } ImageDownloadMessageData messageData = new ImageDownloadMessageData(url, view, defaultImageId); messageData.listerner = listener; DownloadedDrawable downloadedDrawable; downloadedDrawable = new DownloadedDrawable(messageData, mContext.getResources(), cachedBitmap != null ? cachedBitmap : getDefaultImage(defaultImageId)); if (view != null) { if (view instanceof ImageView) { if ((flags & FLAG_IN_BACKGROUND) != 0) { view.setBackgroundDrawable(downloadedDrawable); } else { ((ImageView) view).setImageDrawable(downloadedDrawable); } } else if (view instanceof ImageButton) { if ((flags & FLAG_IN_BACKGROUND) != 0) { view.setBackgroundDrawable(downloadedDrawable); } else { ((ImageButton) view).setImageDrawable(downloadedDrawable); } } else { view.setBackgroundDrawable(downloadedDrawable); } } if (cachedBitmap != null) { // Log.d("test", "version form cache"); Message respMessage = new Message(); messageData.bitmap = cachedBitmap; messageData.flags = flags; respMessage.obj = messageData; mUiHandler.sendMessage(respMessage); } else { messageData.flags = flags; // check if available from sd card if (isSDCacheReadable()) { File extCacheDir = mContext.getExternalCacheDir(); final File img = new File(extCacheDir, md5(url)); final Long currentDate = Calendar.getInstance().getTimeInMillis(); if (img.exists()) { Message msg = new Message(); msg.obj = messageData; msg.what = MSG_LOAD_FROM_SD; mDowloadLooper.enqueueMessage(msg, true); // Log.d("test", "version form sdcard"); if (img.lastModified() + checkNewVersionDelay < currentDate) { final ImageDownloadMessageData test = messageData; Thread thread = new Thread(new Runnable() { @Override public void run() { Message msg = new Message(); msg.obj = test; URL connexion; try { connexion = new URL(url); URLConnection urlConnection; urlConnection = connexion.openConnection(); urlConnection.connect(); String date = urlConnection.getHeaderField("Last-Modified"); if (img.lastModified() >= mDateFormater.parse(date).getTime()) { // Log.d("test", "version form sdcard after server check"); return; } else { // load from web // Log.d("test", "version form server"); msg.what = MSG_DOWNLOAD; mDowloadLooper.enqueueMessage(msg, false); } urlConnection = null; } catch (Exception e) { return; } } }); thread.start(); } } else { Message msg = new Message(); msg.obj = messageData; // load from web // Log.d("test", "version form server"); msg.what = MSG_DOWNLOAD; mDowloadLooper.enqueueMessage(msg, false); } } } } private boolean isSDCacheReadable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { return true; } else { return false; } } private boolean isSDCacheWritable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } else { return false; } } private static ImageDownloadMessageData getImageDownloadData(View view, int flags) { if (view != null) { Drawable drawable = null; if (view instanceof ImageView) { if ((flags & FLAG_IN_BACKGROUND) != 0) { drawable = view.getBackground(); } else { drawable = ((ImageView) view).getDrawable(); } } else if (view instanceof ImageButton) { if ((flags & FLAG_IN_BACKGROUND) != 0) { drawable = view.getBackground(); } else { drawable = ((ImageView) view).getDrawable(); } } else { drawable = view.getBackground(); } if (drawable instanceof DownloadedDrawable) { DownloadedDrawable downloadedDrawable = (DownloadedDrawable) drawable; return downloadedDrawable.getImageDownloadData(); } } return null; } public static void release() { instance.mStopped = true; instance.mDowloadLooper.stopLooper(); // for (Bitmap bmp : instance.mImageLiveCache.snapshot().values()) { // bmp.recycle(); // } instance.mImageLiveCache.evictAll(); instance = null; } private static class ImageDownloadMessageData { public String url; public Bitmap bitmap; public final WeakReference<View> viewRef; public int flags; public ImageDownloaderListener listerner; public ImageDownloadMessageData(String url, View view, int defaultImageId) { this.url = url; viewRef = new WeakReference<View>(view); } } private class LooperThread extends Thread { public Handler mHandler; @Override public void run() { Looper.prepare(); synchronized (instance) { mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MSG_STOP) { Looper.myLooper().quit(); return; } // separate from stop message to do the looper quit once if (mStopped) { return; } ImageDownloadMessageData messageData = (ImageDownloadMessageData) msg.obj; boolean thumbMode = (messageData.flags & FLAG_GET_THUMBNAIL) != 0; // check cache in case the dowload has already be done messageData.bitmap = mImageLiveCache.get(messageData.url); if (messageData.bitmap == null) { switch (msg.what) { case MSG_LOAD_FROM_SD: { // read from SD View v = messageData.viewRef.get(); if (v != null) { File cacheDir = mContext.getExternalCacheDir(); String fileName = md5(messageData.url); File img; if (thumbMode) { img = new File(cacheDir + THUMB_FOLDER, fileName); } else { img = new File(cacheDir, fileName); } // messageData.bitmap = shrinkBitmap(img.getPath(), BITMAP_MAX_WIDTH, BITMAP_MAX_HEIGHT); messageData.bitmap = BitmapFactory.decodeFile(img.getPath()); } else { // view has been garbaged return; } break; } case MSG_DOWNLOAD: { // download bitmap Bitmap bitmap = downloadBitmap(messageData.url); Bitmap thumbBmp = null; // save to SD cache if (bitmap != null && isSDCacheWritable()) { View v = messageData.viewRef.get(); if (v != null) { File cacheDir = mContext.getExternalCacheDir(); File img = new File(cacheDir, md5(messageData.url)); File thumb = new File(cacheDir + THUMB_FOLDER, md5(messageData.url)); try { // make rounded corners if ((messageData.flags & FLAG_ROUNDED_CORNERS) != 0) { // Log.d("ImprovedImageDownloader.handleMessage():", "create rounded corners"); bitmap = getRoundedCornerBitmap(bitmap); } // save image to SD if (img.createNewFile()) { OutputStream out = new FileOutputStream(img); bitmap.compress(Bitmap.CompressFormat.PNG, 80, out); out.flush(); out.close(); } // make thumnail if needed if ((messageData.flags & FLAG_NO_THUMBNAIL) == 0) { // create thumb double ratio = (double) THUMB_MAX_HEIGHT / (double) bitmap.getHeight(); thumbBmp = Bitmap.createScaledBitmap(bitmap, (int) (bitmap.getWidth() * ratio), (int) (bitmap.getHeight() * ratio), true); // recycle image if not needed. if (thumbMode && thumbBmp != bitmap) { bitmap.recycle(); } // save thumb to SD if (thumb.createNewFile()) { OutputStream out = new FileOutputStream(thumb); thumbBmp.compress(Bitmap.CompressFormat.PNG, 80, out); out.flush(); out.close(); } else { // Log.d("ImprovedImageDownloader.LooperThread.run():", "failed to create thumb"); } // recycle image if not needed if (!thumbMode && thumbBmp != bitmap) { thumbBmp.recycle(); } } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } // store bitmap messageData.bitmap = thumbMode ? thumbBmp : bitmap; cleanupSdCard(); break; } default: break; } if (messageData.bitmap == null) { return; } // add to live cache String key; if (thumbMode) { key = messageData.url + THUMB_FOLDER; } else { key = messageData.url; } mImageLiveCache.put(key, messageData.bitmap); } // send message to ui handler to refresh view Message respMessage = new Message(); respMessage.obj = messageData; mUiHandler.sendMessage(respMessage); } }; instance.notifyAll(); } Looper.loop(); } public void stopLooper() { Message msg = new Message(); msg.what = MSG_STOP; mHandler.sendMessageAtFrontOfQueue(msg); } public void enqueueMessage(Message msg, boolean priorityMessage) { // for paranoia only, seem to be useless now... if (mHandler == null) { synchronized (instance) { try { instance.wait(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } if (priorityMessage) { mHandler.sendMessageAtFrontOfQueue(msg); } else { mHandler.sendMessage(msg); } } private Bitmap downloadBitmap(String url) { final AndroidHttpClient client = AndroidHttpClient.newInstance("Android"); final HttpGet getRequest = new HttpGet(url); try { HttpResponse response = client.execute(getRequest); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { Header[] headers = response.getHeaders("Location"); if (headers != null && headers.length > 0) { String newUrl = headers[headers.length - 1].getValue(); // call again with new URL return downloadBitmap(newUrl); } else { Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url); return null; } } final HttpEntity entity = response.getEntity(); if (entity != null) { InputStream inputStream = null; try { inputStream = entity.getContent(); // final Bitmap bitmap = shrinkBitmap(new FlushedInputStream(inputStream), BITMAP_MAX_WIDTH, BITMAP_MAX_HEIGHT); final Bitmap bitmap = BitmapFactory.decodeStream(new FlushedInputStream(inputStream)); return bitmap; } finally { if (inputStream != null) { inputStream.close(); } entity.consumeContent(); } } } catch (Exception e) { // Could provide a more explicit error message for IOException or IllegalStateException getRequest.abort(); Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e); } finally { if (client != null) { client.close(); } } return null; } } public static Bitmap getRoundedCornerBitmap(Bitmap bitmap) { Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); Canvas canvas = new Canvas(output); final int color = 0xff424242; final Paint paint = new Paint(); final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); final RectF rectF = new RectF(rect); final float roundPx = 20; paint.setAntiAlias(true); canvas.drawARGB(0, 0, 0, 0); paint.setColor(color); canvas.drawRoundRect(rectF, roundPx, roundPx, paint); paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); canvas.drawBitmap(bitmap, rect, rect, paint); if (output != bitmap) { bitmap.recycle(); } return output; } public static Bitmap shrinkBitmap(String file, int width, int height) { try { BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; bmpFactoryOptions.inPurgeable = true; bmpFactoryOptions.inInputShareable = true; Bitmap bitmap = BitmapFactory.decodeFile(file, bmpFactoryOptions); computeRatio(width, height, bmpFactoryOptions); bmpFactoryOptions.inJustDecodeBounds = false; bitmap = BitmapFactory.decodeFile(file, bmpFactoryOptions); return bitmap; } catch (OutOfMemoryError e) { // Log.d("ImprovedImageDownloader.shrinkBitmap():", "memory !"); return null; } } public static Bitmap shrinkBitmap(InputStream stream, int width, int height) { try { // TODO check for a better solution for handling purgation BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); Bitmap bitmap = BitmapFactory.decodeStream(stream, null, bmpFactoryOptions); computeRatio(width, height, bmpFactoryOptions); if (bitmap != null) { final int ratio = bmpFactoryOptions.inSampleSize; Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, bmpFactoryOptions.outWidth / ratio, bmpFactoryOptions.outHeight / ratio, false); if (scaledBitmap != bitmap) { bitmap.recycle(); } return scaledBitmap; } return null; } catch (OutOfMemoryError e) { // Log.d("ImprovedImageDownloader.shrinkBitmap():", "memory !"); return null; } } private static void computeRatio(int width, int height, BitmapFactory.Options bmpFactoryOptions) { int heightRatio; int widthRatio; if (bmpFactoryOptions.outHeight > bmpFactoryOptions.outWidth) { heightRatio = (int) FloatMath.floor(bmpFactoryOptions.outHeight / (float) width); widthRatio = (int) FloatMath.floor(bmpFactoryOptions.outWidth / (float) height); } else { heightRatio = (int) FloatMath.floor(bmpFactoryOptions.outHeight / (float) height); widthRatio = (int) FloatMath.floor(bmpFactoryOptions.outWidth / (float) width); } if (heightRatio > 1 || widthRatio > 1) { if (heightRatio < widthRatio) { bmpFactoryOptions.inSampleSize = widthRatio; } else { bmpFactoryOptions.inSampleSize = heightRatio; } } else { bmpFactoryOptions.inSampleSize = 1; } } static Context t; private static class DownloadedDrawable extends BitmapDrawable { private final WeakReference<ImageDownloadMessageData> imageDownloadDataReference; public DownloadedDrawable(ImageDownloadMessageData imageDownloadData, Resources resources, Bitmap bitmap) { super(resources, bitmap); imageDownloadDataReference = new WeakReference<ImageDownloadMessageData>(imageDownloadData); } public ImageDownloadMessageData getImageDownloadData() { return imageDownloadDataReference.get(); } } private static class FlushedInputStream extends FilterInputStream { public FlushedInputStream(InputStream inputStream) { super(inputStream); } @Override public long skip(long n) throws IOException { long totalBytesSkipped = 0L; while (totalBytesSkipped < n) { long bytesSkipped = in.skip(n - totalBytesSkipped); if (bytesSkipped == 0L) { int readByte = read(); if (readByte < 0) { break; // we reached EOF } else { bytesSkipped = 1; // we read one byte } } totalBytesSkipped += bytesSkipped; } return totalBytesSkipped; } } public static String md5(String s) { try { // Create MD5 Hash MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); digest.update(s.getBytes()); byte messageDigest[] = digest.digest(); // Create Hex String StringBuffer hexString = new StringBuffer(); for (int i = 0; i < messageDigest.length; i++) { hexString.append(Integer.toHexString(0xFF & messageDigest[i])); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } private void cleanupSdCard() { File cacheDir = mContext.getExternalCacheDir(); File[] files = cacheDir.listFiles(); Arrays.sort(files, new Comparator<File>() { @Override public int compare(File f1, File f2) { return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); } }); long size = getImagesSize(files); int i = 0; while (size > SD_CACHE_SIZE && i < files.length) { File f = files[i]; if (!f.getName().startsWith(".")) { size -= f.length(); f.delete(); } i++; } } private long getImagesSize(File[] files) { long size = 0; for (File file : files) { if (!file.getName().startsWith(".")) { size += file.length(); } } return size; } public static Bitmap quickCachedImage(String url) { if (instance != null && url != null) { Bitmap cachedBitmap = instance.mImageLiveCache.get(url); if (cachedBitmap != null && !cachedBitmap.isRecycled()) { return cachedBitmap.copy(Config.ARGB_8888, false); } } return null; } }