diff options
Diffstat (limited to 'main/src/cgeo/geocaching/utils/ImageUtils.java')
-rw-r--r-- | main/src/cgeo/geocaching/utils/ImageUtils.java | 180 |
1 files changed, 156 insertions, 24 deletions
diff --git a/main/src/cgeo/geocaching/utils/ImageUtils.java b/main/src/cgeo/geocaching/utils/ImageUtils.java index 739ecc4..71d5e39 100644 --- a/main/src/cgeo/geocaching/utils/ImageUtils.java +++ b/main/src/cgeo/geocaching/utils/ImageUtils.java @@ -1,16 +1,20 @@ package cgeo.geocaching.utils; import cgeo.geocaching.CgeoApplication; +import cgeo.geocaching.Image; import cgeo.geocaching.R; import cgeo.geocaching.compatibility.Compatibility; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import rx.Observable; +import rx.Scheduler.Worker; import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action0; import rx.functions.Action1; import android.content.res.Resources; @@ -25,6 +29,8 @@ import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.net.Uri; import android.os.Environment; +import android.text.Html; +import android.text.Html.ImageGetter; import android.util.Base64; import android.util.Base64InputStream; import android.widget.TextView; @@ -36,8 +42,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Locale; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; public final class ImageUtils { private static final int[] ORIENTATIONS = new int[] { @@ -49,6 +61,10 @@ public final class ImageUtils { private static final int[] ROTATION = new int[] { 90, 180, 270 }; private static final int MAX_DISPLAY_IMAGE_XY = 800; + // Images whose URL contains one of those patterns will not be available on the Images tab + // for opening into an external application. + private final static String[] NO_EXTERNAL = new String[] { "geocheck.org" }; + private ImageUtils() { // Do not let this class be instantiated, this is a utility class. } @@ -61,7 +77,7 @@ public final class ImageUtils { * @return BitmapDrawable The scaled image */ public static BitmapDrawable scaleBitmapToFitDisplay(@NonNull final Bitmap image) { - Point displaySize = Compatibility.getDisplaySize(); + final Point displaySize = Compatibility.getDisplaySize(); final int maxWidth = displaySize.x - 25; final int maxHeight = displaySize.y - 25; return scaleBitmapTo(image, maxWidth, maxHeight); @@ -76,7 +92,7 @@ public final class ImageUtils { */ @Nullable public static Bitmap readAndScaleImageToFitDisplay(@NonNull final String filename) { - Point displaySize = Compatibility.getDisplaySize(); + final Point displaySize = Compatibility.getDisplaySize(); // Restrict image size to 800 x 800 to prevent OOM on tablets final int maxWidth = Math.min(displaySize.x - 25, MAX_DISPLAY_IMAGE_XY); final int maxHeight = Math.min(displaySize.y - 25, MAX_DISPLAY_IMAGE_XY); @@ -128,12 +144,12 @@ public final class ImageUtils { */ public static void storeBitmap(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String pathOfOutputImage) { try { - FileOutputStream out = new FileOutputStream(pathOfOutputImage); - BufferedOutputStream bos = new BufferedOutputStream(out); + final FileOutputStream out = new FileOutputStream(pathOfOutputImage); + final BufferedOutputStream bos = new BufferedOutputStream(out); bitmap.compress(format, quality, bos); bos.flush(); bos.close(); - } catch (IOException e) { + } catch (final IOException e) { Log.e("ImageHelper.storeBitmap", e); } } @@ -152,7 +168,7 @@ public final class ImageUtils { if (maxXY <= 0) { return filePath; } - Bitmap image = readDownsampledImage(filePath, maxXY, maxXY); + final Bitmap image = readDownsampledImage(filePath, maxXY, maxXY); if (image == null) { return null; } @@ -184,7 +200,7 @@ public final class ImageUtils { try { final ExifInterface exif = new ExifInterface(filePath); orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - } catch (IOException e) { + } catch (final IOException e) { Log.e("ImageUtils.readDownsampledImage", e); } final BitmapFactory.Options sizeOnlyOptions = new BitmapFactory.Options(); @@ -233,7 +249,7 @@ public final class ImageUtils { } // Create a media file name - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); return new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg"); } @@ -254,7 +270,7 @@ public final class ImageUtils { * @return <tt>true</tt> if the URL contains at least one of the patterns, <tt>false</tt> otherwise */ public static boolean containsPattern(final String url, final String[] patterns) { - for (String entry : patterns) { + for (final String entry : patterns) { if (StringUtils.containsIgnoreCase(url, entry)) { return true; } @@ -282,7 +298,7 @@ public final class ImageUtils { /** * Decode a base64-encoded string and save the result into a stream. - * + * * @param inString * the encoded string * @param out @@ -303,42 +319,158 @@ public final class ImageUtils { } /** + * Add images present in the HTML description to the existing collection. + * @param images a collection of images + * @param geocode the common title for images in the description + * @param htmlText the HTML description to be parsed, can be repeated + */ + public static void addImagesFromHtml(final Collection<Image> images, final String geocode, final String... htmlText) { + final Set<String> urls = new LinkedHashSet<>(); + for (final Image image : images) { + urls.add(image.getUrl()); + } + for (final String text: htmlText) { + Html.fromHtml(StringUtils.defaultString(text), new ImageGetter() { + @Override + public Drawable getDrawable(final String source) { + if (!urls.contains(source) && canBeOpenedExternally(source)) { + images.add(new Image(source, StringUtils.defaultString(geocode))); + urls.add(source); + } + return null; + } + }, null); + } + } + + /** * Container which can hold a drawable (initially an empty one) and get a newer version when it * becomes available. It also invalidates the view the container belongs to, so that it is * redrawn properly. + * <p/> + * When a new version of the drawable is available, it is put into a queue and, if needed (no other elements + * waiting in the queue), a refresh is launched on the UI thread. This refresh will empty the queue (including + * elements arrived in the meantime) and ensures that the view is uploaded only once all the queued requests have + * been handled. */ - @SuppressWarnings("deprecation") - public final static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> { + public static class ContainerDrawable extends BitmapDrawable implements Action1<Drawable> { + final private static Object lock = new Object(); // Used to lock the queue to determine if a refresh needs to be scheduled + final private static LinkedBlockingQueue<ImmutablePair<ContainerDrawable, Drawable>> REDRAW_QUEUE = new LinkedBlockingQueue<>(); + final private static Set<TextView> VIEWS = new HashSet<>(); // Modified only on the UI thread + final private static Worker UI_WORKER = AndroidSchedulers.mainThread().createWorker(); + final private static Action0 REDRAW_QUEUED_DRAWABLES = new Action0() { + @Override + public void call() { + redrawQueuedDrawables(); + } + }; + private Drawable drawable; - final private TextView view; + final protected TextView view; - public ContainerDrawable(@NonNull final TextView view) { + @SuppressWarnings("deprecation") + public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { this.view = view; drawable = null; setBounds(0, 0, 0, 0); - } - - public ContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { - this(view); - updateFrom(drawableObservable); + drawableObservable.subscribe(this); } @Override - public void draw(final Canvas canvas) { + public final void draw(final Canvas canvas) { if (drawable != null) { drawable.draw(canvas); } } @Override - public void call(final Drawable newDrawable) { + public final void call(final Drawable newDrawable) { + final boolean needsRedraw; + synchronized (lock) { + // Check for emptyness inside the call to match the behaviour in redrawQueuedDrawables(). + needsRedraw = REDRAW_QUEUE.isEmpty(); + REDRAW_QUEUE.add(ImmutablePair.of(this, newDrawable)); + } + if (needsRedraw) { + UI_WORKER.schedule(REDRAW_QUEUED_DRAWABLES); + } + } + + /** + * Update the container with the new drawable. Called on the UI thread. + * + * @param newDrawable the new drawable + * @return the view to update + */ + protected TextView updateDrawable(final Drawable newDrawable) { setBounds(0, 0, newDrawable.getIntrinsicWidth(), newDrawable.getIntrinsicHeight()); drawable = newDrawable; - view.setText(view.getText()); + return view; + } + + private static void redrawQueuedDrawables() { + if (!REDRAW_QUEUE.isEmpty()) { + // Add a small margin so that drawables arriving between the beginning of the allocation and the draining + // of the queue might be absorbed without reallocation. + final ArrayList<ImmutablePair<ContainerDrawable, Drawable>> toRedraw = new ArrayList<>(REDRAW_QUEUE.size() + 16); + synchronized (lock) { + // Empty the queue inside the lock to match the check done in call(). + REDRAW_QUEUE.drainTo(toRedraw); + } + for (final ImmutablePair<ContainerDrawable, Drawable> redrawable : toRedraw) { + VIEWS.add(redrawable.left.updateDrawable(redrawable.right)); + } + for (final TextView view : VIEWS) { + view.setText(view.getText()); + } + VIEWS.clear(); + } + } + + } + + /** + * Image that automatically scales to fit a line of text in the containing {@link TextView}. + */ + public final static class LineHeightContainerDrawable extends ContainerDrawable { + public LineHeightContainerDrawable(@NonNull final TextView view, final Observable<? extends Drawable> drawableObservable) { + super(view, drawableObservable); } - public void updateFrom(final Observable<? extends Drawable> drawableObservable) { - drawableObservable.observeOn(AndroidSchedulers.mainThread()).subscribe(this); + @Override + protected TextView updateDrawable(final Drawable newDrawable) { + super.updateDrawable(newDrawable); + setBounds(ImageUtils.scaleImageToLineHeight(newDrawable, view)); + return view; } } + + public static boolean canBeOpenedExternally(final String source) { + return !containsPattern(source, NO_EXTERNAL); + } + + public static Rect scaleImageToLineHeight(final Drawable drawable, final TextView view) { + final int lineHeight = (int) (view.getLineHeight() * 0.8); + final int width = drawable.getIntrinsicWidth() * lineHeight / drawable.getIntrinsicHeight(); + return new Rect(0, 0, width, lineHeight); + } + + public static Bitmap convertToBitmap(final Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + // handle solid colors, which have no width + int width = drawable.getIntrinsicWidth(); + width = width > 0 ? width : 1; + int height = drawable.getIntrinsicHeight(); + height = height > 0 ? height : 1; + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } } |