/*
* Copyright 2018 The Android Open Source Project
*
* 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 androidx.car.cluster.navigation;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.annotation.SuppressLint;
import android.net.Uri;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
import java.util.Objects;
/**
* Reference to an image. This class encapsulates a 'content://' style URI plus metadata that allows
* consumers to know the image they will receive and how to handle it.
*
* <ul>
* <li><b>Sizing:</b> Producers will always provide an image "original" size which defines the image
* aspect ratio. When requesting these images, consumers must always specify a desired size (width
* and height) based on UI available space and the provided aspect ration. Producers can use this
* "requested" size to select the best version of the requested image, and producers can optionally
* resize the image to exactly match the "requested" size provided, but consumers should not assume
* that the received image will match such size. Instead, consumers should always assume that the
* image will require additional scaling.
* <li><b>Content:</b> Producers should avoid including margins around the image content.
* <li><b>Format:</b> Content URI must reference a file with MIME type 'image/png', 'image/jpeg'
* or 'image/bmp' (vector images are not supported).
* <li><b>Color:</b> Images can be either "tintable" or not. A "tintable" image is such that all its
* content is defined in its alpha channel, while its color (all other channels) can be altered
* without losing information (e.g.: icons). A non "tintable" images contains information in all its
* channels (e.g.: photos).
* <li><b>Caching:</b> Given the same image reference and the same requested size, producers must
* return the exact same image. This means that it should be safe for the consumer to cache an image
* once downloaded and use this image reference plus requested size as key, for as long as they
* need. If a producer needs to provide a different version of a certain image, they must provide a
* different image reference (e.g. producers can opt to include version information as part of the
* content URI).
* </ul>
*/
@VersionedParcelize
public class ImageReference implements VersionedParcelable {
private static final String SCHEME = "content://";
private static final String WIDTH_HINT_PARAMETER = "w";
private static final String HEIGHT_HINT_PARAMETER = "h";
@ParcelField(1)
String mContentUri;
@ParcelField(2)
int mOriginalWidth;
@ParcelField(3)
int mOriginalHeight;
@ParcelField(4)
boolean mIsTintable;
/**
* Used by {@link VersionedParcelable}
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
ImageReference() {
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
ImageReference(@NonNull String contentUri,
@IntRange(from = 1, to = Integer.MAX_VALUE) int originalWidth,
@IntRange(from = 1, to = Integer.MAX_VALUE) int originalHeight,
boolean isTintable) {
mContentUri = Preconditions.checkNotNull(contentUri);
mOriginalWidth = Preconditions.checkArgumentInRange(originalWidth, 1,
Integer.MAX_VALUE, "originalWidth");
mOriginalHeight = Preconditions.checkArgumentInRange(originalHeight, 1,
Integer.MAX_VALUE, "originalHeight");
mIsTintable = isTintable;
}
/**
* Builder for creating an {@link ImageReference}.
*/
public static final class Builder {
private String mContentUri;
private int mOriginalWidth;
private int mOriginalHeight;
private boolean mIsTintable;
/**
* Sets a 'content://' style URI
*
* @return this object for chaining
* @throws NullPointerException if the provided {@code contentUri} is null
* @throws IllegalArgumentException if the provided {@code contentUri} doesn't start with
* 'content://'.
*/
@NonNull
public Builder setContentUri(@NonNull String contentUri) {
Preconditions.checkNotNull(contentUri);
Preconditions.checkArgument(contentUri.startsWith(SCHEME));
mContentUri = contentUri;
return this;
}
/**
* Sets the aspect ratio of this image, expressed as with and height sizes. Both dimensions
* must be greater than 0.
*
* @return this object for chaining
* @throws IllegalArgumentException if any of the dimensions is not positive.
*/
@NonNull
public Builder setOriginalSize(@IntRange(from = 1, to = Integer.MAX_VALUE) int width,
@IntRange(from = 1, to = Integer.MAX_VALUE) int height) {
Preconditions.checkArgumentInRange(width, 1, Integer.MAX_VALUE, "width");
Preconditions.checkArgumentInRange(height, 1, Integer.MAX_VALUE, "height");
mOriginalWidth = width;
mOriginalHeight = height;
return this;
}
/**
* Sets whether this image is "tintable" or not. An image is "tintable" when all its
* content is defined in its alpha-channel, designed to be colorized (e.g. using
* {@link android.graphics.PorterDuff.Mode#SRC_ATOP} image composition).
* If this method is not used, images will be non "tintable" by default.
*
* @return this object for chaining
*/
@NonNull
public Builder setIsTintable(boolean isTintable) {
mIsTintable = isTintable;
return this;
}
/**
* Returns a {@link ImageReference} built with the provided information. Calling
* {@link ImageReference.Builder#setContentUri(String)} and
* {@link ImageReference.Builder#setOriginalSize(int, int)} before calling this method is
* mandatory.
*
* @return an {@link ImageReference} instance
* @throws NullPointerException if content URI is not provided.
* @throws IllegalArgumentException if original size is not set.
*/
@NonNull
public ImageReference build() {
return new ImageReference(mContentUri, mOriginalWidth, mOriginalHeight, mIsTintable);
}
}
/**
* Returns a 'content://' style URI that can be used to retrieve the actual image, or an empty
* string if the URI provided by the producer doesn't comply with the format requirements. If
* this URI is used as-is, the size of the resulting image is undefined.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@NonNull
public String getRawContentUri() {
String value = Common.nonNullOrEmpty(mContentUri);
return value.startsWith(SCHEME) ? value : "";
}
/**
* Returns a fully formed {@link Uri} that can be used to retrieve the actual image, including
* size constraints, or null if this image reference is not properly formed.
* <p>
* Producers can optionally use these size constraints to provide an optimized version of the
* image, but the resulting image might still not match the requested size.
* <p>
* Consumers must confirm the size of the received image and scale it proportionally (
* maintaining the aspect ratio of the received image) if it doesn't match the desired
* dimensions.
*
* @param width desired maximum width (must be greater than 0)
* @param height desired maximum height (must be greater than 0)
* @return fully formed {@link Uri}, or null if this image reference can not be used.
*/
@Nullable
public Uri getContentUri(@IntRange(from = 1, to = Integer.MAX_VALUE) int width,
@IntRange(from = 1, to = Integer.MAX_VALUE) int height) {
Preconditions.checkArgumentInRange(width, 1, Integer.MAX_VALUE, "width");
Preconditions.checkArgumentInRange(height, 1, Integer.MAX_VALUE, "height");
String contentUri = getRawContentUri();
if (contentUri.isEmpty()) {
// We have an invalid content URI.
return null;
}
return Uri.parse(contentUri).buildUpon()
.appendQueryParameter(WIDTH_HINT_PARAMETER, String.valueOf(width))
.appendQueryParameter(HEIGHT_HINT_PARAMETER, String.valueOf(height))
.build();
}
/**
* Returns the image width, which should only be used to determine the image aspect ratio.
*/
public int getOriginalWidth() {
return mOriginalWidth;
}
/**
* Returns the image height, which should only be used to determine the image aspect ratio.
*/
public int getOriginalHeight() {
return mOriginalHeight;
}
/**
* Returns whether this image is "tintable" or not. An image is "tintable" when all its
* content is defined in its alpha-channel, designed to be colorized (e.g. using
* {@link android.graphics.PorterDuff.Mode#SRC_ATOP} image composition).
*/
public boolean isTintable() {
return mIsTintable;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ImageReference image = (ImageReference) o;
return Objects.equals(getRawContentUri(), image.getRawContentUri())
&& getOriginalWidth() == image.getOriginalWidth()
&& getOriginalHeight() == image.getOriginalHeight()
&& isTintable() == image.isTintable();
}
@Override
public int hashCode() {
return Objects.hash(getRawContentUri(), getOriginalWidth(), getOriginalHeight(),
isTintable());
}
// DefaultLocale suppressed as this method is only offered for debugging purposes.
@SuppressLint("DefaultLocale")
@Override
public String toString() {
return String.format("{contentUri: '%s', originalWidth: %d, originalHeight: %d, "
+ "isTintable: %s}",
mContentUri, mOriginalWidth, mOriginalHeight, mIsTintable);
}
}