1package com.bumptech.glide.load.engine;
2
3import android.util.Log;
4
5import com.bumptech.glide.Priority;
6import com.bumptech.glide.load.Encoder;
7import com.bumptech.glide.load.Key;
8import com.bumptech.glide.load.Transformation;
9import com.bumptech.glide.load.data.DataFetcher;
10import com.bumptech.glide.load.engine.cache.DiskCache;
11import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
12import com.bumptech.glide.provider.DataLoadProvider;
13import com.bumptech.glide.util.LogTime;
14
15import java.io.BufferedOutputStream;
16import java.io.File;
17import java.io.FileNotFoundException;
18import java.io.FileOutputStream;
19import java.io.IOException;
20import java.io.OutputStream;
21
22/**
23 * A class responsible for decoding resources either from cached data or from the original source and applying
24 * transformations and transcodes.
25 *
26 * @param <A> The type of the source data the resource can be decoded from.
27 * @param <T> The type of resource that will be decoded.
28 * @param <Z> The type of resource that will be transcoded from the decoded and transformed resource.
29 */
30class DecodeJob<A, T, Z> {
31    private static final String TAG = "DecodeJob";
32    private static final FileOpener DEFAULT_FILE_OPENER = new FileOpener();
33
34    private final EngineKey resultKey;
35    private final int width;
36    private final int height;
37    private final DataFetcher<A> fetcher;
38    private final DataLoadProvider<A, T> loadProvider;
39    private final Transformation<T> transformation;
40    private final ResourceTranscoder<T, Z> transcoder;
41    private final DiskCacheStrategy diskCacheStrategy;
42    private final DiskCache diskCache;
43    private final Priority priority;
44    private final FileOpener fileOpener;
45
46    private volatile boolean isCancelled;
47
48    public DecodeJob(EngineKey resultKey, int width, int height, DataFetcher<A> fetcher,
49            DataLoadProvider<A, T> loadProvider, Transformation<T> transformation, ResourceTranscoder<T, Z> transcoder,
50            DiskCache diskCache, DiskCacheStrategy diskCacheStrategy, Priority priority) {
51        this(resultKey, width, height, fetcher, loadProvider, transformation, transcoder, diskCache, diskCacheStrategy,
52                priority, DEFAULT_FILE_OPENER);
53    }
54
55    // Visible for testing.
56    DecodeJob(EngineKey resultKey, int width, int height, DataFetcher<A> fetcher,
57            DataLoadProvider<A, T> loadProvider, Transformation<T> transformation, ResourceTranscoder<T, Z> transcoder,
58            DiskCache diskCache, DiskCacheStrategy diskCacheStrategy, Priority priority, FileOpener fileOpener) {
59        this.resultKey = resultKey;
60        this.width = width;
61        this.height = height;
62        this.fetcher = fetcher;
63        this.loadProvider = loadProvider;
64        this.transformation = transformation;
65        this.transcoder = transcoder;
66        this.diskCacheStrategy = diskCacheStrategy;
67        this.diskCache = diskCache;
68        this.priority = priority;
69        this.fileOpener = fileOpener;
70    }
71
72    /**
73     * Returns a transcoded resource decoded from transformed resource data in the disk cache, or null if no such
74     * resource exists.
75     *
76     * @throws Exception
77     */
78    public Resource<Z> decodeResultFromCache() throws Exception {
79        if (!diskCacheStrategy.cacheResult()) {
80            return null;
81        }
82
83        long startTime = LogTime.getLogTime();
84        Resource<T> transformed = loadFromCache(resultKey);
85        if (Log.isLoggable(TAG, Log.VERBOSE)) {
86            logWithTimeAndKey("Decoded transformed from cache", startTime);
87        }
88        startTime = LogTime.getLogTime();
89        Resource<Z> result = transcode(transformed);
90        if (Log.isLoggable(TAG, Log.VERBOSE)) {
91            logWithTimeAndKey("Transcoded transformed from cache", startTime);
92        }
93        return result;
94    }
95
96    /**
97     * Returns a transformed and transcoded resource decoded from source data in the disk cache, or null if no such
98     * resource exists.
99     *
100     * @throws Exception
101     */
102    public Resource<Z> decodeSourceFromCache() throws Exception {
103        if (!diskCacheStrategy.cacheSource()) {
104            return null;
105        }
106
107        long startTime = LogTime.getLogTime();
108        Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
109        if (Log.isLoggable(TAG, Log.VERBOSE)) {
110            logWithTimeAndKey("Decoded source from cache", startTime);
111        }
112        return transformEncodeAndTranscode(decoded);
113    }
114
115    /**
116     * Returns a transformed and transcoded resource decoded from source data, or null if no source data could be
117     * obtained or no resource could be decoded.
118     *
119     * <p>
120     *     Depending on the {@link com.bumptech.glide.load.engine.DiskCacheStrategy} used, source data is either decoded
121     *     directly or first written to the disk cache and then decoded from the disk cache.
122     * </p>
123     *
124     * @throws Exception
125     */
126    public Resource<Z> decodeFromSource() throws Exception {
127        Resource<T> decoded = decodeSource();
128        return transformEncodeAndTranscode(decoded);
129    }
130
131    public void cancel() {
132        fetcher.cancel();
133        isCancelled = true;
134    }
135
136    private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
137        long startTime = LogTime.getLogTime();
138        Resource<T> transformed = transform(decoded);
139        if (Log.isLoggable(TAG, Log.VERBOSE)) {
140            logWithTimeAndKey("Transformed resource from source", startTime);
141        }
142
143        writeTransformedToCache(transformed);
144
145        startTime = LogTime.getLogTime();
146        Resource<Z> result = transcode(transformed);
147        if (Log.isLoggable(TAG, Log.VERBOSE)) {
148            logWithTimeAndKey("Transcoded transformed from source", startTime);
149        }
150        return result;
151    }
152
153    private void writeTransformedToCache(Resource<T> transformed) {
154        if (transformed == null || !diskCacheStrategy.cacheResult()) {
155            return;
156        }
157        long startTime = LogTime.getLogTime();
158        SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
159        diskCache.put(resultKey, writer);
160        if (Log.isLoggable(TAG, Log.VERBOSE)) {
161            logWithTimeAndKey("Wrote transformed from source to cache", startTime);
162        }
163    }
164
165    private Resource<T> decodeSource() throws Exception {
166        Resource<T> decoded = null;
167        try {
168            long startTime = LogTime.getLogTime();
169            final A data = fetcher.loadData(priority);
170            if (Log.isLoggable(TAG, Log.VERBOSE)) {
171                logWithTimeAndKey("Fetched data", startTime);
172            }
173            if (isCancelled) {
174                return null;
175            }
176            decoded = decodeFromSourceData(data);
177        } finally {
178            fetcher.cleanup();
179        }
180        return decoded;
181    }
182
183    private Resource<T> decodeFromSourceData(A data) throws IOException {
184        final Resource<T> decoded;
185        if (diskCacheStrategy.cacheSource()) {
186            decoded = cacheAndDecodeSourceData(data);
187        } else {
188            long startTime = LogTime.getLogTime();
189            decoded = loadProvider.getSourceDecoder().decode(data, width, height);
190            if (Log.isLoggable(TAG, Log.VERBOSE)) {
191                logWithTimeAndKey("Decoded from source", startTime);
192            }
193        }
194        return decoded;
195    }
196
197    private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
198        long startTime = LogTime.getLogTime();
199        SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
200        diskCache.put(resultKey.getOriginalKey(), writer);
201        if (Log.isLoggable(TAG, Log.VERBOSE)) {
202            logWithTimeAndKey("Wrote source to cache", startTime);
203        }
204
205        startTime = LogTime.getLogTime();
206        Resource<T> result = loadFromCache(resultKey.getOriginalKey());
207        if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
208            logWithTimeAndKey("Decoded source from cache", startTime);
209        }
210        return result;
211    }
212
213    private Resource<T> loadFromCache(Key key) throws IOException {
214        File cacheFile = diskCache.get(key);
215        if (cacheFile == null) {
216            return null;
217        }
218
219        Resource<T> result = null;
220        try {
221            result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
222        } finally {
223            if (result == null) {
224                diskCache.delete(key);
225            }
226        }
227        return result;
228    }
229
230    private Resource<T> transform(Resource<T> decoded) {
231        if (decoded == null) {
232            return null;
233        }
234
235        Resource<T> transformed = transformation.transform(decoded, width, height);
236        if (!decoded.equals(transformed)) {
237            decoded.recycle();
238        }
239        return transformed;
240    }
241
242    private Resource<Z> transcode(Resource<T> transformed) {
243        if (transformed == null) {
244            return null;
245        }
246        return transcoder.transcode(transformed);
247    }
248
249    private void logWithTimeAndKey(String message, long startTime) {
250        Log.v(TAG, message + " in " + LogTime.getElapsedMillis(startTime) + resultKey);
251    }
252
253    class SourceWriter<DataType> implements DiskCache.Writer {
254
255        private final Encoder<DataType> encoder;
256        private final DataType data;
257
258        public SourceWriter(Encoder<DataType> encoder, DataType data) {
259            this.encoder = encoder;
260            this.data = data;
261        }
262
263        @Override
264        public boolean write(File file) {
265            boolean success = false;
266            OutputStream os = null;
267            try {
268                os = fileOpener.open(file);
269                success = encoder.encode(data, os);
270            } catch (FileNotFoundException e) {
271                if (Log.isLoggable(TAG, Log.DEBUG)) {
272                    Log.d(TAG, "Failed to find file to write to disk cache", e);
273                }
274            } finally {
275                if (os != null) {
276                    try {
277                        os.close();
278                    } catch (IOException e) {
279                        // Do nothing.
280                    }
281                }
282            }
283            return success;
284        }
285    }
286
287    static class FileOpener {
288        public OutputStream open(File file) throws FileNotFoundException {
289            return new BufferedOutputStream(new FileOutputStream(file));
290        }
291    }
292}
293