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 import java.net.FileNameMap;
28 import java.net.URLConnection;
29
30 import org.apache.commons.httpclient.HttpStatus;
31 import org.apache.commons.httpclient.methods.GetMethod;
32
33 import android.accounts.Account;
34 import android.accounts.AccountManager;
35 import android.content.Context;
36 import android.content.SharedPreferences;
37 import android.content.res.Resources;
38 import android.graphics.Bitmap;
39 import android.graphics.Bitmap.CompressFormat;
40 import android.graphics.BitmapFactory;
41 import android.graphics.Canvas;
42 import android.graphics.Point;
43 import android.graphics.Canvas;
44 import android.graphics.Paint;
45 import android.graphics.drawable.BitmapDrawable;
46 import android.graphics.drawable.ColorDrawable;
47 import android.graphics.drawable.Drawable;
48 import android.media.ThumbnailUtils;
49 import android.net.Uri;
50 import android.os.AsyncTask;
51 import android.preference.PreferenceManager;
52 import android.view.Display;
53 import android.view.View;
54 import android.view.WindowManager;
55 import android.widget.ImageView;
56 import android.widget.ProgressBar;
57
58 import com.owncloud.android.MainApp;
59 import com.owncloud.android.R;
60 import com.owncloud.android.authentication.AccountUtils;
61 import com.owncloud.android.lib.common.OwnCloudAccount;
62 import com.owncloud.android.lib.common.OwnCloudClient;
63 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
64 import com.owncloud.android.lib.common.utils.Log_OC;
65 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
66 import com.owncloud.android.ui.adapter.DiskLruImageCache;
67 import com.owncloud.android.utils.BitmapUtils;
68 import com.owncloud.android.utils.DisplayUtils;
69 import com.owncloud.android.utils.FileStorageUtils;
70
71 /**
72 * Manager for concurrent access to thumbnails cache.
73 */
74 public class ThumbnailsCacheManager {
75
76 private static final String TAG = ThumbnailsCacheManager.class.getSimpleName();
77
78 private static final String CACHE_FOLDER = "thumbnailCache";
79 private static final Integer CACHE_SIZE_MB = 10;
80
81 private static final Object mThumbnailsDiskCacheLock = new Object();
82 private static DiskLruImageCache mThumbnailCache = null;
83 private static boolean mThumbnailCacheStarting = true;
84
85 private static final CompressFormat mCompressFormat = CompressFormat.JPEG;
86 private static final int mCompressQuality = 70;
87 private static OwnCloudClient mClient = null;
88
89 public static Bitmap mDefaultImg =
90 BitmapFactory.decodeResource(
91 MainApp.getAppContext().getResources(),
92 R.drawable.file_image
93 );
94
95
96 public static class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
97
98 @Override
99 protected Void doInBackground(File... params) {
100 synchronized (mThumbnailsDiskCacheLock) {
101 mThumbnailCacheStarting = true;
102
103 if (mThumbnailCache == null) {
104 try {
105 SharedPreferences appPrefs =
106 PreferenceManager.getDefaultSharedPreferences(MainApp.getAppContext());
107 // due to backward compatibility
108 Integer cacheSize = CACHE_SIZE_MB * 1024 * 1024;
109 try {
110 cacheSize = appPrefs.getInt("pref_cache_size", cacheSize);
111 } catch (ClassCastException e) {
112 String temp = appPrefs.getString("pref_cache_size",
113 cacheSize.toString());
114 cacheSize = Integer.decode(temp) * 1024 * 1024;
115 }
116
117 // Check if media is mounted or storage is built-in, if so,
118 // try and use external cache dir; otherwise use internal cache dir
119 final String cachePath =
120 MainApp.getAppContext().getExternalCacheDir().getPath() +
121 File.separator + CACHE_FOLDER;
122 Log_OC.d(TAG, "create dir: " + cachePath);
123 final File diskCacheDir = new File(cachePath);
124 mThumbnailCache = new DiskLruImageCache(
125 diskCacheDir,
126 cacheSize,
127 mCompressFormat,
128 mCompressQuality
129 );
130 } catch (Exception e) {
131 Log_OC.d(TAG, "Thumbnail cache could not be opened ", e);
132 mThumbnailCache = null;
133 }
134 }
135 mThumbnailCacheStarting = false; // Finished initialization
136 mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads
137 }
138 return null;
139 }
140 }
141
142
143 public static void addBitmapToCache(String key, Bitmap bitmap) {
144 synchronized (mThumbnailsDiskCacheLock) {
145 if (mThumbnailCache != null) {
146 mThumbnailCache.put(key, bitmap);
147 }
148 }
149 }
150
151
152 public static Bitmap getBitmapFromDiskCache(String key) {
153 synchronized (mThumbnailsDiskCacheLock) {
154 // Wait while disk cache is started from background thread
155 while (mThumbnailCacheStarting) {
156 try {
157 mThumbnailsDiskCacheLock.wait();
158 } catch (InterruptedException e) {
159 Log_OC.e(TAG, "Wait in mThumbnailsDiskCacheLock was interrupted", e);
160 }
161 }
162 if (mThumbnailCache != null) {
163 return mThumbnailCache.getBitmap(key);
164 }
165 }
166 return null;
167 }
168
169 /**
170 * Sets max size of cache
171 * @param maxSize in MB
172 * @return
173 */
174 public static boolean setMaxSize(long maxSize){
175 if (mThumbnailCache != null){
176 mThumbnailCache.setMaxSize(maxSize * 1024 * 1024);
177 return true;
178 } else {
179 return false;
180 }
181 }
182
183 /**
184 * Shows max cache size
185 * @return max cache size in MB.
186 */
187 public static long getMaxSize(){
188 if (mThumbnailCache == null) {
189 new ThumbnailsCacheManager.InitDiskCacheTask().execute();
190 }
191 return mThumbnailCache.getMaxSize() / 1024 / 1024;
192 }
193
194 public static class ThumbnailGenerationTask extends AsyncTask<Object, Void, Bitmap> {
195 private final WeakReference<ImageView> mImageViewReference;
196 private WeakReference<ProgressBar> mProgressWheelRef;
197 private static Account mAccount;
198 private Object mFile;
199 private Boolean mIsThumbnail;
200 private FileDataStorageManager mStorageManager;
201
202 public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager,
203 Account account) {
204 // Use a WeakReference to ensure the ImageView can be garbage collected
205 mImageViewReference = new WeakReference<ImageView>(imageView);
206 if (storageManager == null)
207 throw new IllegalArgumentException("storageManager must not be NULL");
208 mStorageManager = storageManager;
209 mAccount = account;
210 }
211
212 public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager,
213 Account account, ProgressBar progressWheel) {
214 this(imageView, storageManager, account);
215 mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
216 }
217
218 public ThumbnailGenerationTask(ImageView imageView) {
219 // Use a WeakReference to ensure the ImageView can be garbage collected
220 mImageViewReference = new WeakReference<ImageView>(imageView);
221 }
222
223 @Override
224 protected Bitmap doInBackground(Object... params) {
225 Bitmap thumbnail = null;
226
227 try {
228 if (mAccount != null) {
229 OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount,
230 MainApp.getAppContext());
231 mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
232 getClientFor(ocAccount, MainApp.getAppContext());
233 }
234
235 mFile = params[0];
236 mIsThumbnail = (Boolean) params[1];
237
238
239 if (mFile instanceof OCFile) {
240 thumbnail = doOCFileInBackground(mIsThumbnail);
241
242 if (((OCFile) mFile).isVideo()){
243 thumbnail = addVideoOverlay(thumbnail);
244 }
245 } else if (mFile instanceof File) {
246 thumbnail = doFileInBackground(mIsThumbnail);
247
248 String url = ((File) mFile).getAbsolutePath();
249 String mMimeType = FileStorageUtils.getMimeTypeFromName(url);
250
251 if (mMimeType != null && mMimeType.startsWith("video/")){
252 thumbnail = addVideoOverlay(thumbnail);
253 }
254 //} else { do nothing
255 }
256
257 }catch(Throwable t){
258 // the app should never break due to a problem with thumbnails
259 Log_OC.e(TAG, "Generation of thumbnail for " + mFile + " failed", t);
260 if (t instanceof OutOfMemoryError) {
261 System.gc();
262 }
263 }
264
265 return thumbnail;
266 }
267
268 protected void onPostExecute(Bitmap bitmap){
269 if (bitmap != null) {
270 final ImageView imageView = mImageViewReference.get();
271 final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
272 if (this == bitmapWorkerTask) {
273 String tagId = "";
274 if (mFile instanceof OCFile){
275 tagId = String.valueOf(((OCFile)mFile).getFileId());
276 } else if (mFile instanceof File){
277 tagId = String.valueOf(mFile.hashCode());
278 }
279 if (String.valueOf(imageView.getTag()).equals(tagId)) {
280 if (mProgressWheelRef != null) {
281 final ProgressBar progressWheel = mProgressWheelRef.get();
282 if (progressWheel != null) {
283 progressWheel.setVisibility(View.GONE);
284 }
285 }
286 imageView.setImageBitmap(bitmap);
287 // imageView.setVisibility(View.VISIBLE);
288 }
289 }
290 }
291 }
292
293 /**
294 * Add thumbnail to cache
295 * @param imageKey: thumb key
296 * @param bitmap: image for extracting thumbnail
297 * @param path: image path
298 * @param pxW: thumbnail width
299 * @param pxH: thumbnail height
300 * @return Bitmap
301 */
302 private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int pxW, int pxH){
303
304 Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
305
306 // Rotate image, obeying exif tag
307 thumbnail = BitmapUtils.rotateImage(thumbnail,path);
308
309 // Add thumbnail to cache
310 addBitmapToCache(imageKey, thumbnail);
311
312 return thumbnail;
313 }
314
315 /**
316 * Converts size of file icon from dp to pixel
317 * @return int
318 */
319 private int getThumbnailDimension(){
320 // Converts dp to pixel
321 Resources r = MainApp.getAppContext().getResources();
322 return Math.round(r.getDimension(R.dimen.file_icon_size_grid));
323 }
324
325 private Point getScreenDimension(){
326 WindowManager wm = (WindowManager) MainApp.getAppContext().getSystemService(Context.WINDOW_SERVICE);
327 Display display = wm.getDefaultDisplay();
328 Point test = new Point();
329 display.getSize(test);
330 return test;
331 }
332
333 private Bitmap doOCFileInBackground(Boolean isThumbnail) {
334 Bitmap thumbnail = null;
335 OCFile file = (OCFile)mFile;
336
337 // distinguish between thumbnail and resized image
338 String temp = String.valueOf(file.getRemoteId());
339 if (isThumbnail){
340 temp = "t" + temp;
341 } else {
342 temp = "r" + temp;
343 }
344
345 final String imageKey = temp;
346
347 // Check disk cache in background thread
348 thumbnail = getBitmapFromDiskCache(imageKey);
349
350 // Not found in disk cache
351 if (thumbnail == null || file.needsUpdateThumbnail()) {
352 int pxW = 0;
353 int pxH = 0;
354 if (mIsThumbnail) {
355 pxW = pxH = getThumbnailDimension();
356 } else {
357 Point p = getScreenDimension();
358 pxW = p.x;
359 pxH = p.y;
360 }
361
362 if (file.isDown()) {
363 Bitmap tempBitmap = BitmapUtils.decodeSampledBitmapFromFile(
364 file.getStoragePath(), pxW, pxH);
365 Bitmap bitmap = ThumbnailUtils.extractThumbnail(tempBitmap, pxW, pxH);
366
367 if (bitmap != null) {
368 // Handle PNG
369 if (file.getMimetype().equalsIgnoreCase("image/png")) {
370 bitmap = handlePNG(bitmap, pxW);
371 }
372
373 thumbnail = addThumbnailToCache(imageKey, bitmap,
374 file.getStoragePath(), pxW, pxH);
375
376 file.setNeedsUpdateThumbnail(false);
377 mStorageManager.saveFile(file);
378 }
379
380 } else {
381 // Download thumbnail from server
382 OwnCloudVersion serverOCVersion = AccountUtils.getServerVersion(mAccount);
383 if (mClient != null && serverOCVersion != null) {
384 if (serverOCVersion.supportsRemoteThumbnails()) {
385 try {
386 if (mIsThumbnail) {
387 String uri = mClient.getBaseUri() + "" +
388 "/index.php/apps/files/api/v1/thumbnail/" +
389 pxW + "/" + pxH + Uri.encode(file.getRemotePath(), "/");
390 Log_OC.d("Thumbnail", "Download URI: " + uri);
391 GetMethod get = new GetMethod(uri);
392 int status = mClient.executeMethod(get);
393 if (status == HttpStatus.SC_OK) {
394 InputStream inputStream = get.getResponseBodyAsStream();
395 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
396 thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
397 } else {
398 Log_OC.d(TAG, "Status: " + status);
399 }
400 } else {
401 String gallery = "";
402 if (serverOCVersion.supportsNativeGallery()){
403 gallery = "gallery";
404 } else {
405 gallery = "galleryplus";
406 }
407
408 String uri = mClient.getBaseUri() +
409 "/index.php/apps/" + gallery + "/api/preview/" + Integer.parseInt(file.getRemoteId().substring(0,8)) +
410 "/" + pxW + "/" + pxH;
411 Log_OC.d("Thumbnail", "FileName: " + file.getFileName() + " Download URI: " + uri);
412 GetMethod get = new GetMethod(uri);
413 int status = mClient.executeMethod(get);
414 if (status == HttpStatus.SC_OK) {
415 InputStream inputStream = get.getResponseBodyAsStream();
416 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
417 // Download via gallery app
418 thumbnail = bitmap;
419 }
420 }
421
422 // Handle PNG
423 if (thumbnail != null && file.getMimetype().equalsIgnoreCase("image/png")) {
424 thumbnail = handlePNG(thumbnail, pxW);
425 }
426
427 // Add thumbnail to cache
428 if (thumbnail != null) {
429 addBitmapToCache(imageKey, thumbnail);
430 }
431 } catch (Exception e) {
432 e.printStackTrace();
433 }
434 } else {
435 Log_OC.d(TAG, "Server too old");
436 }
437 }
438 }
439 }
440
441 return thumbnail;
442
443 }
444
445 private Bitmap handlePNG(Bitmap bitmap, int px){
446 Bitmap resultBitmap = Bitmap.createBitmap(px,
447 px,
448 Bitmap.Config.ARGB_8888);
449 Canvas c = new Canvas(resultBitmap);
450
451 c.drawColor(MainApp.getAppContext().getResources().
452 getColor(R.color.background_color));
453 c.drawBitmap(bitmap, 0, 0, null);
454
455 return resultBitmap;
456 }
457
458 private Bitmap doFileInBackground(Boolean mIsThumbnail) {
459 File file = (File)mFile;
460
461 // distinguish between thumbnail and resized image
462 String temp = String.valueOf(file.hashCode());
463 if (mIsThumbnail){
464 temp = "t" + temp;
465 } else {
466 temp = "r" + temp;
467 }
468
469 final String imageKey = temp;
470
471 // Check disk cache in background thread
472 Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
473
474 // Not found in disk cache
475 if (thumbnail == null) {
476 int pxW = 0;
477 int pxH = 0;
478 if (mIsThumbnail) {
479 pxW = pxH = getThumbnailDimension();
480 } else {
481 Point p = getScreenDimension();
482 pxW = p.x;
483 pxH = p.y;
484 }
485
486 Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
487 file.getAbsolutePath(), pxW, pxH);
488
489 if (bitmap != null) {
490 thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), pxW, pxH);
491 }
492 }
493 return thumbnail;
494 }
495
496 }
497
498 public static boolean cancelPotentialWork(Object file, ImageView imageView) {
499 final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
500
501 if (bitmapWorkerTask != null) {
502 final Object bitmapData = bitmapWorkerTask.mFile;
503 // If bitmapData is not yet set or it differs from the new data
504 if (bitmapData == null || bitmapData != file) {
505 // Cancel previous task
506 bitmapWorkerTask.cancel(true);
507 Log_OC.v(TAG, "Cancelled generation of thumbnail for a reused imageView");
508 } else {
509 // The same work is already in progress
510 return false;
511 }
512 }
513 // No task associated with the ImageView, or an existing task was cancelled
514 return true;
515 }
516
517 public static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) {
518 if (imageView != null) {
519 final Drawable drawable = imageView.getDrawable();
520 if (drawable instanceof AsyncDrawable) {
521 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
522 return asyncDrawable.getBitmapWorkerTask();
523 }
524 }
525 return null;
526 }
527
528 public static Bitmap addVideoOverlay(Bitmap thumbnail){
529 Bitmap playButton = BitmapFactory.decodeResource(MainApp.getAppContext().getResources(),
530 R.drawable.view_play);
531
532 Bitmap resizedPlayButton = Bitmap.createScaledBitmap(playButton,
533 (int) (thumbnail.getWidth() * 0.3),
534 (int) (thumbnail.getHeight() * 0.3), true);
535
536 Bitmap resultBitmap = Bitmap.createBitmap(thumbnail.getWidth(),
537 thumbnail.getHeight(),
538 Bitmap.Config.ARGB_8888);
539
540 Canvas c = new Canvas(resultBitmap);
541
542 // compute visual center of play button, according to resized image
543 int x1 = resizedPlayButton.getWidth();
544 int y1 = resizedPlayButton.getHeight() / 2;
545 int x2 = 0;
546 int y2 = resizedPlayButton.getWidth();
547 int x3 = 0;
548 int y3 = 0;
549
550 double ym = ( ((Math.pow(x3,2) - Math.pow(x1,2) + Math.pow(y3,2) - Math.pow(y1,2)) *
551 (x2 - x1)) - (Math.pow(x2,2) - Math.pow(x1,2) + Math.pow(y2,2) -
552 Math.pow(y1,2)) * (x3 - x1) ) / (2 * ( ((y3 - y1) * (x2 - x1)) -
553 ((y2 - y1) * (x3 - x1)) ));
554 double xm = ( (Math.pow(x2,2) - Math.pow(x1,2)) + (Math.pow(y2,2) - Math.pow(y1,2)) -
555 (2*ym*(y2 - y1)) ) / (2*(x2 - x1));
556
557 // offset to top left
558 double ox = - xm;
559 double oy = thumbnail.getHeight() - ym;
560
561
562 c.drawBitmap(thumbnail, 0, 0, null);
563
564 Paint p = new Paint();
565 p.setAlpha(230);
566
567 c.drawBitmap(resizedPlayButton, (float) ((thumbnail.getWidth() / 2) + ox),
568 (float) ((thumbnail.getHeight() / 2) - ym), p);
569
570 return resultBitmap;
571 }
572
573 public static class AsyncDrawable extends BitmapDrawable {
574 private final WeakReference<ThumbnailGenerationTask> bitmapWorkerTaskReference;
575
576 public AsyncDrawable(
577 Resources res, Bitmap bitmap, ThumbnailGenerationTask bitmapWorkerTask
578 ) {
579
580 super(res, bitmap);
581 bitmapWorkerTaskReference =
582 new WeakReference<ThumbnailGenerationTask>(bitmapWorkerTask);
583 }
584
585 public ThumbnailGenerationTask getBitmapWorkerTask() {
586 return bitmapWorkerTaskReference.get();
587 }
588 }
589 }