/*
*
* 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.yixia.zi.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import com.yixia.zi.BuildConfig;
/**
* A subclass of {@link ImageWorker} that fetches images from a URL.
*/
public class ImageFetcher extends ImageWorker {
private static final String TAG = "ImageFetcher";
public static final int IO_BUFFER_SIZE_BYTES = 4 * 1024; // 4KB
private ImageFetcherParams mFetcherParams;
// Default fetcher params
private static final int DEFAULT_MAX_THUMBNAIL_BYTES = 70 * 1024; // 70KB
private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
private static final int DEFAULT_MIN_IMAGE_HEIGHT = 256;
private static final int DEFAULT_MIN_IMAGE_WIDTH = 256;
private static final int DEFAULT_HTTP_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
private static final String DEFAULT_HTTP_CACHE_DIR = "http";
/**
* Create an ImageFetcher specifying custom parameters.
*/
public ImageFetcher(Context context, ImageFetcherParams params) {
super(context);
setParams(params);
}
/**
* Create an ImageFetcher using default parameters.
*/
public ImageFetcher(Context context) {
super(context);
setParams(new ImageFetcherParams());
}
public void loadThumbnailImage(String key, ImageView imageView, Bitmap loadingBitmap) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, loadingBitmap);
}
public void loadThumbnailImage(String key, ImageView imageView, int resId) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, resId);
}
public void loadThumbnailImage(String key, ImageView imageView) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_THUMBNAIL), imageView, mLoadingBitmap);
}
public void loadImage(String key, ImageView imageView, Bitmap loadingBitmap) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, loadingBitmap);
}
public void loadImage(String key, ImageView imageView, int resId) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, resId);
}
public void loadImage(String key, ImageView imageView) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_NORMAL), imageView, mLoadingBitmap);
}
public void loadLocalImage(String key, ImageView imageView) {
loadImage(new ImageData(key, ImageData.IMAGE_TYPE_LOCAL), imageView, mLoadingBitmap);
}
public void setParams(ImageFetcherParams params) {
mFetcherParams = params;
}
/**
* Set the target image width and height.
*/
public void setImageSize(int width, int height) {
mFetcherParams.mImageWidth = width;
mFetcherParams.mImageHeight = height;
}
/**
* Set the target image size (width and height will be the same).
*/
public void setImageSize(int size) {
setImageSize(size, size);
}
/**
* The main process method, which will be called by the ImageWorker in the
* AsyncTask background thread.
*
* @param key The key to load the bitmap, in this case, a regular http URL
* @return The downloaded and resized bitmap
*/
private Bitmap processBitmap(String key, int type) {
Log.d(TAG, "processBitmap - " + key);
if (type == ImageData.IMAGE_TYPE_NORMAL) {
final File f = downloadBitmapToFile(mContext, key, mFetcherParams.mHttpCacheDir);
if (f != null) {
// Return a sampled down version
final Bitmap bitmap = decodeSampledBitmapFromFile(f.toString(), mFetcherParams.mImageWidth, mFetcherParams.mImageHeight);
f.delete();
return bitmap;
}
} else if (type == ImageData.IMAGE_TYPE_THUMBNAIL) {
final byte[] bitmapBytes = downloadBitmapToMemory(mContext, key, mFetcherParams.mMaxThumbnailBytes);
if (bitmapBytes != null) {
// Caution: we don't check the size of the bitmap here, we are relying on the output
// of downloadBitmapToMemory to not exceed our memory limits and load a huge bitmap
// into memory.
return BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
}
} else if (type == ImageData.IMAGE_TYPE_LOCAL) {
// Return a sampled down version
final Bitmap bitmap = decodeSampledBitmapFromFile(key, mFetcherParams.mImageThumbnailWidth, mFetcherParams.mImageThumbnailHeight);
return bitmap;
}
return null;
}
@Override
protected Bitmap processBitmap(Object key) {
final ImageData imageData = (ImageData) key;
return processBitmap(imageData.mKey, imageData.mType);
}
/**
* Download a bitmap from a URL, write it to a disk and return the File
* pointer. This implementation uses a simple disk cache.
*
* @param context The context to use
* @param urlString The URL to fetch
* @param maxBytes The maximum number of bytes to read before returning null
* to protect against OutOfMemory exceptions.
* @return A File pointing to the fetched bitmap
*/
public static byte[] downloadBitmapToMemory(Context context, String urlString, int maxBytes) {
HttpURLConnection urlConnection = null;
ByteArrayOutputStream out = null;
InputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return null;
}
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
out = new ByteArrayOutputStream(IO_BUFFER_SIZE_BYTES);
final byte[] buffer = new byte[128];
int total = 0;
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
total += bytesRead;
if (total > maxBytes) {
return null;
}
out.write(buffer, 0, bytesRead);
}
return out.toByteArray();
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmapToMemory - " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmapToMemory - " + e);
}
}
return null;
}
/**
* Download a bitmap from a URL, write it to a disk and return the File
* pointer. This implementation uses a simple disk cache.
*
* @param context The context to use
* @param urlString The URL to fetch
* @return A File pointing to the fetched bitmap
*/
public static File downloadBitmapToFile(Context context, String urlString, String uniqueName) {
final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
if (!cacheDir.exists()) {
cacheDir.mkdir();
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "downloadBitmap - downloading - " + urlString);
}
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
try {
final File tempFile = File.createTempFile("bitmap", null, cacheDir);
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
return null;
}
final InputStream in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE_BYTES);
out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return tempFile;
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (out != null) {
try {
out.close();
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap - " + e);
}
}
}
return null;
}
/**
* Decode and sample down a bitmap from a file to the requested width and
* height.
*
* @param filename The full path of the file to decode
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return A bitmap sampled down from the original with the same aspect ratio
* and dimensions that are equal to or greater than the requested width and
* height
*/
public static synchronized Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filename, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filename, options);
}
/**
* Calculate an inSampleSize for use in a
* {@link android.graphics.BitmapFactory.Options} object when decoding bitmaps
* using the decode* methods from {@link BitmapFactory}. This implementation
* calculates the closest inSampleSize that will result in the final decoded
* bitmap having a width and height equal to or larger than the requested
* width and height. This implementation does not ensure a power of 2 is
* returned for inSampleSize which can be faster when decoding but results in
* a larger bitmap which isn't as useful for caching purposes.
*
* @param options An options object with out* params already populated (run
* through a decode* method with inJustDecodeBounds==true
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return The value to be used for inSampleSize
*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float) height / (float) reqHeight);
} else {
inSampleSize = Math.round((float) width / (float) reqWidth);
}
// 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++;
}
}
return inSampleSize;
}
public static class ImageFetcherParams {
public int mImageWidth = DEFAULT_MAX_IMAGE_WIDTH;
public int mImageHeight = DEFAULT_MAX_IMAGE_HEIGHT;
public int mMaxThumbnailBytes = DEFAULT_MAX_THUMBNAIL_BYTES;
public int mHttpCacheSize = DEFAULT_HTTP_CACHE_SIZE;
public String mHttpCacheDir = DEFAULT_HTTP_CACHE_DIR;
public int mImageThumbnailWidth = DEFAULT_MIN_IMAGE_WIDTH;
public int mImageThumbnailHeight = DEFAULT_MIN_IMAGE_HEIGHT;
}
private static class ImageData {
public static final int IMAGE_TYPE_THUMBNAIL = 0;
public static final int IMAGE_TYPE_NORMAL = 1;
public static final int IMAGE_TYPE_LOCAL = 2;
public String mKey;
public int mType;
public ImageData(String key, int type) {
mKey = key;
mType = type;
}
@Override
public String toString() {
return mKey;
}
}
}