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