blob: 3952450315b0aabd02e1739ee1d346867cde39f5 [file] [log] [blame]
/*
* Copyright (C) 2015 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.tv.util.images;
import android.content.ContentResolver;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.TrafficStats;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.util.NetworkTrafficTags;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
public final class BitmapUtils {
private static final String TAG = "BitmapUtils";
private static final boolean DEBUG = false;
// The value of 64K, for MARK_READ_LIMIT, is chosen to be eight times the default buffer size
// of BufferedInputStream (8K) allowing it to double its buffers three times. Also it is a
// fairly reasonable value, not using too much memory and being large enough for most cases.
private static final int MARK_READ_LIMIT = 64 * 1024; // 64K
private static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec
private static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec
private BitmapUtils() {
/* cannot be instantiated */
}
public static Bitmap scaleBitmap(Bitmap bm, int maxWidth, int maxHeight) {
Rect rect = calculateNewSize(bm, maxWidth, maxHeight);
return Bitmap.createScaledBitmap(bm, rect.right, rect.bottom, false);
}
public static Bitmap getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight) {
Bitmap scaledBitmap = scaleBitmap(bm, maxWidth, maxHeight);
return scaledBitmap.isMutable()
? scaledBitmap
: scaledBitmap.copy(Bitmap.Config.ARGB_8888, true);
}
private static Rect calculateNewSize(Bitmap bm, int maxWidth, int maxHeight) {
final double ratio = maxHeight / (double) maxWidth;
final double bmRatio = bm.getHeight() / (double) bm.getWidth();
Rect rect = new Rect();
if (ratio > bmRatio) {
rect.right = maxWidth;
rect.bottom = Math.round((float) bm.getHeight() * maxWidth / bm.getWidth());
} else {
rect.right = Math.round((float) bm.getWidth() * maxHeight / bm.getHeight());
rect.bottom = maxHeight;
}
return rect;
}
public static ScaledBitmapInfo createScaledBitmapInfo(
String id, Bitmap bm, int maxWidth, int maxHeight) {
return new ScaledBitmapInfo(
id,
scaleBitmap(bm, maxWidth, maxHeight),
calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight));
}
@Nullable
public static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable == null) {
return null;
}
Bitmap bm = Bitmap.createBitmap(
drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bm);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bm;
}
/** Decode large sized bitmap into requested size. */
public static ScaledBitmapInfo decodeSampledBitmapFromUriString(
Context context, String uriString, int reqWidth, int reqHeight) {
if (TextUtils.isEmpty(uriString)) {
return null;
}
Uri uri = Uri.parse(uriString).normalizeScheme();
boolean isResourceUri = isContentResolverUri(uri);
URLConnection urlConnection = null;
InputStream inputStream = null;
final int oldTag = TrafficStats.getThreadStatsTag();
TrafficStats.setThreadStatsTag(NetworkTrafficTags.LOGO_FETCHER);
try {
if (isResourceUri) {
inputStream = context.getContentResolver().openInputStream(uri);
} else {
// If the URLConnection is HttpURLConnection, disconnect() should be called
// explicitly.
urlConnection = getUrlConnection(uriString);
inputStream = urlConnection.getInputStream();
}
inputStream = new BufferedInputStream(inputStream);
inputStream.mark(MARK_READ_LIMIT);
// Check the bitmap dimensions.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
// Rewind the stream in order to restart bitmap decoding.
try {
inputStream.reset();
} catch (IOException e) {
if (DEBUG) Log.i(TAG, "Failed to rewind stream: " + uriString, e);
// Failed to rewind the stream, try to reopen it.
close(inputStream, urlConnection);
if (isResourceUri) {
inputStream = context.getContentResolver().openInputStream(uri);
} else {
urlConnection = getUrlConnection(uriString);
inputStream = urlConnection.getInputStream();
}
}
// Decode the bitmap possibly resizing it.
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
if (bitmap == null) {
return null;
}
return new ScaledBitmapInfo(uriString, bitmap, options.inSampleSize);
} catch (IOException e) {
if (DEBUG) {
// It can happens in normal cases like when a channel doesn't have any logo.
Log.w(TAG, "Failed to open stream: " + uriString, e);
}
return null;
} catch (SQLiteException e) {
Log.e(TAG, "Failed to open stream: " + uriString, e);
return null;
} finally {
close(inputStream, urlConnection);
TrafficStats.setThreadStatsTag(oldTag);
}
}
private static URLConnection getUrlConnection(String uriString) throws IOException {
URLConnection urlConnection = new URL(uriString).openConnection();
urlConnection.setConnectTimeout(CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
urlConnection.setReadTimeout(READ_TIMEOUT_MS_FOR_URLCONNECTION);
return urlConnection;
}
private static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
}
private static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
// Calculates the largest inSampleSize that, is a power of two and, keeps either width or
// height larger or equal to the requested width and height.
int ratio = Math.max(width / reqWidth, height / reqHeight);
return Math.max(1, Integer.highestOneBit(ratio));
}
private static boolean isContentResolverUri(Uri uri) {
String scheme = uri.getScheme();
return ContentResolver.SCHEME_CONTENT.equals(scheme)
|| ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
|| ContentResolver.SCHEME_FILE.equals(scheme);
}
private static void close(Closeable closeable, URLConnection urlConnection) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// Log and continue.
Log.w(TAG, "Error closing " + closeable, e);
}
}
if (urlConnection instanceof HttpURLConnection) {
((HttpURLConnection) urlConnection).disconnect();
}
}
/** A wrapper class which contains the loaded bitmap and the scaling information. */
public static class ScaledBitmapInfo {
/** The id of bitmap, usually this is the URI of the original. */
@NonNull public final String id;
/** The loaded bitmap object. */
@NonNull public final Bitmap bitmap;
/**
* The scaling factor to the original bitmap. It should be an positive integer.
*
* @see android.graphics.BitmapFactory.Options#inSampleSize
*/
public final int inSampleSize;
/**
* A constructor.
*
* @param bitmap The loaded bitmap object.
* @param inSampleSize The sampling size. See {@link
* android.graphics.BitmapFactory.Options#inSampleSize}
*/
public ScaledBitmapInfo(@NonNull String id, @NonNull Bitmap bitmap, int inSampleSize) {
this.id = id;
this.bitmap = bitmap;
this.inSampleSize = inSampleSize;
}
/**
* Checks if the bitmap needs to be reloaded. The scaling is performed by power 2. The
* bitmap can be reloaded only if the required width or height is greater then or equal to
* the existing bitmap. If the full sized bitmap is already loaded, returns {@code false}.
*
* @see android.graphics.BitmapFactory.Options#inSampleSize
*/
public boolean needToReload(int reqWidth, int reqHeight) {
if (inSampleSize <= 1) {
if (DEBUG) Log.d(TAG, "Reload not required " + this + " already full size.");
return false;
}
Rect size = calculateNewSize(this.bitmap, reqWidth, reqHeight);
boolean reload =
(size.right >= bitmap.getWidth() * 2 || size.bottom >= bitmap.getHeight() * 2);
if (DEBUG) {
Log.d(
TAG,
"needToReload("
+ reqWidth
+ ", "
+ reqHeight
+ ")="
+ reload
+ " because the new size would be "
+ size
+ " for "
+ this);
}
return reload;
}
/** Returns {@code true} if a request the size of {@code other} would need a reload. */
public boolean needToReload(ScaledBitmapInfo other) {
return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight());
}
@Override
public String toString() {
return "ScaledBitmapInfo["
+ id
+ "](in="
+ inSampleSize
+ ", w="
+ bitmap.getWidth()
+ ", h="
+ bitmap.getHeight()
+ ")";
}
}
/**
* Applies a color filter to the {@code drawable}. The color filter is made with the given
* {@code color} and {@link android.graphics.PorterDuff.Mode#SRC_ATOP}.
*
* @see Drawable#setColorFilter
*/
public static void setColorFilterToDrawable(int color, Drawable drawable) {
if (drawable != null) {
drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
}
}