|
1 /* |
|
2 * Copyright (C) 2013 Square, Inc. |
|
3 * |
|
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 |
|
7 * |
|
8 * http://www.apache.org/licenses/LICENSE-2.0 |
|
9 * |
|
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. |
|
15 */ |
|
16 package com.squareup.picasso; |
|
17 |
|
18 import android.content.Context; |
|
19 import android.graphics.Bitmap; |
|
20 import android.graphics.BitmapFactory; |
|
21 import android.graphics.Matrix; |
|
22 import android.net.NetworkInfo; |
|
23 import android.net.Uri; |
|
24 import android.provider.MediaStore; |
|
25 import java.io.IOException; |
|
26 import java.io.PrintWriter; |
|
27 import java.io.StringWriter; |
|
28 import java.util.ArrayList; |
|
29 import java.util.List; |
|
30 import java.util.concurrent.Future; |
|
31 |
|
32 import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; |
|
33 import static android.content.ContentResolver.SCHEME_CONTENT; |
|
34 import static android.content.ContentResolver.SCHEME_FILE; |
|
35 import static android.provider.ContactsContract.Contacts; |
|
36 import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY; |
|
37 |
|
38 abstract class BitmapHunter implements Runnable { |
|
39 |
|
40 /** |
|
41 * Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since |
|
42 * this will only ever happen in background threads we help avoid excessive memory thrashing as |
|
43 * well as potential OOMs. Shamelessly stolen from Volley. |
|
44 */ |
|
45 private static final Object DECODE_LOCK = new Object(); |
|
46 private static final String ANDROID_ASSET = "android_asset"; |
|
47 protected static final int ASSET_PREFIX_LENGTH = |
|
48 (SCHEME_FILE + ":///" + ANDROID_ASSET + "/").length(); |
|
49 |
|
50 final Picasso picasso; |
|
51 final Dispatcher dispatcher; |
|
52 final Cache cache; |
|
53 final Stats stats; |
|
54 final String key; |
|
55 final Request data; |
|
56 final List<Action> actions; |
|
57 final boolean skipMemoryCache; |
|
58 |
|
59 Bitmap result; |
|
60 Future<?> future; |
|
61 Picasso.LoadedFrom loadedFrom; |
|
62 Exception exception; |
|
63 int exifRotation; // Determined during decoding of original resource. |
|
64 |
|
65 BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) { |
|
66 this.picasso = picasso; |
|
67 this.dispatcher = dispatcher; |
|
68 this.cache = cache; |
|
69 this.stats = stats; |
|
70 this.key = action.getKey(); |
|
71 this.data = action.getData(); |
|
72 this.skipMemoryCache = action.skipCache; |
|
73 this.actions = new ArrayList<Action>(4); |
|
74 attach(action); |
|
75 } |
|
76 |
|
77 protected void setExifRotation(int exifRotation) { |
|
78 this.exifRotation = exifRotation; |
|
79 } |
|
80 |
|
81 @Override public void run() { |
|
82 try { |
|
83 Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName()); |
|
84 |
|
85 result = hunt(); |
|
86 |
|
87 if (result == null) { |
|
88 dispatcher.dispatchFailed(this); |
|
89 } else { |
|
90 dispatcher.dispatchComplete(this); |
|
91 } |
|
92 } catch (Downloader.ResponseException e) { |
|
93 exception = e; |
|
94 dispatcher.dispatchFailed(this); |
|
95 } catch (IOException e) { |
|
96 exception = e; |
|
97 dispatcher.dispatchRetry(this); |
|
98 } catch (OutOfMemoryError e) { |
|
99 StringWriter writer = new StringWriter(); |
|
100 stats.createSnapshot().dump(new PrintWriter(writer)); |
|
101 exception = new RuntimeException(writer.toString(), e); |
|
102 dispatcher.dispatchFailed(this); |
|
103 } catch (Exception e) { |
|
104 exception = e; |
|
105 dispatcher.dispatchFailed(this); |
|
106 } finally { |
|
107 Thread.currentThread().setName(Utils.THREAD_IDLE_NAME); |
|
108 } |
|
109 } |
|
110 |
|
111 abstract Bitmap decode(Request data) throws IOException; |
|
112 |
|
113 Bitmap hunt() throws IOException { |
|
114 Bitmap bitmap; |
|
115 |
|
116 if (!skipMemoryCache) { |
|
117 bitmap = cache.get(key); |
|
118 if (bitmap != null) { |
|
119 stats.dispatchCacheHit(); |
|
120 loadedFrom = MEMORY; |
|
121 return bitmap; |
|
122 } |
|
123 } |
|
124 |
|
125 bitmap = decode(data); |
|
126 |
|
127 if (bitmap != null) { |
|
128 stats.dispatchBitmapDecoded(bitmap); |
|
129 if (data.needsTransformation() || exifRotation != 0) { |
|
130 synchronized (DECODE_LOCK) { |
|
131 if (data.needsMatrixTransform() || exifRotation != 0) { |
|
132 bitmap = transformResult(data, bitmap, exifRotation); |
|
133 } |
|
134 if (data.hasCustomTransformations()) { |
|
135 bitmap = applyCustomTransformations(data.transformations, bitmap); |
|
136 } |
|
137 } |
|
138 stats.dispatchBitmapTransformed(bitmap); |
|
139 } |
|
140 } |
|
141 |
|
142 return bitmap; |
|
143 } |
|
144 |
|
145 void attach(Action action) { |
|
146 actions.add(action); |
|
147 } |
|
148 |
|
149 void detach(Action action) { |
|
150 actions.remove(action); |
|
151 } |
|
152 |
|
153 boolean cancel() { |
|
154 return actions.isEmpty() && future != null && future.cancel(false); |
|
155 } |
|
156 |
|
157 boolean isCancelled() { |
|
158 return future != null && future.isCancelled(); |
|
159 } |
|
160 |
|
161 boolean shouldSkipMemoryCache() { |
|
162 return skipMemoryCache; |
|
163 } |
|
164 |
|
165 boolean shouldRetry(boolean airplaneMode, NetworkInfo info) { |
|
166 return false; |
|
167 } |
|
168 |
|
169 Bitmap getResult() { |
|
170 return result; |
|
171 } |
|
172 |
|
173 String getKey() { |
|
174 return key; |
|
175 } |
|
176 |
|
177 Request getData() { |
|
178 return data; |
|
179 } |
|
180 |
|
181 List<Action> getActions() { |
|
182 return actions; |
|
183 } |
|
184 |
|
185 Exception getException() { |
|
186 return exception; |
|
187 } |
|
188 |
|
189 Picasso.LoadedFrom getLoadedFrom() { |
|
190 return loadedFrom; |
|
191 } |
|
192 |
|
193 static BitmapHunter forRequest(Context context, Picasso picasso, Dispatcher dispatcher, |
|
194 Cache cache, Stats stats, Action action, Downloader downloader) { |
|
195 if (action.getData().resourceId != 0) { |
|
196 return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
197 } |
|
198 Uri uri = action.getData().uri; |
|
199 String scheme = uri.getScheme(); |
|
200 if (SCHEME_CONTENT.equals(scheme)) { |
|
201 if (Contacts.CONTENT_URI.getHost().equals(uri.getHost()) // |
|
202 && !uri.getPathSegments().contains(Contacts.Photo.CONTENT_DIRECTORY)) { |
|
203 return new ContactsPhotoBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
204 } else if (MediaStore.AUTHORITY.equals(uri.getAuthority())) { |
|
205 return new MediaStoreBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
206 } else { |
|
207 return new ContentStreamBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
208 } |
|
209 } else if (SCHEME_FILE.equals(scheme)) { |
|
210 if (!uri.getPathSegments().isEmpty() && ANDROID_ASSET.equals(uri.getPathSegments().get(0))) { |
|
211 return new AssetBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
212 } |
|
213 return new FileBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
214 } else if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { |
|
215 return new ResourceBitmapHunter(context, picasso, dispatcher, cache, stats, action); |
|
216 } else { |
|
217 return new NetworkBitmapHunter(picasso, dispatcher, cache, stats, action, downloader); |
|
218 } |
|
219 } |
|
220 |
|
221 static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) { |
|
222 calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options); |
|
223 } |
|
224 |
|
225 static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height, |
|
226 BitmapFactory.Options options) { |
|
227 int sampleSize = 1; |
|
228 if (height > reqHeight || width > reqWidth) { |
|
229 final int heightRatio = Math.round((float) height / (float) reqHeight); |
|
230 final int widthRatio = Math.round((float) width / (float) reqWidth); |
|
231 sampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; |
|
232 } |
|
233 |
|
234 options.inSampleSize = sampleSize; |
|
235 options.inJustDecodeBounds = false; |
|
236 } |
|
237 |
|
238 static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) { |
|
239 for (int i = 0, count = transformations.size(); i < count; i++) { |
|
240 final Transformation transformation = transformations.get(i); |
|
241 Bitmap newResult = transformation.transform(result); |
|
242 |
|
243 if (newResult == null) { |
|
244 final StringBuilder builder = new StringBuilder() // |
|
245 .append("Transformation ") |
|
246 .append(transformation.key()) |
|
247 .append(" returned null after ") |
|
248 .append(i) |
|
249 .append(" previous transformation(s).\n\nTransformation list:\n"); |
|
250 for (Transformation t : transformations) { |
|
251 builder.append(t.key()).append('\n'); |
|
252 } |
|
253 Picasso.HANDLER.post(new Runnable() { |
|
254 @Override public void run() { |
|
255 throw new NullPointerException(builder.toString()); |
|
256 } |
|
257 }); |
|
258 return null; |
|
259 } |
|
260 |
|
261 if (newResult == result && result.isRecycled()) { |
|
262 Picasso.HANDLER.post(new Runnable() { |
|
263 @Override public void run() { |
|
264 throw new IllegalStateException("Transformation " |
|
265 + transformation.key() |
|
266 + " returned input Bitmap but recycled it."); |
|
267 } |
|
268 }); |
|
269 return null; |
|
270 } |
|
271 |
|
272 // If the transformation returned a new bitmap ensure they recycled the original. |
|
273 if (newResult != result && !result.isRecycled()) { |
|
274 Picasso.HANDLER.post(new Runnable() { |
|
275 @Override public void run() { |
|
276 throw new IllegalStateException("Transformation " |
|
277 + transformation.key() |
|
278 + " mutated input Bitmap but failed to recycle the original."); |
|
279 } |
|
280 }); |
|
281 return null; |
|
282 } |
|
283 |
|
284 result = newResult; |
|
285 } |
|
286 return result; |
|
287 } |
|
288 |
|
289 static Bitmap transformResult(Request data, Bitmap result, int exifRotation) { |
|
290 int inWidth = result.getWidth(); |
|
291 int inHeight = result.getHeight(); |
|
292 |
|
293 int drawX = 0; |
|
294 int drawY = 0; |
|
295 int drawWidth = inWidth; |
|
296 int drawHeight = inHeight; |
|
297 |
|
298 Matrix matrix = new Matrix(); |
|
299 |
|
300 if (data.needsMatrixTransform()) { |
|
301 int targetWidth = data.targetWidth; |
|
302 int targetHeight = data.targetHeight; |
|
303 |
|
304 float targetRotation = data.rotationDegrees; |
|
305 if (targetRotation != 0) { |
|
306 if (data.hasRotationPivot) { |
|
307 matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY); |
|
308 } else { |
|
309 matrix.setRotate(targetRotation); |
|
310 } |
|
311 } |
|
312 |
|
313 if (data.centerCrop) { |
|
314 float widthRatio = targetWidth / (float) inWidth; |
|
315 float heightRatio = targetHeight / (float) inHeight; |
|
316 float scale; |
|
317 if (widthRatio > heightRatio) { |
|
318 scale = widthRatio; |
|
319 int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio)); |
|
320 drawY = (inHeight - newSize) / 2; |
|
321 drawHeight = newSize; |
|
322 } else { |
|
323 scale = heightRatio; |
|
324 int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio)); |
|
325 drawX = (inWidth - newSize) / 2; |
|
326 drawWidth = newSize; |
|
327 } |
|
328 matrix.preScale(scale, scale); |
|
329 } else if (data.centerInside) { |
|
330 float widthRatio = targetWidth / (float) inWidth; |
|
331 float heightRatio = targetHeight / (float) inHeight; |
|
332 float scale = widthRatio < heightRatio ? widthRatio : heightRatio; |
|
333 matrix.preScale(scale, scale); |
|
334 } else if (targetWidth != 0 && targetHeight != 0 // |
|
335 && (targetWidth != inWidth || targetHeight != inHeight)) { |
|
336 // If an explicit target size has been specified and they do not match the results bounds, |
|
337 // pre-scale the existing matrix appropriately. |
|
338 float sx = targetWidth / (float) inWidth; |
|
339 float sy = targetHeight / (float) inHeight; |
|
340 matrix.preScale(sx, sy); |
|
341 } |
|
342 } |
|
343 |
|
344 if (exifRotation != 0) { |
|
345 matrix.preRotate(exifRotation); |
|
346 } |
|
347 |
|
348 Bitmap newResult = |
|
349 Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true); |
|
350 if (newResult != result) { |
|
351 result.recycle(); |
|
352 result = newResult; |
|
353 } |
|
354 |
|
355 return result; |
|
356 } |
|
357 } |