michael@0: /* michael@0: * Copyright (C) 2013 Square, Inc. michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: package com.squareup.picasso; michael@0: michael@0: import android.annotation.TargetApi; michael@0: import android.app.ActivityManager; michael@0: import android.content.ContentResolver; michael@0: import android.content.Context; michael@0: import android.content.pm.PackageManager; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Bitmap; michael@0: import android.os.Looper; michael@0: import android.os.Process; michael@0: import android.os.StatFs; michael@0: import android.provider.Settings; michael@0: import java.io.ByteArrayOutputStream; michael@0: import java.io.File; michael@0: import java.io.FileNotFoundException; michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.util.List; michael@0: import java.util.concurrent.ThreadFactory; michael@0: michael@0: import static android.content.Context.ACTIVITY_SERVICE; michael@0: import static android.content.pm.ApplicationInfo.FLAG_LARGE_HEAP; michael@0: import static android.os.Build.VERSION.SDK_INT; michael@0: import static android.os.Build.VERSION_CODES.HONEYCOMB; michael@0: import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1; michael@0: import static android.os.Process.THREAD_PRIORITY_BACKGROUND; michael@0: import static android.provider.Settings.System.AIRPLANE_MODE_ON; michael@0: michael@0: final class Utils { michael@0: static final String THREAD_PREFIX = "Picasso-"; michael@0: static final String THREAD_IDLE_NAME = THREAD_PREFIX + "Idle"; michael@0: static final int DEFAULT_READ_TIMEOUT = 20 * 1000; // 20s michael@0: static final int DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // 15s michael@0: private static final String PICASSO_CACHE = "picasso-cache"; michael@0: private static final int KEY_PADDING = 50; // Determined by exact science. michael@0: private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB michael@0: private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB michael@0: michael@0: /* WebP file header michael@0: 0 1 2 3 michael@0: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 michael@0: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: | 'R' | 'I' | 'F' | 'F' | michael@0: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: | File Size | michael@0: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: | 'W' | 'E' | 'B' | 'P' | michael@0: +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ michael@0: */ michael@0: private static final int WEBP_FILE_HEADER_SIZE = 12; michael@0: private static final String WEBP_FILE_HEADER_RIFF = "RIFF"; michael@0: private static final String WEBP_FILE_HEADER_WEBP = "WEBP"; michael@0: michael@0: private Utils() { michael@0: // No instances. michael@0: } michael@0: michael@0: static int getBitmapBytes(Bitmap bitmap) { michael@0: int result; michael@0: if (SDK_INT >= HONEYCOMB_MR1) { michael@0: result = BitmapHoneycombMR1.getByteCount(bitmap); michael@0: } else { michael@0: result = bitmap.getRowBytes() * bitmap.getHeight(); michael@0: } michael@0: if (result < 0) { michael@0: throw new IllegalStateException("Negative size: " + bitmap); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: static void checkNotMain() { michael@0: if (Looper.getMainLooper().getThread() == Thread.currentThread()) { michael@0: throw new IllegalStateException("Method call should not happen from the main thread."); michael@0: } michael@0: } michael@0: michael@0: static String createKey(Request data) { michael@0: StringBuilder builder; michael@0: michael@0: if (data.uri != null) { michael@0: String path = data.uri.toString(); michael@0: builder = new StringBuilder(path.length() + KEY_PADDING); michael@0: builder.append(path); michael@0: } else { michael@0: builder = new StringBuilder(KEY_PADDING); michael@0: builder.append(data.resourceId); michael@0: } michael@0: builder.append('\n'); michael@0: michael@0: if (data.rotationDegrees != 0) { michael@0: builder.append("rotation:").append(data.rotationDegrees); michael@0: if (data.hasRotationPivot) { michael@0: builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY); michael@0: } michael@0: builder.append('\n'); michael@0: } michael@0: if (data.targetWidth != 0) { michael@0: builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight); michael@0: builder.append('\n'); michael@0: } michael@0: if (data.centerCrop) { michael@0: builder.append("centerCrop\n"); michael@0: } else if (data.centerInside) { michael@0: builder.append("centerInside\n"); michael@0: } michael@0: michael@0: if (data.transformations != null) { michael@0: //noinspection ForLoopReplaceableByForEach michael@0: for (int i = 0, count = data.transformations.size(); i < count; i++) { michael@0: builder.append(data.transformations.get(i).key()); michael@0: builder.append('\n'); michael@0: } michael@0: } michael@0: michael@0: return builder.toString(); michael@0: } michael@0: michael@0: static void closeQuietly(InputStream is) { michael@0: if (is == null) return; michael@0: try { michael@0: is.close(); michael@0: } catch (IOException ignored) { michael@0: } michael@0: } michael@0: michael@0: /** Returns {@code true} if header indicates the response body was loaded from the disk cache. */ michael@0: static boolean parseResponseSourceHeader(String header) { michael@0: if (header == null) { michael@0: return false; michael@0: } michael@0: String[] parts = header.split(" ", 2); michael@0: if ("CACHE".equals(parts[0])) { michael@0: return true; michael@0: } michael@0: if (parts.length == 1) { michael@0: return false; michael@0: } michael@0: try { michael@0: return "CONDITIONAL_CACHE".equals(parts[0]) && Integer.parseInt(parts[1]) == 304; michael@0: } catch (NumberFormatException e) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: static Downloader createDefaultDownloader(Context context) { michael@0: return new UrlConnectionDownloader(context); michael@0: } michael@0: michael@0: static File createDefaultCacheDir(Context context) { michael@0: File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE); michael@0: if (!cache.exists()) { michael@0: cache.mkdirs(); michael@0: } michael@0: return cache; michael@0: } michael@0: michael@0: static long calculateDiskCacheSize(File dir) { michael@0: long size = MIN_DISK_CACHE_SIZE; michael@0: michael@0: try { michael@0: StatFs statFs = new StatFs(dir.getAbsolutePath()); michael@0: long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize(); michael@0: // Target 2% of the total space. michael@0: size = available / 50; michael@0: } catch (IllegalArgumentException ignored) { michael@0: } michael@0: michael@0: // Bound inside min/max size for disk cache. michael@0: return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE); michael@0: } michael@0: michael@0: static int calculateMemoryCacheSize(Context context) { michael@0: ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); michael@0: boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0; michael@0: int memoryClass = am.getMemoryClass(); michael@0: if (largeHeap && SDK_INT >= HONEYCOMB) { michael@0: memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am); michael@0: } michael@0: // Target ~15% of the available heap. michael@0: return 1024 * 1024 * memoryClass / 7; michael@0: } michael@0: michael@0: static boolean isAirplaneModeOn(Context context) { michael@0: ContentResolver contentResolver = context.getContentResolver(); michael@0: return Settings.System.getInt(contentResolver, AIRPLANE_MODE_ON, 0) != 0; michael@0: } michael@0: michael@0: static boolean hasPermission(Context context, String permission) { michael@0: return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; michael@0: } michael@0: michael@0: static byte[] toByteArray(InputStream input) throws IOException { michael@0: ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); michael@0: byte[] buffer = new byte[1024 * 4]; michael@0: int n = 0; michael@0: while (-1 != (n = input.read(buffer))) { michael@0: byteArrayOutputStream.write(buffer, 0, n); michael@0: } michael@0: return byteArrayOutputStream.toByteArray(); michael@0: } michael@0: michael@0: static boolean isWebPFile(InputStream stream) throws IOException { michael@0: byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE]; michael@0: boolean isWebPFile = false; michael@0: if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) { michael@0: // If a file's header starts with RIFF and end with WEBP, the file is a WebP file michael@0: isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII")) michael@0: && WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII")); michael@0: } michael@0: return isWebPFile; michael@0: } michael@0: michael@0: static int getResourceId(Resources resources, Request data) throws FileNotFoundException { michael@0: if (data.resourceId != 0 || data.uri == null) { michael@0: return data.resourceId; michael@0: } michael@0: michael@0: String pkg = data.uri.getAuthority(); michael@0: if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri); michael@0: michael@0: int id; michael@0: List segments = data.uri.getPathSegments(); michael@0: if (segments == null || segments.isEmpty()) { michael@0: throw new FileNotFoundException("No path segments: " + data.uri); michael@0: } else if (segments.size() == 1) { michael@0: try { michael@0: id = Integer.parseInt(segments.get(0)); michael@0: } catch (NumberFormatException e) { michael@0: throw new FileNotFoundException("Last path segment is not a resource ID: " + data.uri); michael@0: } michael@0: } else if (segments.size() == 2) { michael@0: String type = segments.get(0); michael@0: String name = segments.get(1); michael@0: michael@0: id = resources.getIdentifier(name, type, pkg); michael@0: } else { michael@0: throw new FileNotFoundException("More than two path segments: " + data.uri); michael@0: } michael@0: return id; michael@0: } michael@0: michael@0: static Resources getResources(Context context, Request data) throws FileNotFoundException { michael@0: if (data.resourceId != 0 || data.uri == null) { michael@0: return context.getResources(); michael@0: } michael@0: michael@0: String pkg = data.uri.getAuthority(); michael@0: if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri); michael@0: try { michael@0: PackageManager pm = context.getPackageManager(); michael@0: return pm.getResourcesForApplication(pkg); michael@0: } catch (PackageManager.NameNotFoundException e) { michael@0: throw new FileNotFoundException("Unable to obtain resources for package: " + data.uri); michael@0: } michael@0: } michael@0: michael@0: @TargetApi(HONEYCOMB) michael@0: private static class ActivityManagerHoneycomb { michael@0: static int getLargeMemoryClass(ActivityManager activityManager) { michael@0: return activityManager.getLargeMemoryClass(); michael@0: } michael@0: } michael@0: michael@0: static class PicassoThreadFactory implements ThreadFactory { michael@0: @SuppressWarnings("NullableProblems") michael@0: public Thread newThread(Runnable r) { michael@0: return new PicassoThread(r); michael@0: } michael@0: } michael@0: michael@0: private static class PicassoThread extends Thread { michael@0: public PicassoThread(Runnable r) { michael@0: super(r); michael@0: } michael@0: michael@0: @Override public void run() { michael@0: Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); michael@0: super.run(); michael@0: } michael@0: } michael@0: michael@0: @TargetApi(HONEYCOMB_MR1) michael@0: private static class BitmapHoneycombMR1 { michael@0: static int getByteCount(Bitmap bitmap) { michael@0: return bitmap.getByteCount(); michael@0: } michael@0: } michael@0: }