Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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;
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;
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;
38 abstract class BitmapHunter implements Runnable {
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();
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;
59 Bitmap result;
60 Future<?> future;
61 Picasso.LoadedFrom loadedFrom;
62 Exception exception;
63 int exifRotation; // Determined during decoding of original resource.
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 }
77 protected void setExifRotation(int exifRotation) {
78 this.exifRotation = exifRotation;
79 }
81 @Override public void run() {
82 try {
83 Thread.currentThread().setName(Utils.THREAD_PREFIX + data.getName());
85 result = hunt();
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 }
111 abstract Bitmap decode(Request data) throws IOException;
113 Bitmap hunt() throws IOException {
114 Bitmap bitmap;
116 if (!skipMemoryCache) {
117 bitmap = cache.get(key);
118 if (bitmap != null) {
119 stats.dispatchCacheHit();
120 loadedFrom = MEMORY;
121 return bitmap;
122 }
123 }
125 bitmap = decode(data);
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 }
142 return bitmap;
143 }
145 void attach(Action action) {
146 actions.add(action);
147 }
149 void detach(Action action) {
150 actions.remove(action);
151 }
153 boolean cancel() {
154 return actions.isEmpty() && future != null && future.cancel(false);
155 }
157 boolean isCancelled() {
158 return future != null && future.isCancelled();
159 }
161 boolean shouldSkipMemoryCache() {
162 return skipMemoryCache;
163 }
165 boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {
166 return false;
167 }
169 Bitmap getResult() {
170 return result;
171 }
173 String getKey() {
174 return key;
175 }
177 Request getData() {
178 return data;
179 }
181 List<Action> getActions() {
182 return actions;
183 }
185 Exception getException() {
186 return exception;
187 }
189 Picasso.LoadedFrom getLoadedFrom() {
190 return loadedFrom;
191 }
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 }
221 static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {
222 calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options);
223 }
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 }
234 options.inSampleSize = sampleSize;
235 options.inJustDecodeBounds = false;
236 }
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);
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 }
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 }
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 }
284 result = newResult;
285 }
286 return result;
287 }
289 static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
290 int inWidth = result.getWidth();
291 int inHeight = result.getHeight();
293 int drawX = 0;
294 int drawY = 0;
295 int drawWidth = inWidth;
296 int drawHeight = inHeight;
298 Matrix matrix = new Matrix();
300 if (data.needsMatrixTransform()) {
301 int targetWidth = data.targetWidth;
302 int targetHeight = data.targetHeight;
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 }
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 }
344 if (exifRotation != 0) {
345 matrix.preRotate(exifRotation);
346 }
348 Bitmap newResult =
349 Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true);
350 if (newResult != result) {
351 result.recycle();
352 result = newResult;
353 }
355 return result;
356 }
357 }