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