blob: d45a8dce79ded55f0480a36c1de93cadc85f781a [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;
import android.content.ContentResolver;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
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);
}
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));
}
/**
* 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;
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);
}
}
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);
}
}
}