Java tutorial
/* * Copyright 2009, 2010 Tyler Levine * * This file is part of Hunch for Android. * * Hunch for Android is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Hunch for Android 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Hunch for Android. If not, see <http://www.gnu.org/licenses/>. */ package com.hunch; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; import android.util.Log; import android.widget.ImageView; public class ImageManager { private final static Map<URL, SoftReference<Bitmap>> drawableMap = new HashMap<URL, SoftReference<Bitmap>>(); private static ExecutorService executor; private static ImageManager instance = null; // no instantiation private ImageManager() { } public static ImageManager getInstance() { if (instance == null) instance = new ImageManager(); startExecutor(); return instance; } private static void startExecutor() { // if we already have an executor, theres no need for another one. if (executor != null) return; if (Const.IMG_FETCH_THREADS > 1) executor = Executors.newFixedThreadPool(Const.IMG_FETCH_THREADS); else if (Const.IMG_FETCH_THREADS == 1) executor = Executors.newSingleThreadExecutor(); else executor = Executors.newCachedThreadPool(); } protected void cacheBitmap(Bitmap bitmap, URL url, Context context, CachePolicy... policies) { if (policies == null || policies.length == 0) { Log.w(Const.TAG, "tried to cache image with no cache policy?!?"); return; } List<CachePolicy> completedAdds = new ArrayList<CachePolicy>(policies.length); for (CachePolicy policy : policies) { // if we have already added to this cache, don't add again // in case someone accidently passes the same cache policy // more than once (which should never happen) if (completedAdds.contains(policy)) { continue; } switch (policy) { case MEMORY: addToMemoryCache(bitmap, url); break; case INTERNAL: addToInternalCache(bitmap, url, context); break; case CACHE: addToApplicationCache(bitmap, url, context); break; } completedAdds.add(policy); } } protected void addToMemoryCache(Bitmap d, URL url) { //Log.v( Const.TAG, "adding bitmap to memory cache (" + url + ")" ); // add by soft reference to the level one cache (in memory) drawableMap.put(url, new SoftReference<Bitmap>(d)); } protected void addToInternalCache(Bitmap d, URL url, Context context) { final String fileName = fileNameFromURL(url); File hunchImagesDir = context.getDir(Const.INTERNAL_IMG_DIR, Context.MODE_PRIVATE); // directory is guaranteed to exist by getDir() File imageFile = new File(hunchImagesDir, fileName); /*if( !imageFile.canWrite() ) { Log.w( Const.TAG, "can't write to internal image cache! (" + imageFile.toString() + ")" ); return; }*/ FileOutputStream fOut = null; try { fOut = new FileOutputStream(imageFile); } catch (FileNotFoundException e) { Log.w(Const.TAG, "can't find or write to file in internal cache! (" + imageFile.getAbsolutePath() + ")"); return; } boolean success = d.compress(Bitmap.CompressFormat.PNG, 0, fOut); if (!success) { Log.w(Const.TAG, "image compress for internal cache failed! (" + imageFile.getAbsolutePath() + ")"); } try { fOut.flush(); fOut.close(); } catch (IOException e) { Log.w(Const.TAG, "couldn't flush or close output stream for internal cache! (" + imageFile.getAbsolutePath() + ")"); } } protected void addToApplicationCache(Bitmap d, URL url, Context context) { final String fileName = fileNameFromURL(url); File cachedImagesDir = new File(context.getCacheDir(), Const.CACHE_IMG_DIR); if (!cachedImagesDir.exists()) { cachedImagesDir.mkdir(); } File imageFile = new File(cachedImagesDir, fileName); /*if( !imageFile.canWrite() ) { Log.w( Const.TAG, "can't write to app image cache! (" + imageFile.toString() + ")" ); return; }*/ FileOutputStream fOut = null; try { fOut = new FileOutputStream(imageFile); } catch (FileNotFoundException e) { Log.w(Const.TAG, "can't find or write to file in app cache! (" + imageFile.getAbsolutePath() + ")"); e.printStackTrace(); return; } boolean success = d.compress(Bitmap.CompressFormat.PNG, 0, fOut); if (!success) { Log.w(Const.TAG, "image compress for app cache failed! (" + imageFile.getAbsolutePath() + ")"); } try { fOut.flush(); fOut.close(); } catch (IOException e) { Log.w(Const.TAG, "couldn't flush or close output stream for app cache! (" + imageFile.getAbsolutePath() + ")"); } } protected String fileNameFromURL(URL url) { String urlString = url.toString(); String retString = urlString.substring(urlString.lastIndexOf('/') + 1, urlString.length()); return retString; } protected FileInputStream getImageFileStreamFromCache(final Context context, final URL url) { final String fileName = fileNameFromURL(url); File hunchCacheDir = new File(context.getCacheDir(), Const.CACHE_IMG_DIR); File[] imageFileList = hunchCacheDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String aFilename) { return aFilename.equals(fileName); } }); if (imageFileList == null || imageFileList.length == 0) { return null; } if (imageFileList.length > 1) { Log.i(Const.TAG, "found " + imageFileList.length + " images for url in app cache! " + "(" + fileName + ")"); } FileInputStream iStream = null; try { iStream = new FileInputStream(imageFileList[0]); } catch (FileNotFoundException e) { return null; } //Log.v( Const.TAG, "found image in app cache (" + url + ")" ); return iStream; } protected FileInputStream getImageFileStreamFromInternal(final Context context, final URL url) { final String fileName = fileNameFromURL(url); File internalImagesDir = context.getDir(Const.INTERNAL_IMG_DIR, Context.MODE_PRIVATE); // the directory is guaranteed to exist by getDir() File[] imageFileList = internalImagesDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String aFilename) { return fileName.equals(aFilename); } }); if (imageFileList == null || imageFileList.length == 0) { return null; } if (imageFileList.length > 1) { Log.i(Const.TAG, "found " + imageFileList.length + " images for url in internal cache! " + "(" + fileName + ")"); } FileInputStream iStream = null; try { iStream = new FileInputStream(imageFileList[0]); } catch (FileNotFoundException e) { return null; } //Log.v( Const.TAG, "found image in internal cache (" + url + ")" ); return iStream; } protected Drawable getDrawableFromMemoryCache(final Context context, final URL url) { SoftReference<Bitmap> drawableRef = drawableMap.get(url); if (drawableRef != null) { // the key exists Bitmap image = drawableRef.get(); if (image != null) { Log.v(Const.TAG, "found image in memory cache (" + fileNameFromURL(url) + ")"); // the image hasn't been gc'd yet. return new BitmapDrawable(context.getResources(), image); } else { // the drawable has been gc'd, remove the key drawableMap.remove(url); } } return null; } protected Drawable getCachedCategoryImage(final Context context, final URL url) { // is the drawable in the level one cache (in memory) Drawable drawable = getDrawableFromMemoryCache(context, url); if (drawable != null) { // we got lucky, it was in memory return drawable; } // otherwise check the internal file cache FileInputStream imageFileStream = getImageFileStreamFromInternal(context, url); // if the stream doesn't exist, we don't have the image cached if (imageFileStream == null) { return null; } Bitmap bitmap = BitmapFactory.decodeStream(imageFileStream); // add the bitmap to the in memory cache addToMemoryCache(bitmap, url); return new BitmapDrawable(context.getResources(), bitmap); } protected Drawable getCachedTopicImage(final Context context, final URL url) { // is the drawable in the level one cache (in memory) Drawable drawable = getDrawableFromMemoryCache(context, url); if (drawable != null) { // we got lucky, it was in memory return drawable; } // otherwise check the internal file cache FileInputStream imageFileStream = getImageFileStreamFromCache(context, url); // if the stream doesn't exist, we don't have the image cached if (imageFileStream == null) { return null; } Bitmap bitmap = BitmapFactory.decodeStream(imageFileStream); return new BitmapDrawable(context.getResources(), bitmap); } protected Runnable getDownloadTask(final URL url, final Callback callback, final Context context, final CachePolicy... level) { // hander to deal with the image once UI thread has it final Handler imgHandler = new Handler() { @Override public void handleMessage(Message msg) { Drawable d = (Drawable) msg.obj; callback.callComplete(d); } }; // the job to grab the image off the network and cache it Runnable task = new Runnable() { @Override public void run() { try { Log.d(Const.TAG, String.format("[%d] fetching image from URI (%s)", Thread.currentThread().getId(), url.toString())); InputStream iStream = ImageManager.this.getInputStream(url); final Bitmap image = BitmapFactory.decodeStream(iStream); BitmapDrawable drawable = new BitmapDrawable(context.getResources(), image); final Message msg = imgHandler.obtainMessage(); msg.obj = drawable; // send off the message with the drawable imgHandler.sendMessage(msg); // then cache the stream cacheBitmap(image, url, context, level); } catch (FileNotFoundException e) { Log.e(Const.TAG, "couldn't get image off network (404 error)"); } catch (IOException e) { Log.e(Const.TAG, "couldn't get image off network"); } } }; return task; } protected InputStream getInputStream(URL url) throws IOException { DefaultHttpClient httpClient = new DefaultHttpClient(); HttpGet request = new HttpGet(url.toString()); HttpResponse response = httpClient.execute(request); return response.getEntity().getContent(); } /** * Get the image for a Hunch category. These images are cached into internal storage * after the first time they are downloaded. If the image is not present in the cache, * it will be downloaded in a background thread then cached, and a placeholder image * will be displayed it its place. * * @param context Application context (to get to internal storage dir) * @param image The ImageView that will display the image * @param imgURL The URL to the image. */ public void getCategoryImage(final Context context, final ImageView image, final String imgURL) { URL url = stringToURL(imgURL); Drawable cachedDrawable = getCachedCategoryImage(context, url); if (cachedDrawable != null) { // the image is in the cache image.setImageDrawable(cachedDrawable); } else { // the image is not in the cache, must download downloadCategoryDrawable(url, context, new Callback() { @Override public void callComplete(Drawable d) { image.setImageDrawable(d); } }); // set it to the placeholder image for now image.setImageResource(Const.CAT_DEFAULT_IMG); } } public void getTopicImage(final Context context, final ImageView image, final String imgURL) { URL url = stringToURL(imgURL); Drawable cachedDrawable = getCachedTopicImage(context, url); if (cachedDrawable != null) { // the image is in the cache image.setImageDrawable(cachedDrawable); } else { // the image is not in the cache, must download downloadTopicDrawable(url, context, new Callback() { @Override public void callComplete(Drawable d) { image.setImageDrawable(d); } }); // set it to the placeholder image for now image.setImageResource(Const.TOPIC_DEFAULT_IMG); } } protected URL stringToURL(String url) { URL temp = null; try { temp = new URL(url); } catch (MalformedURLException e) { Log.w(Const.TAG, "Malformed URL Exception! (" + url + ")"); e.printStackTrace(); // TODO: better handling of malformed urls? // but really this should never happen unless the // hunch API starts returning bad URLs. I guess I'll // rely on the hunch API returning valid and parseable // URLs for now. return null; } return temp; } protected void downloadCategoryDrawable(URL imgURL, Context context, Callback callback) { Runnable task = getDownloadTask(imgURL, callback, context, CachePolicy.MEMORY, CachePolicy.INTERNAL); executor.execute(task); } protected void downloadTopicDrawable(URL imgURL, Context context, Callback callback) { Runnable task = getDownloadTask(imgURL, callback, context, CachePolicy.MEMORY, CachePolicy.CACHE); executor.execute(task); } public void getTopicImageWithCallback(Context context, String imgURL, Callback callback) { Log.d(Const.TAG, "getting image drawable (" + imgURL + ")"); URL url = stringToURL(imgURL); Drawable cachedDrawable = getCachedTopicImage(context, url); if (cachedDrawable != null) { // the image is in the cache Log.d(Const.TAG, "Set image drawable from cache (" + imgURL + ")"); callback.callComplete(cachedDrawable); } else { // the image is not in the cache, must download Log.d(Const.TAG, "downloading image drawable from net (" + imgURL + ")"); downloadTopicDrawable(url, context, callback); } } public interface Callback { public void callComplete(Drawable d); } // INTERNAL is the internal memory // CACHE is the application cache // MEMORY is an in-memory HashMap backed with SoftReferences protected enum CachePolicy { INTERNAL, CACHE, MEMORY } }