package com.kenny.openimgur.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.media.ExifInterface;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.res.ResourcesCompat;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.ImageView;
import com.kenny.openimgur.activities.SettingsActivity;
import com.kenny.openimgur.classes.OpengurApp;
import com.kenny.openimgur.classes.VideoCache;
import com.kenny.openimgur.ui.CircleBitmapDisplayer;
import com.nostra13.universalimageloader.cache.disc.DiskCache;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiskCache;
import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.utils.DiskCacheUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import pl.droidsonroids.gif.GifDrawable;
/**
* Created by kcampagna on 7/1/14.
*/
public class ImageUtil {
private static final String TAG = "ImageUtil";
private static ImageLoader imageLoader;
/**
* Converts a bitmap to grayscale
*
* @param bmpOriginal
* @return
*/
public static Bitmap toGrayScale(Bitmap bmpOriginal) {
if (bmpOriginal == null) {
return null;
}
int width, height;
height = bmpOriginal.getHeight();
width = bmpOriginal.getWidth();
// RGB_565 uses half the amount of pixels than ARGB_8888. Since this will be used for a notification,
// and is just gray we'll use RGB_565 to save on some memory
Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
Canvas c = new Canvas(bmpGrayscale);
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
c.drawBitmap(bmpOriginal, 0, 0, paint);
return bmpGrayscale;
}
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);
}
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromResource(File file, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
}
/**
* Loads a gif into an image view. The gif must have been saved to the disk cache before calling this method or it will fail
*
* @param imageView The ImageView where the gif will be displayed
* @param url The url of the image. This is the key for the cached image
* @param imageLoader The Imageloader where we will retrieve the image from
* @return if successful
*/
public static boolean loadAndDisplayGif(@Nullable ImageView imageView, @NonNull String url, @NonNull ImageLoader imageLoader) {
File file = DiskCacheUtils.findInCache(url, imageLoader.getDiskCache());
return loadAndDisplayGif(imageView, file);
}
/**
* Loads a gif into an image view. The gif must have been saved to the disk cache before calling this method or it will fail
*
* @param imageView The ImageView where the gif will be displayed
* @param file File of the gif
* @return
*/
public static boolean loadAndDisplayGif(@Nullable ImageView imageView, @Nullable File file) {
if (imageView == null) return false;
if (FileUtil.isFileValid(file)) {
try {
imageView.setImageDrawable(new GifDrawable(file));
return true;
} catch (IOException e) {
LogUtil.e(TAG, "Unable to play gif", e);
}
} else {
LogUtil.w(TAG, "Gif file is invalid");
}
return false;
}
public static ImageLoader getImageLoader(@NonNull Context context) {
if (imageLoader == null || !imageLoader.isInited()) {
initImageLoader(context.getApplicationContext());
imageLoader = ImageLoader.getInstance();
}
return imageLoader;
}
/**
* Initializes the ImageLoader
*
* @param context App context
*/
public static void initImageLoader(Context context) {
long discCacheSize = 1024 * 1024;
DiskCache discCache;
int threadPoolSize;
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
String discCacheAllowance = pref.getString(SettingsActivity.KEY_CACHE_SIZE, SettingsActivity.CACHE_SIZE_512MB);
String threadSize = pref.getString(SettingsActivity.KEY_THREAD_SIZE, SettingsActivity.THREAD_SIZE_5);
String cacheKey = pref.getString(SettingsActivity.KEY_CACHE_LOC, SettingsActivity.CACHE_LOC_INTERNAL);
File baseDir = getCacheDirectory(context, cacheKey);
checkForOldCache(pref, baseDir);
File dir = new File(baseDir, "image_cache");
switch (discCacheAllowance) {
case SettingsActivity.CACHE_SIZE_256MB:
discCacheSize *= 256;
break;
case SettingsActivity.CACHE_SIZE_1GB:
discCacheSize *= 1024;
break;
case SettingsActivity.CACHE_SIZE_2GB:
discCacheSize *= 2048;
break;
case SettingsActivity.CACHE_SIZE_UNLIMITED:
discCacheSize = -1;
break;
case SettingsActivity.CACHE_SIZE_512MB:
default:
discCacheSize *= 512;
break;
}
switch (threadSize) {
case SettingsActivity.THREAD_SIZE_7:
threadPoolSize = 7;
break;
case SettingsActivity.THREAD_SIZE_10:
threadPoolSize = 10;
break;
case SettingsActivity.THREAD_SIZE_12:
threadPoolSize = 12;
break;
case SettingsActivity.THREAD_SIZE_5:
default:
threadPoolSize = 5;
break;
}
if (discCacheSize > 0) {
try {
discCache = new LruDiskCache(dir, new Md5FileNameGenerator(), discCacheSize);
LogUtil.v(TAG, "Disc cache set to " + FileUtil.humanReadableByteCount(discCacheSize, false));
} catch (IOException e) {
LogUtil.e(TAG, "Unable to set the disc cache, falling back to unlimited", e);
discCache = new UnlimitedDiskCache(dir);
}
} else {
LogUtil.v(TAG, "Disc cache set to unlimited");
discCache = new UnlimitedDiskCache(dir);
}
final int memory = (int) (Runtime.getRuntime().maxMemory() / 8);
LogUtil.v(TAG, "Disc Cache located at " + discCache.getDirectory().getAbsolutePath());
LogUtil.v(TAG, "Using " + FileUtil.humanReadableByteCount(memory, false) + " for memory cache");
LogUtil.v(TAG, "Using " + threadPoolSize + " threads for image loader");
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.threadPoolSize(threadPoolSize)
.denyCacheImageMultipleSizesInMemory()
.diskCache(discCache)
.defaultDisplayImageOptions(getDefaultDisplayOptions().build())
.memoryCacheSize(memory)
.build();
if (ImageLoader.getInstance().isInited()) {
ImageLoader.getInstance().destroy();
}
ImageLoader.getInstance().init(config);
// Check our cache to see if we should delete it
long lastClear = pref.getLong("lastClear", 0);
// We will clear the cache every 3 days
if (lastClear == 0) {
pref.edit().putLong("lastClear", System.currentTimeMillis()).apply();
} else {
long currentTime = System.currentTimeMillis();
if (currentTime - lastClear >= DateUtils.DAY_IN_MILLIS * 3) {
LogUtil.v(TAG, "Cache older than 3 days, clearing");
ImageLoader.getInstance().clearMemoryCache();
ImageLoader.getInstance().clearDiskCache();
VideoCache.getInstance().deleteCache();
pref.edit().putLong("lastClear", currentTime).apply();
}
}
}
/**
* Returns the display options for the image loader when loading for the gallery.
* Fades in the images when loaded from the network. Also uses Bitmap.Config.RGB_565 for less memory usage
*
* @return
*/
public static DisplayImageOptions.Builder getDisplayOptionsForGallery() {
return getDefaultDisplayOptions()
.displayer(new FadeInBitmapDisplayer(250, true, false, false))
.bitmapConfig(Bitmap.Config.RGB_565)
.imageScaleType(ImageScaleType.EXACTLY);
}
/**
* Returns the display options for viewing an image in the view activity
* Uses a placeholder whiling loading images
*
* @return
*/
public static DisplayImageOptions.Builder getDisplayOptionsForView() {
return getDefaultDisplayOptions()
.showImageOnLoading(new ColorDrawable(Color.TRANSPARENT));
}
public static DisplayImageOptions.Builder getDisplayOptionsForComments() {
return getDefaultDisplayOptions()
.displayer(new CircleBitmapDisplayer(OpengurApp.getInstance().getResources()));
}
public static DisplayImageOptions.Builder getDisplayOptionsForFullscreen() {
return getDefaultDisplayOptions()
.cacheInMemory(false);
}
public static DisplayImageOptions.Builder getDisplayOptionsForPhotoPicker() {
return getDisplayOptionsForGallery()
.cacheOnDisk(false)
.considerExifParams(true)
.imageScaleType(ImageScaleType.EXACTLY_STRETCHED);
}
/**
* Returns the default display options for the image loader
* Resets view before loading, caches in memory and on disk.
*
* @return
*/
public static DisplayImageOptions.Builder getDefaultDisplayOptions() {
return new DisplayImageOptions.Builder()
.resetViewBeforeLoading(true)
.cacheInMemory(true)
.cacheOnDisk(true);
}
/**
* Returns the images rotation from it's EXIF data
*
* @param file Image file
* @return EXIF rotation, Undefined if no orientation was obtained
*/
public static int getImageRotation(File file) {
if (!FileUtil.isFileValid(file)) {
return ExifInterface.ORIENTATION_UNDEFINED;
}
try {
ExifInterface exif = new ExifInterface(file.getAbsolutePath());
return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
} catch (Exception e) {
LogUtil.e(TAG, "Unable to get EXIF data from file", e);
}
return ExifInterface.ORIENTATION_UNDEFINED;
}
/**
* Returns the directory to be used for caching
*
* @param context
* @param key
* @return
*/
public static File getCacheDirectory(Context context, String key) {
if (SettingsActivity.CACHE_LOC_EXTERNAL.equals(key) &&
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return context.getExternalCacheDir();
}
return context.getCacheDir();
}
/**
* Returns the thumbnail for the given image url
*
* @param url
* @param thumbnailSize
* @return
*/
public static String getThumbnail(String url, String thumbnailSize) {
if (TextUtils.isEmpty(url) || TextUtils.isEmpty(thumbnailSize)) {
Log.w(TAG, "Url or thumbnailSize is empty");
return null;
}
String[] fileExtension = url.split("^(.*[\\.])");
String[] imageUrl = url.split("\\.\\w+$");
if (fileExtension.length > 0 && imageUrl.length > 0) {
return imageUrl[imageUrl.length - 1] + thumbnailSize + "." + fileExtension[fileExtension.length - 1];
}
return null;
}
/**
* Returns a drawable that has been tinted
*
* @param drawableId
* @param resources
* @param color
* @return
*/
public static Drawable tintDrawable(@DrawableRes int drawableId, @NonNull Resources resources, int color) {
Drawable drawable = ResourcesCompat.getDrawable(resources, drawableId, null).mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
return drawable;
}
/**
* Saves a bitmap to a local file
*
* @param bitmap
* @return
*/
public static File saveBitmap(@NonNull Bitmap bitmap) {
File file = FileUtil.createFile(".jpeg");
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
} catch (Exception e) {
e.printStackTrace();
file = null;
} finally {
FileUtil.closeStream(out);
}
return file;
}
/**
* Decodes a file path to a bitmap and returns its width and height. This <b><i>WILL NOT</i></b> load the image int memory
*
* @param file The file path to decode
* @return An int array containing the width and height of the bitmap
*/
@NonNull
public static int[] getBitmapDimensions(@NonNull File file) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
return new int[]{options.outWidth, options.outHeight};
}
/**
* Returns the entire size of the image cache, including videos
*
* @param context
* @return
*/
public static long getTotalImageCacheSize(@NonNull Context context) {
long cacheSize = FileUtil.getDirectorySize(getImageLoader(context).getDiskCache().getDirectory());
cacheSize += VideoCache.getInstance().getCacheSize();
return cacheSize;
}
/**
* Checks if the user is using the new cache directory of images. If they are not, the old one will be deleted for updating.
* <p/>
* TODO Delete the method after several versions
*
* @param preferences
* @param cacheDir
*/
private static void checkForOldCache(@NonNull SharedPreferences preferences, @NonNull File cacheDir) {
if (!preferences.getBoolean("has_updated_cache", false)) {
preferences.edit().putBoolean("has_updated_cache", true).apply();
for (File f : cacheDir.listFiles()) {
if (!f.isDirectory()) f.delete();
}
}
}
}