check available beta version
[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 public static long getMaxSize(){
170 if (mThumbnailCache != null) {
171 return mThumbnailCache.getMaxSize();
172 } else {
173 return -1l;
174 }
175 }
176
177 public static class ThumbnailGenerationTask extends AsyncTask<Object, Void, Bitmap> {
178 private final WeakReference<ImageView> mImageViewReference;
179 private WeakReference<ProgressBar> mProgressWheelRef;
180 private static Account mAccount;
181 private Object mFile;
182 private Boolean mIsThumbnail;
183 private FileDataStorageManager mStorageManager;
184
185 public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager,
186 Account account) {
187 // Use a WeakReference to ensure the ImageView can be garbage collected
188 mImageViewReference = new WeakReference<ImageView>(imageView);
189 if (storageManager == null)
190 throw new IllegalArgumentException("storageManager must not be NULL");
191 mStorageManager = storageManager;
192 mAccount = account;
193 }
194
195 public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager,
196 Account account, ProgressBar progressWheel) {
197 this(imageView, storageManager, account);
198 mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
199 }
200
201 public ThumbnailGenerationTask(ImageView imageView) {
202 // Use a WeakReference to ensure the ImageView can be garbage collected
203 mImageViewReference = new WeakReference<ImageView>(imageView);
204 }
205
206 @Override
207 protected Bitmap doInBackground(Object... params) {
208 Bitmap thumbnail = null;
209
210 try {
211 if (mAccount != null) {
212 OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount,
213 MainApp.getAppContext());
214 mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
215 getClientFor(ocAccount, MainApp.getAppContext());
216 }
217
218 mFile = params[0];
219 mIsThumbnail = (Boolean) params[1];
220
221
222 if (mFile instanceof OCFile) {
223 thumbnail = doOCFileInBackground(mIsThumbnail);
224
225 if (((OCFile) mFile).isVideo()){
226 thumbnail = addVideoOverlay(thumbnail);
227 }
228 } else if (mFile instanceof File) {
229 thumbnail = doFileInBackground(mIsThumbnail);
230
231 String url = ((File) mFile).getAbsolutePath();
232 String mMimeType = FileStorageUtils.getMimeTypeFromName(url);
233
234 if (mMimeType != null && mMimeType.startsWith("video/")){
235 thumbnail = addVideoOverlay(thumbnail);
236 }
237 //} else { do nothing
238 }
239
240 }catch(Throwable t){
241 // the app should never break due to a problem with thumbnails
242 Log_OC.e(TAG, "Generation of thumbnail for " + mFile + " failed", t);
243 if (t instanceof OutOfMemoryError) {
244 System.gc();
245 }
246 }
247
248 return thumbnail;
249 }
250
251 protected void onPostExecute(Bitmap bitmap){
252 if (bitmap != null) {
253 final ImageView imageView = mImageViewReference.get();
254 final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
255 if (this == bitmapWorkerTask) {
256 String tagId = "";
257 if (mFile instanceof OCFile){
258 tagId = String.valueOf(((OCFile)mFile).getFileId());
259 } else if (mFile instanceof File){
260 tagId = String.valueOf(mFile.hashCode());
261 }
262 if (String.valueOf(imageView.getTag()).equals(tagId)) {
263 if (mProgressWheelRef != null) {
264 final ProgressBar progressWheel = mProgressWheelRef.get();
265 if (progressWheel != null) {
266 progressWheel.setVisibility(View.GONE);
267 }
268 }
269 imageView.setImageBitmap(bitmap);
270 imageView.setVisibility(View.VISIBLE);
271 }
272 }
273 }
274 }
275
276 /**
277 * Add thumbnail to cache
278 * @param imageKey: thumb key
279 * @param bitmap: image for extracting thumbnail
280 * @param path: image path
281 * @param pxW: thumbnail width
282 * @param pxH: thumbnail height
283 * @return Bitmap
284 */
285 private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int pxW, int pxH){
286
287 Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
288
289 // Rotate image, obeying exif tag
290 thumbnail = BitmapUtils.rotateImage(thumbnail,path);
291
292 // Add thumbnail to cache
293 addBitmapToCache(imageKey, thumbnail);
294
295 return thumbnail;
296 }
297
298 /**
299 * Converts size of file icon from dp to pixel
300 * @return int
301 */
302 private int getThumbnailDimension(){
303 // Converts dp to pixel
304 Resources r = MainApp.getAppContext().getResources();
305 return Math.round(r.getDimension(R.dimen.file_icon_size_grid));
306 }
307
308 private Point getScreenDimension(){
309 WindowManager wm = (WindowManager) MainApp.getAppContext().getSystemService(Context.WINDOW_SERVICE);
310 Display display = wm.getDefaultDisplay();
311 Point test = new Point();
312 display.getSize(test);
313 return test;
314 }
315
316 private Bitmap doOCFileInBackground(Boolean isThumbnail) {
317 Bitmap thumbnail = null;
318 OCFile file = (OCFile)mFile;
319
320 // distinguish between thumbnail and resized image
321 String temp = String.valueOf(file.getRemoteId());
322 if (isThumbnail){
323 temp = "t" + temp;
324 } else {
325 temp = "r" + temp;
326 }
327
328 final String imageKey = temp;
329
330 // Check disk cache in background thread
331 thumbnail = getBitmapFromDiskCache(imageKey);
332
333 // Not found in disk cache
334 if (thumbnail == null || file.needsUpdateThumbnail()) {
335 int pxW = 0;
336 int pxH = 0;
337 if (mIsThumbnail) {
338 pxW = pxH = getThumbnailDimension();
339 } else {
340 Point p = getScreenDimension();
341 pxW = p.x;
342 pxH = p.y;
343 }
344
345 if (file.isDown()) {
346 Bitmap tempBitmap = BitmapUtils.decodeSampledBitmapFromFile(
347 file.getStoragePath(), pxW, pxH);
348 Bitmap bitmap = ThumbnailUtils.extractThumbnail(tempBitmap, pxW, pxH);
349
350 if (bitmap != null) {
351 // Handle PNG
352 if (file.getMimetype().equalsIgnoreCase("image/png")) {
353 bitmap = handlePNG(bitmap, pxW);
354 }
355
356 thumbnail = addThumbnailToCache(imageKey, bitmap,
357 file.getStoragePath(), pxW, pxH);
358
359 file.setNeedsUpdateThumbnail(false);
360 mStorageManager.saveFile(file);
361 }
362
363 } else {
364 // Download thumbnail from server
365 OwnCloudVersion serverOCVersion = AccountUtils.getServerVersion(mAccount);
366 if (mClient != null && serverOCVersion != null) {
367 if (serverOCVersion.supportsRemoteThumbnails()) {
368 try {
369 if (mIsThumbnail) {
370 String uri = mClient.getBaseUri() + "" +
371 "/index.php/apps/files/api/v1/thumbnail/" +
372 pxW + "/" + pxH + Uri.encode(file.getRemotePath(), "/");
373 Log_OC.d("Thumbnail", "Download URI: " + uri);
374 GetMethod get = new GetMethod(uri);
375 int status = mClient.executeMethod(get);
376 if (status == HttpStatus.SC_OK) {
377 InputStream inputStream = get.getResponseBodyAsStream();
378 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
379 thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH);
380 } else {
381 Log_OC.d(TAG, "Status: " + status);
382 }
383 } else {
384 String gallery = "";
385 if (serverOCVersion.supportsNativeGallery()){
386 gallery = "gallery";
387 } else {
388 gallery = "galleryplus";
389 }
390
391 String uri = mClient.getBaseUri() +
392 "/index.php/apps/" + gallery + "/api/preview/" + Integer.parseInt(file.getRemoteId().substring(0,8)) +
393 "/" + pxW + "/" + pxH;
394 Log_OC.d("Thumbnail", "FileName: " + file.getFileName() + " Download URI: " + uri);
395 GetMethod get = new GetMethod(uri);
396 int status = mClient.executeMethod(get);
397 if (status == HttpStatus.SC_OK) {
398 InputStream inputStream = get.getResponseBodyAsStream();
399 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
400 // Download via gallery app
401 thumbnail = bitmap;
402 }
403 }
404
405 // Handle PNG
406 if (thumbnail != null && file.getMimetype().equalsIgnoreCase("image/png")) {
407 thumbnail = handlePNG(thumbnail, pxW);
408 }
409
410 // Add thumbnail to cache
411 if (thumbnail != null) {
412 addBitmapToCache(imageKey, thumbnail);
413 }
414 } catch (Exception e) {
415 e.printStackTrace();
416 }
417 } else {
418 Log_OC.d(TAG, "Server too old");
419 }
420 }
421 }
422 }
423
424 return thumbnail;
425
426 }
427
428 private Bitmap handlePNG(Bitmap bitmap, int px){
429 Bitmap resultBitmap = Bitmap.createBitmap(px,
430 px,
431 Bitmap.Config.ARGB_8888);
432 Canvas c = new Canvas(resultBitmap);
433
434 c.drawColor(MainApp.getAppContext().getResources().
435 getColor(R.color.background_color));
436 c.drawBitmap(bitmap, 0, 0, null);
437
438 return resultBitmap;
439 }
440
441 private Bitmap doFileInBackground(Boolean mIsThumbnail) {
442 File file = (File)mFile;
443
444 // distinguish between thumbnail and resized image
445 String temp = String.valueOf(file.hashCode());
446 if (mIsThumbnail){
447 temp = "t" + temp;
448 } else {
449 temp = "r" + temp;
450 }
451
452 final String imageKey = temp;
453
454 // Check disk cache in background thread
455 Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
456
457 // Not found in disk cache
458 if (thumbnail == null) {
459 int pxW = 0;
460 int pxH = 0;
461 if (mIsThumbnail) {
462 pxW = pxH = getThumbnailDimension();
463 } else {
464 Point p = getScreenDimension();
465 pxW = p.x;
466 pxH = p.y;
467 }
468
469 Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
470 file.getAbsolutePath(), pxW, pxH);
471
472 if (bitmap != null) {
473 thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), pxW, pxH);
474 }
475 }
476 return thumbnail;
477 }
478
479 }
480
481 public static boolean cancelPotentialWork(Object file, ImageView imageView) {
482 final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
483
484 if (bitmapWorkerTask != null) {
485 final Object bitmapData = bitmapWorkerTask.mFile;
486 // If bitmapData is not yet set or it differs from the new data
487 if (bitmapData == null || bitmapData != file) {
488 // Cancel previous task
489 bitmapWorkerTask.cancel(true);
490 Log_OC.v(TAG, "Cancelled generation of thumbnail for a reused imageView");
491 } else {
492 // The same work is already in progress
493 return false;
494 }
495 }
496 // No task associated with the ImageView, or an existing task was cancelled
497 return true;
498 }
499
500 public static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) {
501 if (imageView != null) {
502 final Drawable drawable = imageView.getDrawable();
503 if (drawable instanceof AsyncDrawable) {
504 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
505 return asyncDrawable.getBitmapWorkerTask();
506 }
507 }
508 return null;
509 }
510
511 public static Bitmap addVideoOverlay(Bitmap thumbnail){
512 Bitmap playButton = BitmapFactory.decodeResource(MainApp.getAppContext().getResources(),
513 R.drawable.view_play);
514
515 Bitmap resizedPlayButton = Bitmap.createScaledBitmap(playButton,
516 (int) (thumbnail.getWidth() * 0.3),
517 (int) (thumbnail.getHeight() * 0.3), true);
518
519 Bitmap resultBitmap = Bitmap.createBitmap(thumbnail.getWidth(),
520 thumbnail.getHeight(),
521 Bitmap.Config.ARGB_8888);
522
523 Canvas c = new Canvas(resultBitmap);
524
525 // compute visual center of play button, according to resized image
526 int x1 = resizedPlayButton.getWidth();
527 int y1 = resizedPlayButton.getHeight() / 2;
528 int x2 = 0;
529 int y2 = resizedPlayButton.getWidth();
530 int x3 = 0;
531 int y3 = 0;
532
533 double ym = ( ((Math.pow(x3,2) - Math.pow(x1,2) + Math.pow(y3,2) - Math.pow(y1,2)) *
534 (x2 - x1)) - (Math.pow(x2,2) - Math.pow(x1,2) + Math.pow(y2,2) -
535 Math.pow(y1,2)) * (x3 - x1) ) / (2 * ( ((y3 - y1) * (x2 - x1)) -
536 ((y2 - y1) * (x3 - x1)) ));
537 double xm = ( (Math.pow(x2,2) - Math.pow(x1,2)) + (Math.pow(y2,2) - Math.pow(y1,2)) -
538 (2*ym*(y2 - y1)) ) / (2*(x2 - x1));
539
540 // offset to top left
541 double ox = - xm;
542 double oy = thumbnail.getHeight() - ym;
543
544
545 c.drawBitmap(thumbnail, 0, 0, null);
546
547 Paint p = new Paint();
548 p.setAlpha(230);
549
550 c.drawBitmap(resizedPlayButton, (float) ((thumbnail.getWidth() / 2) + ox),
551 (float) ((thumbnail.getHeight() / 2) - ym), p);
552
553 return resultBitmap;
554 }
555
556 public static class AsyncDrawable extends BitmapDrawable {
557 private final WeakReference<ThumbnailGenerationTask> bitmapWorkerTaskReference;
558
559 public AsyncDrawable(
560 Resources res, Bitmap bitmap, ThumbnailGenerationTask bitmapWorkerTask
561 ) {
562
563 super(res, bitmap);
564 bitmapWorkerTaskReference =
565 new WeakReference<ThumbnailGenerationTask>(bitmapWorkerTask);
566 }
567
568 public ThumbnailGenerationTask getBitmapWorkerTask() {
569 return bitmapWorkerTaskReference.get();
570 }
571 }
572 }