2 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.cyanogenmod.eleven.utils;
18 import android.content.Context;
19 import android.graphics.Bitmap;
20 import android.graphics.BitmapFactory;
21 import android.widget.ImageView;
23 import com.cyanogenmod.eleven.cache.ImageCache;
24 import com.cyanogenmod.eleven.cache.ImageWorker;
25 import com.cyanogenmod.eleven.lastfm.ImageSize;
26 import com.cyanogenmod.eleven.lastfm.MusicEntry;
28 import java.io.BufferedInputStream;
29 import java.io.BufferedOutputStream;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.net.HttpURLConnection;
36 import java.util.concurrent.atomic.AtomicInteger;
38 public class ImageUtils {
39 private static final String DEFAULT_HTTP_CACHE_DIR = "http"; //$NON-NLS-1$
41 public static final int IO_BUFFER_SIZE_BYTES = 1024;
43 private static final int DEFAULT_MAX_IMAGE_HEIGHT = 1024;
45 private static final int DEFAULT_MAX_IMAGE_WIDTH = 1024;
47 private static AtomicInteger sInteger = new AtomicInteger(0);
50 * Gets the image url based on the imageType
51 * @param artistName The artist name param used in the Last.fm API.
52 * @param albumName The album name param used in the Last.fm API.
53 * @param imageType The type of image URL to fetch for.
54 * @return The image URL for an artist image or album image.
56 public static String processImageUrl(final Context context, final String artistName,
57 final String albumName, final ImageWorker.ImageType imageType) {
60 // Disable last.fm calls - TODO: Find an alternative artwork provider that has
61 // the proper license rights for artwork
62 /*if (!TextUtils.isEmpty(artistName)) {
63 if (PreferenceUtils.getInstance(context).downloadMissingArtistImages()) {
64 final Artist artist = Artist.getInfo(context, artistName);
66 return getBestImage(artist);
72 // Disable last.fm calls - TODO: Find an alternative artwork provider that has
73 // the proper license rights for artwork
74 /*if (!TextUtils.isEmpty(artistName) && !TextUtils.isEmpty(albumName)) {
75 if (PreferenceUtils.getInstance(context).downloadMissingArtwork()) {
76 final Artist correction = Artist.getCorrection(context, artistName);
77 if (correction != null) {
78 final Album album = Album.getInfo(context, correction.getName(),
81 return getBestImage(album);
94 * Downloads the bitmap from the url and returns it after some processing
96 * @param key The key to identify which image to process, as provided by
97 * {@link ImageWorker#loadImage(mKey, android.widget.ImageView)}
98 * @return The processed {@link Bitmap}.
100 public static Bitmap processBitmap(final Context context, final String url) {
104 final File file = downloadBitmapToFile(context, url, DEFAULT_HTTP_CACHE_DIR);
106 // Return a sampled down version
107 final Bitmap bitmap = decodeSampledBitmapFromFile(file.toString());
109 if (bitmap != null) {
117 * Decode and sample down a {@link Bitmap} from a file to the requested
120 * @param filename The full path of the file to decode
121 * @param reqWidth The requested width of the resulting bitmap
122 * @param reqHeight The requested height of the resulting bitmap
123 * @return A {@link Bitmap} sampled down from the original with the same
124 * aspect ratio and dimensions that are equal to or greater than the
125 * requested width and height
127 public static Bitmap decodeSampledBitmapFromFile(final String filename) {
129 // First decode with inJustDecodeBounds=true to check dimensions
130 final BitmapFactory.Options options = new BitmapFactory.Options();
131 options.inJustDecodeBounds = true;
132 BitmapFactory.decodeFile(filename, options);
134 // Calculate inSampleSize
135 options.inSampleSize = calculateInSampleSize(options, DEFAULT_MAX_IMAGE_WIDTH,
136 DEFAULT_MAX_IMAGE_HEIGHT);
138 // Decode bitmap with inSampleSize set
139 options.inJustDecodeBounds = false;
140 return BitmapFactory.decodeFile(filename, options);
144 * Calculate an inSampleSize for use in a
145 * {@link android.graphics.BitmapFactory.Options} object when decoding
146 * bitmaps using the decode* methods from {@link BitmapFactory}. This
147 * implementation calculates the closest inSampleSize that will result in
148 * the final decoded bitmap having a width and height equal to or larger
149 * than the requested width and height. This implementation does not ensure
150 * a power of 2 is returned for inSampleSize which can be faster when
151 * decoding but results in a larger bitmap which isn't as useful for caching
154 * @param options An options object with out* params already populated (run
155 * through a decode* method with inJustDecodeBounds==true
156 * @param reqWidth The requested width of the resulting bitmap
157 * @param reqHeight The requested height of the resulting bitmap
158 * @return The value to be used for inSampleSize
160 public static final int calculateInSampleSize(final BitmapFactory.Options options,
161 final int reqWidth, final int reqHeight) {
162 /* Raw height and width of image */
163 final int height = options.outHeight;
164 final int width = options.outWidth;
165 int inSampleSize = 1;
167 if (height > reqHeight || width > reqWidth) {
168 if (width > height) {
169 inSampleSize = Math.round((float)height / (float)reqHeight);
171 inSampleSize = Math.round((float)width / (float)reqWidth);
174 // This offers some additional logic in case the image has a strange
175 // aspect ratio. For example, a panorama may have a much larger
176 // width than height. In these cases the total pixels might still
177 // end up being too large to fit comfortably in memory, so we should
178 // be more aggressive with sample down the image (=larger
181 final float totalPixels = width * height;
183 /* More than 2x the requested pixels we'll sample down further */
184 final float totalReqPixelsCap = reqWidth * reqHeight * 2;
186 while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
193 private static String getBestImage(MusicEntry e) {
194 final ImageSize[] QUALITY = {ImageSize.EXTRALARGE, ImageSize.LARGE, ImageSize.MEDIUM,
195 ImageSize.SMALL, ImageSize.UNKNOWN};
196 for(ImageSize q : QUALITY) {
197 String url = e.getImageURL(q);
206 * Download a {@link Bitmap} from a URL, write it to a disk and return the
207 * File pointer. This implementation uses a simple disk cache.
209 * @param context The context to use
210 * @param urlString The URL to fetch
211 * @return A {@link File} pointing to the fetched bitmap
213 public static final File downloadBitmapToFile(final Context context, final String urlString,
214 final String uniqueName) {
215 final File cacheDir = ImageCache.getDiskCacheDir(context, uniqueName);
217 if (!cacheDir.exists()) {
221 HttpURLConnection urlConnection = null;
222 BufferedOutputStream out = null;
225 // increment the number to not collisions on the temp file name. A collision can
226 // potentially cause up to 50s on the first creation of the temp file but not on
227 // subsequent ones for some reason.
228 int number = sInteger.getAndIncrement() % 10;
229 final File tempFile = File.createTempFile("bitmap" + number, null, cacheDir); //$NON-NLS-1$
231 final URL url = new URL(urlString);
232 urlConnection = (HttpURLConnection)url.openConnection();
233 if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
236 int contentLength = urlConnection.getContentLength();
237 final InputStream in = new BufferedInputStream(urlConnection.getInputStream(),
238 IO_BUFFER_SIZE_BYTES);
239 out = new BufferedOutputStream(new FileOutputStream(tempFile), IO_BUFFER_SIZE_BYTES);
241 final byte[] buffer = new byte[IO_BUFFER_SIZE_BYTES];
243 while ((numBytes = in.read(buffer)) != -1) {
244 out.write(buffer, 0, numBytes);
245 contentLength -= numBytes;
248 // valid values for contentLength are either -ve (meaning it wasn't set) or 0
249 // if it is > 0 that means we got a value but didn't fully download the content
250 if (contentLength > 0) {
255 } catch (final IOException ignored) {
257 if (urlConnection != null) {
258 urlConnection.disconnect();
263 } catch (final IOException ignored) {
271 * Scale the bitmap to an image view. The bitmap will fill the image view bounds. The bitmap will be scaled
272 * while maintaining the aspect ratio and cropped if it exceeds the image-view bounds.
274 public static Bitmap scaleBitmapForImageView(Bitmap src, ImageView imageView) {
275 if (src == null || imageView == null) {
278 // get bitmap properties
279 int srcHeight = src.getHeight();
280 int srcWidth = src.getWidth();
282 // get image view bounds
283 int viewHeight = imageView.getHeight();
284 int viewWidth = imageView.getWidth();
286 int deltaWidth = viewWidth - srcWidth;
287 int deltaHeight = viewHeight - srcHeight;
289 if (deltaWidth <= 0 && deltaWidth <= 0) // nothing to do if src bitmap is bigger than image-view
292 // scale bitmap along the dimension that is lacking the greatest
293 float scale = Math.max( ((float)viewWidth) / srcWidth, ((float)viewHeight) / srcHeight);
295 // calculate the new bitmap dimensions
296 int dstHeight = (int) Math.ceil(srcHeight * scale);
297 int dstWidth = (int) Math.ceil(srcWidth * scale);
298 Bitmap scaledBitmap = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, false);
300 return Bitmap.createBitmap(scaledBitmap, 0, 0, viewWidth, viewHeight);