summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java334
1 files changed, 334 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
new file mode 100644
index 0000000000..d57147f363
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
@@ -0,0 +1,334 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import org.mozilla.geckoview.GeckoResult;
+
+/**
+ * Represents an Web API image resource as used in web app manifests and media session metadata.
+ *
+ * @see <a href="https://www.w3.org/TR/image-resource">Image Resource</a>
+ */
+@AnyThread
+public class ImageResource {
+ private static final String LOGTAG = "ImageResource";
+ private static final boolean DEBUG = false;
+
+ /** Represents the size of an image resource option. */
+ public static class Size {
+ /** The width in pixels. */
+ public final int width;
+
+ /** The height in pixels. */
+ public final int height;
+
+ /**
+ * Size contructor.
+ *
+ * @param width The width in pixels.
+ * @param height The height in pixels.
+ */
+ public Size(final int width, final int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+
+ /** The URI of the image resource. */
+ public final @NonNull String src;
+
+ /** The MIME type of the image resource. */
+ public final @Nullable String type;
+
+ /** A {@link Size} array of supported images sizes. */
+ public final @Nullable Size[] sizes;
+
+ /**
+ * ImageResource constructor.
+ *
+ * @param src The URI string of the image resource.
+ * @param type The MIME type of the image resource.
+ * @param sizes The supported images {@link Size} array.
+ */
+ public ImageResource(
+ final @NonNull String src, final @Nullable String type, final @Nullable Size[] sizes) {
+ this.src = src.toLowerCase(Locale.ROOT);
+ this.type = type != null ? type.toLowerCase(Locale.ROOT) : null;
+ this.sizes = sizes;
+ }
+
+ /**
+ * ImageResource constructor.
+ *
+ * @param src The URI string of the image resource.
+ * @param type The MIME type of the image resource.
+ * @param sizes The supported images sizes string.
+ * @see <a href="https://html.spec.whatwg.org/multipage/semantics.html#dom-link-sizes">Attribute
+ * spec for sizes</a>
+ */
+ public ImageResource(
+ final @NonNull String src, final @Nullable String type, final @Nullable String sizes) {
+ this(src, type, parseSizes(sizes));
+ }
+
+ private static @Nullable Size[] parseSizes(final @Nullable String sizesStr) {
+ if (sizesStr == null || sizesStr.isEmpty()) {
+ return null;
+ }
+
+ final String[] sizesStrs = sizesStr.toLowerCase(Locale.ROOT).split(" ");
+ final List<Size> sizes = new ArrayList<Size>();
+
+ for (final String sizeStr : sizesStrs) {
+ if (sizesStr.equals("any")) {
+ // 0-width size will always be favored.
+ sizes.add(new Size(0, 0));
+ continue;
+ }
+ final String[] widthHeight = sizeStr.split("x");
+ if (widthHeight.length != 2) {
+ // Not spec-compliant size.
+ continue;
+ }
+ try {
+ sizes.add(new Size(Integer.valueOf(widthHeight[0]), Integer.valueOf(widthHeight[1])));
+ } catch (final NumberFormatException e) {
+ Log.e(LOGTAG, "Invalid image resource size", e);
+ }
+ }
+ if (sizes.isEmpty()) {
+ return null;
+ }
+ return sizes.toArray(new Size[0]);
+ }
+
+ public static @NonNull ImageResource fromBundle(final GeckoBundle bundle) {
+ return new ImageResource(
+ bundle.getString("src"), bundle.getString("type"), bundle.getString("sizes"));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("ImageResource {");
+ builder
+ .append("src=")
+ .append(src)
+ .append("type=")
+ .append(type)
+ .append("sizes=")
+ .append(sizes)
+ .append("}");
+ return builder.toString();
+ }
+
+ /**
+ * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
+ * cache the result of this method keyed with this instance.
+ *
+ * @param size pixel size at which this image will be displayed at.
+ * @return A {@link GeckoResult} that resolves to the bitmap when ready.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> getBitmap(final int size) {
+ return ImageDecoder.instance().decode(src, size);
+ }
+
+ /**
+ * Represents a collection of {@link ImageResource} options. Image resources are often used in a
+ * collection to provide multiple image options for various sizes. This data structure can be used
+ * to retrieve the best image resource for any given target image size.
+ */
+ public static class Collection {
+ private static class SizeIndexPair {
+ public final int width;
+ public final int idx;
+
+ public SizeIndexPair(final int width, final int idx) {
+ this.width = width;
+ this.idx = idx;
+ }
+ }
+
+ // The individual image resources, usually each with a unique src.
+ private final List<ImageResource> mImages;
+
+ // A sorted size-index list. The list is sorted based on the supported
+ // sizes of the images in ascending order.
+ private final List<SizeIndexPair> mSizeIndex;
+
+ /* package */ Collection() {
+ mImages = new ArrayList<>();
+ mSizeIndex = new ArrayList<>();
+ }
+
+ /** Builder class for the construction of a {@link Collection}. */
+ public static class Builder {
+ final Collection mCollection;
+
+ public Builder() {
+ mCollection = new Collection();
+ }
+
+ /**
+ * Add an image resource to the collection.
+ *
+ * @param image The {@link ImageResource} to be added.
+ * @return This builder instance.
+ */
+ public @NonNull Builder add(final ImageResource image) {
+ final int index = mCollection.mImages.size();
+
+ if (image.sizes == null) {
+ // Null-sizes are handled the same as `any`.
+ mCollection.mSizeIndex.add(new SizeIndexPair(0, index));
+ } else {
+ for (final Size size : image.sizes) {
+ mCollection.mSizeIndex.add(new SizeIndexPair(size.width, index));
+ }
+ }
+ mCollection.mImages.add(image);
+ return this;
+ }
+
+ /**
+ * Finalize the collection.
+ *
+ * @return The final collection.
+ */
+ public @NonNull Collection build() {
+ Collections.sort(mCollection.mSizeIndex, (a, b) -> Integer.compare(a.width, b.width));
+ return mCollection;
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("ImageResource.Collection {");
+ builder.append("images=[");
+
+ for (final ImageResource image : mImages) {
+ builder.append(image).append(", ");
+ }
+ builder.append("]}");
+ return builder.toString();
+ }
+
+ /**
+ * Returns the best suited {@link ImageResource} for the given size. This is usually determined
+ * based on the minimal difference between the given size and one of the supported widths of an
+ * image resource.
+ *
+ * @param size The target size for the image in pixels.
+ * @return The best {@link ImageResource} for the given size from this collection.
+ */
+ public @Nullable ImageResource getBest(final int size) {
+ if (mSizeIndex.isEmpty()) {
+ return null;
+ }
+ int bestMatchIdx = mSizeIndex.get(0).idx;
+ int lastDiff = size;
+ for (final SizeIndexPair sizeIndex : mSizeIndex) {
+ final int diff = Math.abs(sizeIndex.width - size);
+ if (lastDiff <= diff) {
+ // With increasing widths, the difference can only grow now.
+ // 0-width means "any", so we're finished at the first
+ // entry.
+ break;
+ }
+ lastDiff = diff;
+ bestMatchIdx = sizeIndex.idx;
+ }
+ return mImages.get(bestMatchIdx);
+ }
+
+ /**
+ * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
+ * cache the result of this method keyed with this instance.
+ *
+ * @param size pixel size at which this image will be displayed at.
+ * @return A {@link GeckoResult} that resolves to the bitmap when ready.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> getBitmap(final int size) {
+ final ImageResource image = getBest(size);
+ if (image == null) {
+ return GeckoResult.fromValue(null);
+ }
+ return image.getBitmap(size);
+ }
+
+ public static Collection fromSizeSrcBundle(final GeckoBundle bundle) {
+ final Builder builder = new Builder();
+
+ for (final String key : bundle.keys()) {
+ final Integer intKey = Integer.valueOf(key);
+ if (intKey == null) {
+ Log.e(LOGTAG, "Non-integer image key: " + intKey);
+
+ if (DEBUG) {
+ throw new RuntimeException("Non-integer image key: " + key);
+ }
+ continue;
+ }
+
+ final String src = getImageValue(bundle.get(key));
+ if (src != null) {
+ // Given the bundle structure, we don't have insight on
+ // individual image resources so we have to create an
+ // instance for each size entry.
+ final ImageResource image =
+ new ImageResource(src, null, new Size[] {new Size(intKey, intKey)});
+ builder.add(image);
+ }
+ }
+ return builder.build();
+ }
+
+ private static String getImageValue(final Object value) {
+ // The image value can either be an object containing images for
+ // each theme...
+ if (value instanceof GeckoBundle) {
+ // We don't support theme_images yet, so let's just return the
+ // default value.
+ final GeckoBundle themeImages = (GeckoBundle) value;
+ final Object defaultImages = themeImages.get("default");
+
+ if (!(defaultImages instanceof String)) {
+ if (DEBUG) {
+ throw new RuntimeException("Unexpected themed_icon value.");
+ }
+ Log.e(LOGTAG, "Unexpected themed_icon value.");
+ return null;
+ }
+
+ return (String) defaultImages;
+ }
+
+ // ... or just a URL.
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ // We never expect it to be something else, so let's error out here.
+ if (DEBUG) {
+ throw new RuntimeException("Unexpected image value: " + value);
+ }
+
+ Log.e(LOGTAG, "Unexpected image value.");
+ return null;
+ }
+ }
+}