package de.thedevstack.conversationsplus.utils; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.net.Uri; import android.os.AsyncTask; import android.util.DisplayMetrics; import android.util.LruCache; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.concurrent.RejectedExecutionException; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.exceptions.ImageResizeException; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.utils.ui.ViewUtil; /** * This util provides */ public final class ImageUtil { private static int IMAGE_SIZE = 1920; private static LruCache BITMAP_CACHE; /** * Returns a bitmap from the cache. * @see LruCache#get(Object) for details * @param key the key of the bitmap to get * @return the bitmap */ public static Bitmap getBitmapFromCache(String key) { return BITMAP_CACHE.get(key); } /** * Adds a bitmap with the given key to the cache. * @see LruCache#put(Object, Object) for details * @param key the key to identify this bitmap * @param bitmap the bitmap to cache */ public static void addBitmapToCache(String key, Bitmap bitmap) { BITMAP_CACHE.put(key, bitmap); } /** * Removes the bitmap with given key from the cache. * @param key the key of the bitmap to remove */ public static void removeBitmapFromCache(String key) { BITMAP_CACHE.remove(key); } /** * Clears the cache. * @see LruCache#evictAll() for more details. */ public static void evictBitmapCache() { BITMAP_CACHE.evictAll(); } /** * Initializes the bitmap cache. * This has to be executed once on application start. * @see LruCache#LruCache(int) for details */ public static void initBitmapCache() { Logging.i("Conversations+ImageUtil", "Initializing BitmapCache"); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; BITMAP_CACHE = new LruCache(cacheSize) { @Override protected int sizeOf(final String key, final Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; } /** * Resizes a given bitmap and return a new and resized bitmap. * The bitmap is only resized if either the width or the height of the original bitmap is smaller than the given size. * @param originalBitmap the bitmap to resize * @param size the size to scale to * @return new and resized bitmap or the original bitmap if width and height are smaller than size */ public static Bitmap resize(Bitmap originalBitmap, int size) { int w = originalBitmap.getWidth(); int h = originalBitmap.getHeight(); if (Math.max(w, h) > size) { int scalledW; int scalledH; if (w <= h) { scalledW = (int) (w / ((double) h / size)); scalledH = size; } else { scalledW = size; scalledH = (int) (h / ((double) w / size)); } return Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); } else { return originalBitmap; } } /** * Resizes and rotates an image given by uri and returns the bitmap. * @param image the uri of the image to be resized and rotated * @return resized and rotated bitmap * @throws ImageResizeException */ public static Bitmap resizeAndRotateImage(Uri image) throws ImageResizeException { return ImageUtil.resizeAndRotateImage(image, 0); } /** * Resizes and rotates an image given by uri and returns the bitmap. * @param image the uri of the image to be resized and rotated * @return resized and rotated bitmap * @throws ImageResizeException */ private static Bitmap resizeAndRotateImage(Uri image, int sampleSize) throws ImageResizeException { InputStream imageInputStream = null; try { imageInputStream = StreamUtil.openInputStreamFromContentResolver(image); Bitmap originalBitmap; BitmapFactory.Options options = new BitmapFactory.Options(); int inSampleSize = (int) Math.pow(2, sampleSize); Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); options.inSampleSize = inSampleSize; originalBitmap = BitmapFactory.decodeStream(imageInputStream, null, options); imageInputStream.close(); if (originalBitmap == null) { throw new ImageResizeException(R.string.error_not_an_image_file); } Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE); int rotation = ImageUtil.getRotation(image); if (rotation > 0) { scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation); } return scaledBitmap; } catch (FileNotFoundException e) { throw new ImageResizeException(R.string.error_file_not_found); } catch (IOException e) { throw new ImageResizeException(R.string.error_io_exception); } catch (OutOfMemoryError e) { ++sampleSize; if (sampleSize <= 3) { return resizeAndRotateImage(image, sampleSize); } else { throw new ImageResizeException(R.string.error_out_of_memory); } } finally { StreamUtil.close(imageInputStream); } } /** * Returns the rotation from the exif information of an image identified with the given uri. * The orientation is retrieved by parsing the stream of the image. * FileNotFoundException is silently ignored. * @param image the uri of the image to get the rotation * @return the rotation value for the image, 0 if the file cannot be found. */ public static int getRotation(Uri image) { InputStream is = null; try { is = StreamUtil.openInputStreamFromContentResolver(image); return ExifHelper.getOrientation(is); } catch (FileNotFoundException e) { return 0; } finally { StreamUtil.close(is); } } /** * Returns a thumbnail for a bitmap in a message. * @param message the message to get the thumbnail for * @param size the size to resize the original image to * @param cacheOnly whether only cached images should be returned or not * @return the resized thumbail * @throws FileNotFoundException if the original image does not exist anymore or an IOException occurs. */ public static Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { Bitmap thumbnail = ImageUtil.getBitmapFromCache(message.getUuid()); if ((thumbnail == null) && (!cacheOnly)) { File file = FileBackend.getFile(message); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(file, size); Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options); if (fullsize == null) { throw new FileNotFoundException(); } thumbnail = resize(fullsize, size); thumbnail = rotate(thumbnail, file.getAbsolutePath()); ImageUtil.addBitmapToCache(message.getUuid(), thumbnail); } return thumbnail; } /** * Rotates an bitmap. Only the values 90°, 180° and 270° are considered to rotate the image. * The orientation information is read using the ExifInterface. * @param original the original bitmap * @param srcPath the path to the original bitmap (used to read the exif information) * @return rotated bitmap, or original bitmap if criteria are not met * @throws IOException */ public static Bitmap rotate(Bitmap original, String srcPath) { try { ExifInterface exif = new ExifInterface(srcPath); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); int rotation = 0; switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: rotation = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: rotation = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: rotation = 270; break; } if (rotation > 0) { return rotate(original, rotation); } } catch (IOException e) { Logging.w("filebackend", "Error while rotating image, returning original (" + e.getMessage() + ")"); } return original; } /** * Rotates a bitmap with given degrees. * @param bitmap the bitmap to be rotated * @param degree the degrees to rotate the bitmap * @return a newly created bitmap */ public static Bitmap rotate(Bitmap bitmap, int degree) { int w = bitmap.getWidth(); int h = bitmap.getHeight(); Matrix mtx = new Matrix(); mtx.postRotate(degree); return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); } public static Bitmap cropCenterSquare(Uri image, int size) { if (image == null) { return null; } InputStream is = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image, size); is = StreamUtil.openInputStreamFromContentResolver(image); Bitmap input = BitmapFactory.decodeStream(is, null, options); if (input == null) { return null; } else { int rotation = getRotation(image); if (rotation > 0) { input = rotate(input, rotation); } return cropCenterSquare(input, size); } } catch (FileNotFoundException e) { return null; } finally { StreamUtil.close(is); } } public static Bitmap cropCenter(Uri image, int newHeight, int newWidth) { if (image == null) { return null; } InputStream is = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth)); is = StreamUtil.openInputStreamFromContentResolver(image); Bitmap source = BitmapFactory.decodeStream(is, null, options); if (source == null) { return null; } int sourceWidth = source.getWidth(); int sourceHeight = source.getHeight(); float xScale = (float) newWidth / sourceWidth; float yScale = (float) newHeight / sourceHeight; float scale = Math.max(xScale, yScale); float scaledWidth = scale * sourceWidth; float scaledHeight = scale * sourceHeight; float left = (newWidth - scaledWidth) / 2; float top = (newHeight - scaledHeight) / 2; RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(dest); canvas.drawBitmap(source, null, targetRect, null); return dest; } catch (FileNotFoundException e) { return null; } finally { StreamUtil.close(is); } } public static Bitmap cropCenterSquare(Bitmap input, int size) { int w = input.getWidth(); int h = input.getHeight(); float scale = Math.max((float) size / h, (float) size / w); float outWidth = scale * w; float outHeight = scale * h; float left = (size - outWidth) / 2; float top = (size - outHeight) / 2; RectF target = new RectF(left, top, left + outWidth, top + outHeight); Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(output); canvas.drawBitmap(input, null, target, null); return output; } public static int calcSampleSize(Uri image, int size) throws FileNotFoundException { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options); int height = options.outHeight; int width = options.outWidth; if (null != bmp) { bmp.recycle(); } return calcSampleSize(width, height, size); } public static int calcSampleSize(File image, int size) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(image.getAbsolutePath(), options); int height = options.outHeight; int width = options.outWidth; if (null != bmp) { bmp.recycle(); } return calcSampleSize(width, height, size); } private static int calcSampleSize(int width, int height, int size) { int inSampleSize = 1; if (height > size || width > size) { int halfHeight = height / 2; int halfWidth = width / 2; while ((halfHeight / inSampleSize) > size && (halfWidth / inSampleSize) > size) { inSampleSize *= 2; } } return inSampleSize; } static class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; private final WeakReference alternativeViewReference; private final boolean setSize; private Message message = null; public BitmapWorkerTask(ImageView imageView, TextView alternativeView, boolean setSize) { imageViewReference = new WeakReference<>(imageView); alternativeViewReference = new WeakReference<>(alternativeView); this.setSize = setSize; } @Override protected Bitmap doInBackground(Message... params) { message = params[0]; try { DisplayMetrics metrics = ConversationsPlusApplication.getAppContext().getResources().getDisplayMetrics(); return ImageUtil.getThumbnail(message, (int) (metrics.density * 288), false); } catch (FileNotFoundException e) { return null; } } @Override protected void onPostExecute(Bitmap bitmap) { final ImageView imageView = imageViewReference.get(); final TextView alternativeView = alternativeViewReference.get(); if (bitmap != null) { if (imageView != null) { imageView.setImageBitmap(bitmap); imageView.setBackgroundColor(0x00000000); if (setSize) { imageView.setLayoutParams(new LinearLayout.LayoutParams( bitmap.getWidth(), bitmap.getHeight())); } ViewUtil.gone(alternativeView); ViewUtil.visible(imageView); } } else { ViewUtil.visible(alternativeView); alternativeView.setText(message.getBody()); // TODO Should be the same as MessageAdapter.displayText... or display preview of ConversationAdapter ViewUtil.gone(imageView); } } } public static void loadBitmap(Message message, ImageView imageView, TextView alternativeView, boolean setSize) { Bitmap bm; try { DisplayMetrics metrics = ConversationsPlusApplication.getAppContext().getResources().getDisplayMetrics(); bm = ImageUtil.getThumbnail(message,(int) (metrics.density * 288), true); } catch (FileNotFoundException e) { bm = null; } if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); if (setSize) { imageView.setLayoutParams(new LinearLayout.LayoutParams( bm.getWidth(), bm.getHeight())); } ViewUtil.gone(alternativeView); ViewUtil.visible(imageView); } else { if (cancelPotentialWork(message, imageView)) { imageView.setBackgroundColor(0xff333333); imageView.setImageDrawable(null); final BitmapWorkerTask task = new BitmapWorkerTask(imageView, alternativeView, setSize); final AsyncDrawable asyncDrawable = new AsyncDrawable(ConversationsPlusApplication.getAppContext().getResources(), null, task); imageView.setImageDrawable(asyncDrawable); ViewUtil.gone(alternativeView); ViewUtil.visible(imageView); try { task.execute(message); } catch (final RejectedExecutionException ignored) { } } } } public static boolean cancelPotentialWork(Message message, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final Message oldMessage = bitmapWorkerTask.message; if (oldMessage == null || message != oldMessage) { bitmapWorkerTask.cancel(true); } else { return false; } } return true; } private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; } static class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<>( bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } private ImageUtil() { // Static helper class } }