blob: 31c9552fda5ddf3597ec7489b7f56c6d735b4f09 [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.wallpaper.asset;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Log;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Represents an asset located via an Android content URI.
*/
public final class ContentUriAsset extends StreamableAsset {
private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
private static final String TAG = "ContentUriAsset";
private static final String JPEG_MIME_TYPE = "image/jpeg";
private static final String PNG_MIME_TYPE = "image/png";
private final Context mContext;
private final Uri mUri;
private final RequestOptions mRequestOptions;
private ExifInterfaceCompat mExifCompat;
private int mExifOrientation;
/**
* @param context The application's context.
* @param uri Content URI locating the asset.
* @param requestOptions {@link RequestOptions} to be applied when loading the asset.
* @param uncached If true, {@link #loadDrawable(Context, ImageView, int)} and
* {@link #loadDrawableWithTransition(Context, ImageView, int, DrawableLoadedListener, int)}
* will not cache data, and fetch it each time.
*/
public ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions,
boolean uncached) {
mExifOrientation = ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN;
mContext = context.getApplicationContext();
mUri = uri;
if (uncached) {
mRequestOptions = requestOptions.apply(RequestOptions
.diskCacheStrategyOf(DiskCacheStrategy.NONE)
.skipMemoryCache(true));
} else {
mRequestOptions = requestOptions;
}
}
/**
* @param context The application's context.
* @param uri Content URI locating the asset.
* @param requestOptions {@link RequestOptions} to be applied when loading the asset.
*/
public ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions) {
this(context, uri, requestOptions, /* uncached */ false);
}
/**
* @param context The application's context.
* @param uri Content URI locating the asset.
* @param uncached If true, {@link #loadDrawable(Context, ImageView, int)} and
* {@link #loadDrawableWithTransition(Context, ImageView, int, DrawableLoadedListener, int)}
* will not cache data, and fetch it each time.
*/
public ContentUriAsset(Context context, Uri uri, boolean uncached) {
this(context, uri, RequestOptions.centerCropTransform(), uncached);
}
/**
* @param context The application's context.
* @param uri Content URI locating the asset.
*/
public ContentUriAsset(Context context, Uri uri) {
this(context, uri, /* uncached */ false);
}
@Override
public void decodeBitmapRegion(final Rect rect, int targetWidth, int targetHeight,
boolean shouldAdjustForRtl, final BitmapReceiver receiver) {
// BitmapRegionDecoder only supports images encoded in either JPEG or PNG, so if the content
// URI asset is encoded with another format (for example, GIF), then fall back to cropping a
// bitmap region from the full-sized bitmap.
if (isJpeg() || isPng()) {
super.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver);
return;
}
decodeRawDimensions(null /* activity */, new DimensionsReceiver() {
@Override
public void onDimensionsDecoded(@Nullable Point dimensions) {
if (dimensions == null) {
Log.e(TAG, "There was an error decoding the asset's raw dimensions with " +
"content URI: " + mUri);
receiver.onBitmapDecoded(null);
return;
}
decodeBitmap(dimensions.x, dimensions.y, new BitmapReceiver() {
@Override
public void onBitmapDecoded(@Nullable Bitmap fullBitmap) {
if (fullBitmap == null) {
Log.e(TAG, "There was an error decoding the asset's full bitmap with " +
"content URI: " + mUri);
decodeBitmapCompleted(receiver, null);
return;
}
sExecutorService.execute(()-> {
decodeBitmapCompleted(receiver, Bitmap.createBitmap(
fullBitmap, rect.left, rect.top, rect.width(), rect.height()));
});
}
});
}
});
}
/**
* Returns whether this image is encoded in the JPEG file format.
*/
public boolean isJpeg() {
String mimeType = mContext.getContentResolver().getType(mUri);
return mimeType != null && mimeType.equals(JPEG_MIME_TYPE);
}
/**
* Returns whether this image is encoded in the PNG file format.
*/
public boolean isPng() {
String mimeType = mContext.getContentResolver().getType(mUri);
return mimeType != null && mimeType.equals(PNG_MIME_TYPE);
}
/**
* Reads the EXIF tag on the asset. Automatically trims leading and trailing whitespace.
*
* @return String attribute value for this tag ID, or null if ExifInterface failed to read tags
* for this asset, if this tag was not found in the image's metadata, or if this tag was
* empty (i.e., only whitespace).
*/
public String readExifTag(String tagId) {
ensureExifInterface();
if (mExifCompat == null) {
Log.w(TAG, "Unable to read EXIF tags for content URI asset");
return null;
}
String attribute = mExifCompat.getAttribute(tagId);
if (attribute == null || attribute.trim().isEmpty()) {
return null;
}
return attribute.trim();
}
private void ensureExifInterface() {
if (mExifCompat == null) {
try (InputStream inputStream = openInputStream()) {
if (inputStream != null) {
mExifCompat = new ExifInterfaceCompat(inputStream);
}
} catch (IOException e) {
Log.w(TAG, "Couldn't read stream for " + mUri, e);
}
}
}
@Override
protected InputStream openInputStream() {
try {
return mContext.getContentResolver().openInputStream(mUri);
} catch (FileNotFoundException e) {
Log.w(TAG, "Image file not found", e);
return null;
}
}
@Override
protected int getExifOrientation() {
if (mExifOrientation != ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN) {
return mExifOrientation;
}
mExifOrientation = readExifOrientation();
return mExifOrientation;
}
/**
* Returns the EXIF rotation for the content URI asset. This method should only be called off
* the main UI thread.
*/
private int readExifOrientation() {
ensureExifInterface();
if (mExifCompat == null) {
Log.w(TAG, "Unable to read EXIF rotation for content URI asset with content URI: "
+ mUri);
return ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL;
}
return mExifCompat.getAttributeInt(ExifInterfaceCompat.TAG_ORIENTATION,
ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL);
}
@Override
public void loadDrawable(Context context, ImageView imageView,
int placeholderColor) {
Glide.with(context)
.asDrawable()
.load(mUri)
.apply(mRequestOptions
.placeholder(new ColorDrawable(placeholderColor)))
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView);
}
@Override
public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
BitmapTransformation transformation) {
MultiTransformation<Bitmap> multiTransformation =
new MultiTransformation<>(new FitCenter(), transformation);
Glide.with(activity)
.asDrawable()
.load(mUri)
.apply(RequestOptions.bitmapTransform(multiTransformation)
.placeholder(new ColorDrawable(placeholderColor)))
.into(imageView);
}
@Override
public void loadDrawableWithTransition(Context context, ImageView imageView,
int transitionDurationMillis, @Nullable DrawableLoadedListener drawableLoadedListener,
int placeholderColor) {
Glide.with(context)
.asDrawable()
.load(mUri)
.apply(mRequestOptions
.placeholder(new ColorDrawable(placeholderColor)))
.transition(DrawableTransitionOptions.withCrossFade(transitionDurationMillis))
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource,
boolean isFirstResource) {
if (drawableLoadedListener != null) {
drawableLoadedListener.onDrawableLoaded();
}
return false;
}
})
.into(imageView);
}
public Uri getUri() {
return mUri;
}
}