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