| /* |
| * 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; |
| } |
| } |