Refactor ThumbnailsCacheManager to reduce repeated code
[pub/Android/ownCloud.git] / src / com / owncloud / android / datamodel / ThumbnailsCacheManager.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012-2014 ownCloud Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2,
6 * as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18 package com.owncloud.android.datamodel;
19
20 import java.io.File;
21 import java.lang.ref.WeakReference;
22
23 import org.apache.commons.httpclient.HttpStatus;
24 import org.apache.commons.httpclient.methods.GetMethod;
25
26 import android.accounts.Account;
27 import android.accounts.AccountManager;
28 import android.content.res.Resources;
29 import android.graphics.Bitmap;
30 import android.graphics.Bitmap.CompressFormat;
31 import android.graphics.BitmapFactory;
32 import android.graphics.Matrix;
33 import android.graphics.drawable.BitmapDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.media.ExifInterface;
36 import android.media.ThumbnailUtils;
37 import android.net.Uri;
38 import android.os.AsyncTask;
39 import android.widget.ImageView;
40
41 import com.owncloud.android.MainApp;
42 import com.owncloud.android.R;
43 import com.owncloud.android.lib.common.OwnCloudAccount;
44 import com.owncloud.android.lib.common.OwnCloudClient;
45 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
46 import com.owncloud.android.lib.common.accounts.AccountUtils.Constants;
47 import com.owncloud.android.lib.common.utils.Log_OC;
48 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
49 import com.owncloud.android.ui.adapter.DiskLruImageCache;
50 import com.owncloud.android.utils.BitmapUtils;
51 import com.owncloud.android.utils.DisplayUtils;
52
53 /**
54 * Manager for concurrent access to thumbnails cache.
55 *
56 * @author Tobias Kaminsky
57 * @author David A. Velasco
58 */
59 public class ThumbnailsCacheManager {
60
61 private static final String TAG = ThumbnailsCacheManager.class.getSimpleName();
62
63 private static final String CACHE_FOLDER = "thumbnailCache";
64 private static final String MINOR_SERVER_VERSION_FOR_THUMBS = "7.8.0";
65
66 private static final Object mThumbnailsDiskCacheLock = new Object();
67 private static DiskLruImageCache mThumbnailCache = null;
68 private static boolean mThumbnailCacheStarting = true;
69
70 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
71 private static final CompressFormat mCompressFormat = CompressFormat.JPEG;
72 private static final int mCompressQuality = 70;
73 private static OwnCloudClient mClient = null;
74 private static String mServerVersion = null;
75
76 public static Bitmap mDefaultImg =
77 BitmapFactory.decodeResource(
78 MainApp.getAppContext().getResources(),
79 DisplayUtils.getResourceId("image/png", "default.png")
80 );
81
82
83 public static class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
84
85 @Override
86 protected Void doInBackground(File... params) {
87 synchronized (mThumbnailsDiskCacheLock) {
88 mThumbnailCacheStarting = true;
89
90 if (mThumbnailCache == null) {
91 try {
92 // Check if media is mounted or storage is built-in, if so,
93 // try and use external cache dir; otherwise use internal cache dir
94 final String cachePath =
95 MainApp.getAppContext().getExternalCacheDir().getPath() +
96 File.separator + CACHE_FOLDER;
97 Log_OC.d(TAG, "create dir: " + cachePath);
98 final File diskCacheDir = new File(cachePath);
99 mThumbnailCache = new DiskLruImageCache(
100 diskCacheDir,
101 DISK_CACHE_SIZE,
102 mCompressFormat,
103 mCompressQuality
104 );
105 } catch (Exception e) {
106 Log_OC.d(TAG, "Thumbnail cache could not be opened ", e);
107 mThumbnailCache = null;
108 }
109 }
110 mThumbnailCacheStarting = false; // Finished initialization
111 mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads
112 }
113 return null;
114 }
115 }
116
117
118 public static void addBitmapToCache(String key, Bitmap bitmap) {
119 synchronized (mThumbnailsDiskCacheLock) {
120 if (mThumbnailCache != null) {
121 mThumbnailCache.put(key, bitmap);
122 }
123 }
124 }
125
126
127 public static Bitmap getBitmapFromDiskCache(String key) {
128 synchronized (mThumbnailsDiskCacheLock) {
129 // Wait while disk cache is started from background thread
130 while (mThumbnailCacheStarting) {
131 try {
132 mThumbnailsDiskCacheLock.wait();
133 } catch (InterruptedException e) {}
134 }
135 if (mThumbnailCache != null) {
136 return (Bitmap) mThumbnailCache.getBitmap(key);
137 }
138 }
139 return null;
140 }
141
142 public interface AsyncTaskFile {
143 public String getId();
144 public String getTagId();
145 public String getPath();
146 public String getRemotePath();
147 public boolean getNeedsUpdateThumbnail();
148 public void setNeedsUpdateThumbnail(boolean needsUpdate);
149 public boolean getIsDown();
150 public Object getFile();
151
152 }
153
154 public static class AsyncTaskFileLocal implements AsyncTaskFile{
155
156 private File mFile;
157 private boolean mNeedsUpdate;
158
159 public AsyncTaskFileLocal(File file){
160 mFile = file;
161 mNeedsUpdate = false;
162 }
163
164 @Override
165 public String getId() {
166 return String.valueOf(mFile.hashCode());
167 }
168
169 @Override
170 public String getTagId() {
171 return String.valueOf(mFile.hashCode());
172 }
173
174 @Override
175 public String getPath() {
176 return mFile.getAbsolutePath();
177 }
178
179 @Override
180 public String getRemotePath() {
181 return null;
182 }
183
184 @Override
185 public boolean getNeedsUpdateThumbnail() {
186 return mNeedsUpdate;
187 }
188
189 @Override
190 public void setNeedsUpdateThumbnail(boolean needsUpdate) {
191 mNeedsUpdate = needsUpdate;
192 }
193
194 @Override
195 public boolean getIsDown() {
196 return false;
197 }
198
199 @Override
200 public Object getFile() {
201 return mFile;
202 }
203 }
204
205 public static class AsyncTaskOCFile implements AsyncTaskFile{
206
207 private OCFile mFile;
208
209 public AsyncTaskOCFile(OCFile file){
210 mFile = file;
211 }
212
213 @Override
214 public String getId() {
215 return mFile.getRemoteId();
216 }
217
218 @Override
219 public String getTagId() {
220 return String.valueOf(mFile.getFileId());
221 }
222
223 @Override
224 public String getPath() {
225 return mFile.getStoragePath();
226 }
227
228 @Override
229 public String getRemotePath() {
230 return mFile.getRemotePath();
231 }
232
233 @Override
234 public boolean getNeedsUpdateThumbnail() {
235 return mFile.needsUpdateThumbnail();
236 }
237
238 @Override
239 public void setNeedsUpdateThumbnail(boolean needsUpdate) {
240 mFile.setNeedsUpdateThumbnail(needsUpdate);
241 }
242
243 @Override
244 public boolean getIsDown() {
245 return mFile.isDown();
246 }
247
248 @Override
249 public Object getFile() {
250 return mFile;
251 }
252 }
253
254 public static class ThumbnailGenerationGlobalTask extends AsyncTask<AsyncTaskFile, Void, Bitmap> {
255 private final WeakReference<ImageView> mImageViewReference;
256 private static Account mAccount;
257 private AsyncTaskFile mFile;
258 private FileDataStorageManager mStorageManager;
259
260
261 public ThumbnailGenerationGlobalTask(ImageView imageView, FileDataStorageManager storageManager, Account account) {
262 // Use a WeakReference to ensure the ImageView can be garbage collected
263 mImageViewReference = new WeakReference<ImageView>(imageView);
264 if (storageManager == null)
265 throw new IllegalArgumentException("storageManager must not be NULL");
266 mStorageManager = storageManager;
267 mAccount = account;
268 }
269
270 public ThumbnailGenerationGlobalTask(ImageView imageView) {
271 // Use a WeakReference to ensure the ImageView can be garbage collected
272 mImageViewReference = new WeakReference<ImageView>(imageView);
273 }
274
275 @Override
276 protected Bitmap doInBackground(AsyncTaskFile... params) {
277 Bitmap thumbnail = null;
278
279 try {
280 if (mAccount != null) {
281 AccountManager accountMgr = AccountManager.get(MainApp.getAppContext());
282
283 mServerVersion = accountMgr.getUserData(mAccount, Constants.KEY_OC_VERSION);
284 OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, MainApp.getAppContext());
285 mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
286 getClientFor(ocAccount, MainApp.getAppContext());
287 }
288
289 mFile = params[0];
290
291 final String imageKey = String.valueOf(mFile.getId());
292
293 // Check disk cache in background thread
294 thumbnail = getBitmapFromDiskCache(imageKey);
295
296 if (mFile instanceof AsyncTaskOCFile) {
297
298 // Not found in disk cache
299 if (thumbnail == null || mFile.getNeedsUpdateThumbnail()) {
300 // Converts dp to pixel
301 Resources r = MainApp.getAppContext().getResources();
302
303 int px = (int) Math.round(r.getDimension(R.dimen.file_icon_size));
304
305 if (mFile.getIsDown()) {
306 Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
307 mFile.getPath(), px, px);
308
309 if (bitmap != null) {
310 thumbnail = addThumbnailToCache(imageKey, bitmap, mFile.getPath(), px);
311
312 mFile.setNeedsUpdateThumbnail(false);
313 mStorageManager.saveFile((OCFile) mFile.getFile());
314 }
315
316 } else {
317 // Download thumbnail from server
318 if (mClient != null && mServerVersion != null) {
319 OwnCloudVersion serverOCVersion = new OwnCloudVersion(mServerVersion);
320 if (serverOCVersion.compareTo(new OwnCloudVersion(MINOR_SERVER_VERSION_FOR_THUMBS)) >= 0) {
321 try {
322 int status = -1;
323
324 String uri = mClient.getBaseUri() + "/index.php/apps/files/api/v1/thumbnail/" +
325 px + "/" + px + Uri.encode(mFile.getRemotePath(), "/");
326 Log_OC.d("Thumbnail", "URI: " + uri);
327 GetMethod get = new GetMethod(uri);
328 status = mClient.executeMethod(get);
329 if (status == HttpStatus.SC_OK) {
330 byte[] bytes = get.getResponseBody();
331 Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
332 thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
333
334 // Add thumbnail to cache
335 if (thumbnail != null) {
336 addBitmapToCache(imageKey, thumbnail);
337 }
338 }
339 } catch (Exception e) {
340 e.printStackTrace();
341 }
342 } else {
343 Log_OC.d(TAG, "Server too old");
344 }
345 }
346 }
347 }
348 } else if (mFile instanceof AsyncTaskFileLocal) {
349
350 // Not found in disk cache
351 if (thumbnail == null) {
352 // Converts dp to pixel
353 Resources r = MainApp.getAppContext().getResources();
354
355 int px = (int) Math.round(r.getDimension(R.dimen.file_icon_size));
356
357 Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
358 mFile.getPath(), px, px);
359
360 if (bitmap != null) {
361 thumbnail = addThumbnailToCache(imageKey, bitmap, mFile.getPath(), px);
362 }
363 }
364
365 }
366
367 }catch(Throwable t){
368 // the app should never break due to a problem with thumbnails
369 Log_OC.e(TAG, "Generation of thumbnail for " + mFile + " failed", t);
370 if (t instanceof OutOfMemoryError) {
371 System.gc();
372 }
373 }
374
375
376 return thumbnail;
377 }
378
379 protected void onPostExecute(Bitmap bitmap){
380 if (isCancelled()) {
381 bitmap = null;
382 }
383
384 if (mImageViewReference != null && bitmap != null) {
385 final ImageView imageView = mImageViewReference.get();
386 final ThumbnailGenerationGlobalTask bitmapWorkerTask = getBitmapGlobalWorkerTask(imageView);
387 if (this == bitmapWorkerTask && imageView != null) {
388 if (String.valueOf(imageView.getTag()).equals(mFile.getTagId())) {
389 imageView.setImageBitmap(bitmap);
390 }
391 }
392 }
393 }
394
395 private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int px){
396
397 Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
398
399 // Rotate image, obeying exif tag
400 thumbnail = BitmapUtils.rotateImage(thumbnail,path);
401
402 // Add thumbnail to cache
403 addBitmapToCache(imageKey, thumbnail);
404
405 return thumbnail;
406
407 }
408 }
409
410 public static boolean cancelPotentialGlobalWork(AsyncTaskFile file, ImageView imageView) {
411 final ThumbnailGenerationGlobalTask bitmapWorkerTask = getBitmapGlobalWorkerTask(imageView);
412
413 if (bitmapWorkerTask != null) {
414 final AsyncTaskFile bitmapData = bitmapWorkerTask.mFile;
415 // If bitmapData is not yet set or it differs from the new data
416 if (bitmapData == null || bitmapData != file) {
417 // Cancel previous task
418 bitmapWorkerTask.cancel(true);
419 } else {
420 // The same work is already in progress
421 return false;
422 }
423 }
424 // No task associated with the ImageView, or an existing task was cancelled
425 return true;
426 }
427
428 public static ThumbnailGenerationGlobalTask getBitmapGlobalWorkerTask(ImageView imageView) {
429 if (imageView != null) {
430 final Drawable drawable = imageView.getDrawable();
431 if (drawable instanceof AsyncGlobalDrawable) {
432 final AsyncGlobalDrawable asyncDrawable = (AsyncGlobalDrawable) drawable;
433 return asyncDrawable.getBitmapWorkerTask();
434 }
435 }
436 return null;
437 }
438
439 public static class AsyncGlobalDrawable extends BitmapDrawable {
440 private final WeakReference<ThumbnailGenerationGlobalTask> bitmapWorkerTaskReference;
441
442 public AsyncGlobalDrawable(
443 Resources res, Bitmap bitmap, ThumbnailGenerationGlobalTask bitmapWorkerTask
444 ) {
445
446 super(res, bitmap);
447 bitmapWorkerTaskReference =
448 new WeakReference<ThumbnailGenerationGlobalTask>(bitmapWorkerTask);
449 }
450
451 public ThumbnailGenerationGlobalTask getBitmapWorkerTask() {
452 return bitmapWorkerTaskReference.get();
453 }
454 }
455 }