Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* |
michael@0 | 2 | * Copyright (C) 2013 Square, Inc. |
michael@0 | 3 | * |
michael@0 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
michael@0 | 5 | * you may not use this file except in compliance with the License. |
michael@0 | 6 | * You may obtain a copy of the License at |
michael@0 | 7 | * |
michael@0 | 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
michael@0 | 9 | * |
michael@0 | 10 | * Unless required by applicable law or agreed to in writing, software |
michael@0 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
michael@0 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
michael@0 | 13 | * See the License for the specific language governing permissions and |
michael@0 | 14 | * limitations under the License. |
michael@0 | 15 | */ |
michael@0 | 16 | package com.squareup.picasso; |
michael@0 | 17 | |
michael@0 | 18 | import android.annotation.TargetApi; |
michael@0 | 19 | import android.app.ActivityManager; |
michael@0 | 20 | import android.content.ContentResolver; |
michael@0 | 21 | import android.content.Context; |
michael@0 | 22 | import android.content.pm.PackageManager; |
michael@0 | 23 | import android.content.res.Resources; |
michael@0 | 24 | import android.graphics.Bitmap; |
michael@0 | 25 | import android.os.Looper; |
michael@0 | 26 | import android.os.Process; |
michael@0 | 27 | import android.os.StatFs; |
michael@0 | 28 | import android.provider.Settings; |
michael@0 | 29 | import java.io.ByteArrayOutputStream; |
michael@0 | 30 | import java.io.File; |
michael@0 | 31 | import java.io.FileNotFoundException; |
michael@0 | 32 | import java.io.IOException; |
michael@0 | 33 | import java.io.InputStream; |
michael@0 | 34 | import java.util.List; |
michael@0 | 35 | import java.util.concurrent.ThreadFactory; |
michael@0 | 36 | |
michael@0 | 37 | import static android.content.Context.ACTIVITY_SERVICE; |
michael@0 | 38 | import static android.content.pm.ApplicationInfo.FLAG_LARGE_HEAP; |
michael@0 | 39 | import static android.os.Build.VERSION.SDK_INT; |
michael@0 | 40 | import static android.os.Build.VERSION_CODES.HONEYCOMB; |
michael@0 | 41 | import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1; |
michael@0 | 42 | import static android.os.Process.THREAD_PRIORITY_BACKGROUND; |
michael@0 | 43 | import static android.provider.Settings.System.AIRPLANE_MODE_ON; |
michael@0 | 44 | |
michael@0 | 45 | final class Utils { |
michael@0 | 46 | static final String THREAD_PREFIX = "Picasso-"; |
michael@0 | 47 | static final String THREAD_IDLE_NAME = THREAD_PREFIX + "Idle"; |
michael@0 | 48 | static final int DEFAULT_READ_TIMEOUT = 20 * 1000; // 20s |
michael@0 | 49 | static final int DEFAULT_CONNECT_TIMEOUT = 15 * 1000; // 15s |
michael@0 | 50 | private static final String PICASSO_CACHE = "picasso-cache"; |
michael@0 | 51 | private static final int KEY_PADDING = 50; // Determined by exact science. |
michael@0 | 52 | private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB |
michael@0 | 53 | private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB |
michael@0 | 54 | |
michael@0 | 55 | /* WebP file header |
michael@0 | 56 | 0 1 2 3 |
michael@0 | 57 | 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 | 58 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
michael@0 | 59 | | 'R' | 'I' | 'F' | 'F' | |
michael@0 | 60 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
michael@0 | 61 | | File Size | |
michael@0 | 62 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
michael@0 | 63 | | 'W' | 'E' | 'B' | 'P' | |
michael@0 | 64 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
michael@0 | 65 | */ |
michael@0 | 66 | private static final int WEBP_FILE_HEADER_SIZE = 12; |
michael@0 | 67 | private static final String WEBP_FILE_HEADER_RIFF = "RIFF"; |
michael@0 | 68 | private static final String WEBP_FILE_HEADER_WEBP = "WEBP"; |
michael@0 | 69 | |
michael@0 | 70 | private Utils() { |
michael@0 | 71 | // No instances. |
michael@0 | 72 | } |
michael@0 | 73 | |
michael@0 | 74 | static int getBitmapBytes(Bitmap bitmap) { |
michael@0 | 75 | int result; |
michael@0 | 76 | if (SDK_INT >= HONEYCOMB_MR1) { |
michael@0 | 77 | result = BitmapHoneycombMR1.getByteCount(bitmap); |
michael@0 | 78 | } else { |
michael@0 | 79 | result = bitmap.getRowBytes() * bitmap.getHeight(); |
michael@0 | 80 | } |
michael@0 | 81 | if (result < 0) { |
michael@0 | 82 | throw new IllegalStateException("Negative size: " + bitmap); |
michael@0 | 83 | } |
michael@0 | 84 | return result; |
michael@0 | 85 | } |
michael@0 | 86 | |
michael@0 | 87 | static void checkNotMain() { |
michael@0 | 88 | if (Looper.getMainLooper().getThread() == Thread.currentThread()) { |
michael@0 | 89 | throw new IllegalStateException("Method call should not happen from the main thread."); |
michael@0 | 90 | } |
michael@0 | 91 | } |
michael@0 | 92 | |
michael@0 | 93 | static String createKey(Request data) { |
michael@0 | 94 | StringBuilder builder; |
michael@0 | 95 | |
michael@0 | 96 | if (data.uri != null) { |
michael@0 | 97 | String path = data.uri.toString(); |
michael@0 | 98 | builder = new StringBuilder(path.length() + KEY_PADDING); |
michael@0 | 99 | builder.append(path); |
michael@0 | 100 | } else { |
michael@0 | 101 | builder = new StringBuilder(KEY_PADDING); |
michael@0 | 102 | builder.append(data.resourceId); |
michael@0 | 103 | } |
michael@0 | 104 | builder.append('\n'); |
michael@0 | 105 | |
michael@0 | 106 | if (data.rotationDegrees != 0) { |
michael@0 | 107 | builder.append("rotation:").append(data.rotationDegrees); |
michael@0 | 108 | if (data.hasRotationPivot) { |
michael@0 | 109 | builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY); |
michael@0 | 110 | } |
michael@0 | 111 | builder.append('\n'); |
michael@0 | 112 | } |
michael@0 | 113 | if (data.targetWidth != 0) { |
michael@0 | 114 | builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight); |
michael@0 | 115 | builder.append('\n'); |
michael@0 | 116 | } |
michael@0 | 117 | if (data.centerCrop) { |
michael@0 | 118 | builder.append("centerCrop\n"); |
michael@0 | 119 | } else if (data.centerInside) { |
michael@0 | 120 | builder.append("centerInside\n"); |
michael@0 | 121 | } |
michael@0 | 122 | |
michael@0 | 123 | if (data.transformations != null) { |
michael@0 | 124 | //noinspection ForLoopReplaceableByForEach |
michael@0 | 125 | for (int i = 0, count = data.transformations.size(); i < count; i++) { |
michael@0 | 126 | builder.append(data.transformations.get(i).key()); |
michael@0 | 127 | builder.append('\n'); |
michael@0 | 128 | } |
michael@0 | 129 | } |
michael@0 | 130 | |
michael@0 | 131 | return builder.toString(); |
michael@0 | 132 | } |
michael@0 | 133 | |
michael@0 | 134 | static void closeQuietly(InputStream is) { |
michael@0 | 135 | if (is == null) return; |
michael@0 | 136 | try { |
michael@0 | 137 | is.close(); |
michael@0 | 138 | } catch (IOException ignored) { |
michael@0 | 139 | } |
michael@0 | 140 | } |
michael@0 | 141 | |
michael@0 | 142 | /** Returns {@code true} if header indicates the response body was loaded from the disk cache. */ |
michael@0 | 143 | static boolean parseResponseSourceHeader(String header) { |
michael@0 | 144 | if (header == null) { |
michael@0 | 145 | return false; |
michael@0 | 146 | } |
michael@0 | 147 | String[] parts = header.split(" ", 2); |
michael@0 | 148 | if ("CACHE".equals(parts[0])) { |
michael@0 | 149 | return true; |
michael@0 | 150 | } |
michael@0 | 151 | if (parts.length == 1) { |
michael@0 | 152 | return false; |
michael@0 | 153 | } |
michael@0 | 154 | try { |
michael@0 | 155 | return "CONDITIONAL_CACHE".equals(parts[0]) && Integer.parseInt(parts[1]) == 304; |
michael@0 | 156 | } catch (NumberFormatException e) { |
michael@0 | 157 | return false; |
michael@0 | 158 | } |
michael@0 | 159 | } |
michael@0 | 160 | |
michael@0 | 161 | static Downloader createDefaultDownloader(Context context) { |
michael@0 | 162 | return new UrlConnectionDownloader(context); |
michael@0 | 163 | } |
michael@0 | 164 | |
michael@0 | 165 | static File createDefaultCacheDir(Context context) { |
michael@0 | 166 | File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE); |
michael@0 | 167 | if (!cache.exists()) { |
michael@0 | 168 | cache.mkdirs(); |
michael@0 | 169 | } |
michael@0 | 170 | return cache; |
michael@0 | 171 | } |
michael@0 | 172 | |
michael@0 | 173 | static long calculateDiskCacheSize(File dir) { |
michael@0 | 174 | long size = MIN_DISK_CACHE_SIZE; |
michael@0 | 175 | |
michael@0 | 176 | try { |
michael@0 | 177 | StatFs statFs = new StatFs(dir.getAbsolutePath()); |
michael@0 | 178 | long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize(); |
michael@0 | 179 | // Target 2% of the total space. |
michael@0 | 180 | size = available / 50; |
michael@0 | 181 | } catch (IllegalArgumentException ignored) { |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | // Bound inside min/max size for disk cache. |
michael@0 | 185 | return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE); |
michael@0 | 186 | } |
michael@0 | 187 | |
michael@0 | 188 | static int calculateMemoryCacheSize(Context context) { |
michael@0 | 189 | ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); |
michael@0 | 190 | boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0; |
michael@0 | 191 | int memoryClass = am.getMemoryClass(); |
michael@0 | 192 | if (largeHeap && SDK_INT >= HONEYCOMB) { |
michael@0 | 193 | memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am); |
michael@0 | 194 | } |
michael@0 | 195 | // Target ~15% of the available heap. |
michael@0 | 196 | return 1024 * 1024 * memoryClass / 7; |
michael@0 | 197 | } |
michael@0 | 198 | |
michael@0 | 199 | static boolean isAirplaneModeOn(Context context) { |
michael@0 | 200 | ContentResolver contentResolver = context.getContentResolver(); |
michael@0 | 201 | return Settings.System.getInt(contentResolver, AIRPLANE_MODE_ON, 0) != 0; |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | static boolean hasPermission(Context context, String permission) { |
michael@0 | 205 | return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; |
michael@0 | 206 | } |
michael@0 | 207 | |
michael@0 | 208 | static byte[] toByteArray(InputStream input) throws IOException { |
michael@0 | 209 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
michael@0 | 210 | byte[] buffer = new byte[1024 * 4]; |
michael@0 | 211 | int n = 0; |
michael@0 | 212 | while (-1 != (n = input.read(buffer))) { |
michael@0 | 213 | byteArrayOutputStream.write(buffer, 0, n); |
michael@0 | 214 | } |
michael@0 | 215 | return byteArrayOutputStream.toByteArray(); |
michael@0 | 216 | } |
michael@0 | 217 | |
michael@0 | 218 | static boolean isWebPFile(InputStream stream) throws IOException { |
michael@0 | 219 | byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE]; |
michael@0 | 220 | boolean isWebPFile = false; |
michael@0 | 221 | if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) { |
michael@0 | 222 | // If a file's header starts with RIFF and end with WEBP, the file is a WebP file |
michael@0 | 223 | isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII")) |
michael@0 | 224 | && WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII")); |
michael@0 | 225 | } |
michael@0 | 226 | return isWebPFile; |
michael@0 | 227 | } |
michael@0 | 228 | |
michael@0 | 229 | static int getResourceId(Resources resources, Request data) throws FileNotFoundException { |
michael@0 | 230 | if (data.resourceId != 0 || data.uri == null) { |
michael@0 | 231 | return data.resourceId; |
michael@0 | 232 | } |
michael@0 | 233 | |
michael@0 | 234 | String pkg = data.uri.getAuthority(); |
michael@0 | 235 | if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri); |
michael@0 | 236 | |
michael@0 | 237 | int id; |
michael@0 | 238 | List<String> segments = data.uri.getPathSegments(); |
michael@0 | 239 | if (segments == null || segments.isEmpty()) { |
michael@0 | 240 | throw new FileNotFoundException("No path segments: " + data.uri); |
michael@0 | 241 | } else if (segments.size() == 1) { |
michael@0 | 242 | try { |
michael@0 | 243 | id = Integer.parseInt(segments.get(0)); |
michael@0 | 244 | } catch (NumberFormatException e) { |
michael@0 | 245 | throw new FileNotFoundException("Last path segment is not a resource ID: " + data.uri); |
michael@0 | 246 | } |
michael@0 | 247 | } else if (segments.size() == 2) { |
michael@0 | 248 | String type = segments.get(0); |
michael@0 | 249 | String name = segments.get(1); |
michael@0 | 250 | |
michael@0 | 251 | id = resources.getIdentifier(name, type, pkg); |
michael@0 | 252 | } else { |
michael@0 | 253 | throw new FileNotFoundException("More than two path segments: " + data.uri); |
michael@0 | 254 | } |
michael@0 | 255 | return id; |
michael@0 | 256 | } |
michael@0 | 257 | |
michael@0 | 258 | static Resources getResources(Context context, Request data) throws FileNotFoundException { |
michael@0 | 259 | if (data.resourceId != 0 || data.uri == null) { |
michael@0 | 260 | return context.getResources(); |
michael@0 | 261 | } |
michael@0 | 262 | |
michael@0 | 263 | String pkg = data.uri.getAuthority(); |
michael@0 | 264 | if (pkg == null) throw new FileNotFoundException("No package provided: " + data.uri); |
michael@0 | 265 | try { |
michael@0 | 266 | PackageManager pm = context.getPackageManager(); |
michael@0 | 267 | return pm.getResourcesForApplication(pkg); |
michael@0 | 268 | } catch (PackageManager.NameNotFoundException e) { |
michael@0 | 269 | throw new FileNotFoundException("Unable to obtain resources for package: " + data.uri); |
michael@0 | 270 | } |
michael@0 | 271 | } |
michael@0 | 272 | |
michael@0 | 273 | @TargetApi(HONEYCOMB) |
michael@0 | 274 | private static class ActivityManagerHoneycomb { |
michael@0 | 275 | static int getLargeMemoryClass(ActivityManager activityManager) { |
michael@0 | 276 | return activityManager.getLargeMemoryClass(); |
michael@0 | 277 | } |
michael@0 | 278 | } |
michael@0 | 279 | |
michael@0 | 280 | static class PicassoThreadFactory implements ThreadFactory { |
michael@0 | 281 | @SuppressWarnings("NullableProblems") |
michael@0 | 282 | public Thread newThread(Runnable r) { |
michael@0 | 283 | return new PicassoThread(r); |
michael@0 | 284 | } |
michael@0 | 285 | } |
michael@0 | 286 | |
michael@0 | 287 | private static class PicassoThread extends Thread { |
michael@0 | 288 | public PicassoThread(Runnable r) { |
michael@0 | 289 | super(r); |
michael@0 | 290 | } |
michael@0 | 291 | |
michael@0 | 292 | @Override public void run() { |
michael@0 | 293 | Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); |
michael@0 | 294 | super.run(); |
michael@0 | 295 | } |
michael@0 | 296 | } |
michael@0 | 297 | |
michael@0 | 298 | @TargetApi(HONEYCOMB_MR1) |
michael@0 | 299 | private static class BitmapHoneycombMR1 { |
michael@0 | 300 | static int getByteCount(Bitmap bitmap) { |
michael@0 | 301 | return bitmap.getByteCount(); |
michael@0 | 302 | } |
michael@0 | 303 | } |
michael@0 | 304 | } |