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