2 * ownCloud Android client application
4 * @author Tobias Kaminsky
5 * @author David A. Velasco
6 * Copyright (C) 2015 ownCloud Inc.
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.
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.
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/>.
22 package com
.owncloud
.android
.datamodel
;
25 import java
.io
.InputStream
;
26 import java
.lang
.ref
.WeakReference
;
27 import java
.net
.FileNameMap
;
28 import java
.net
.URLConnection
;
30 import org
.apache
.commons
.httpclient
.HttpStatus
;
31 import org
.apache
.commons
.httpclient
.methods
.GetMethod
;
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
;
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
;
72 * Manager for concurrent access to thumbnails cache.
74 public class ThumbnailsCacheManager
{
76 private static final String TAG
= ThumbnailsCacheManager
.class.getSimpleName();
78 private static final String CACHE_FOLDER
= "thumbnailCache";
79 private static final Integer CACHE_SIZE_MB
= 10;
81 private static final Object mThumbnailsDiskCacheLock
= new Object();
82 private static DiskLruImageCache mThumbnailCache
= null
;
83 private static boolean mThumbnailCacheStarting
= true
;
85 private static final CompressFormat mCompressFormat
= CompressFormat
.JPEG
;
86 private static final int mCompressQuality
= 70;
87 private static OwnCloudClient mClient
= null
;
89 public static Bitmap mDefaultImg
=
90 BitmapFactory
.decodeResource(
91 MainApp
.getAppContext().getResources(),
96 public static class InitDiskCacheTask
extends AsyncTask
<File
, Void
, Void
> {
99 protected Void
doInBackground(File
... params
) {
100 synchronized (mThumbnailsDiskCacheLock
) {
101 mThumbnailCacheStarting
= true
;
103 if (mThumbnailCache
== null
) {
105 SharedPreferences appPrefs
=
106 PreferenceManager
.getDefaultSharedPreferences(MainApp
.getAppContext());
107 // due to backward compatibility
108 Integer cacheSize
= CACHE_SIZE_MB
* 1024 * 1024;
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;
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(
130 } catch (Exception e
) {
131 Log_OC
.d(TAG
, "Thumbnail cache could not be opened ", e
);
132 mThumbnailCache
= null
;
135 mThumbnailCacheStarting
= false
; // Finished initialization
136 mThumbnailsDiskCacheLock
.notifyAll(); // Wake any waiting threads
143 public static void addBitmapToCache(String key
, Bitmap bitmap
) {
144 synchronized (mThumbnailsDiskCacheLock
) {
145 if (mThumbnailCache
!= null
) {
146 mThumbnailCache
.put(key
, bitmap
);
152 public static Bitmap
getBitmapFromDiskCache(String key
) {
153 synchronized (mThumbnailsDiskCacheLock
) {
154 // Wait while disk cache is started from background thread
155 while (mThumbnailCacheStarting
) {
157 mThumbnailsDiskCacheLock
.wait();
158 } catch (InterruptedException e
) {
159 Log_OC
.e(TAG
, "Wait in mThumbnailsDiskCacheLock was interrupted", e
);
162 if (mThumbnailCache
!= null
) {
163 return mThumbnailCache
.getBitmap(key
);
170 * Sets max size of cache
171 * @param maxSize in MB
174 public static boolean setMaxSize(long maxSize
){
175 if (mThumbnailCache
!= null
){
176 mThumbnailCache
.setMaxSize(maxSize
* 1024 * 1024);
184 * Shows max cache size
185 * @return max cache size in MB.
187 public static long getMaxSize(){
188 if (mThumbnailCache
== null
) {
189 new ThumbnailsCacheManager
.InitDiskCacheTask().execute();
191 return mThumbnailCache
.getMaxSize() / 1024 / 1024;
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
;
202 public ThumbnailGenerationTask(ImageView imageView
, FileDataStorageManager storageManager
,
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
;
212 public ThumbnailGenerationTask(ImageView imageView
, FileDataStorageManager storageManager
,
213 Account account
, ProgressBar progressWheel
) {
214 this(imageView
, storageManager
, account
);
215 mProgressWheelRef
= new WeakReference
<ProgressBar
>(progressWheel
);
218 public ThumbnailGenerationTask(ImageView imageView
) {
219 // Use a WeakReference to ensure the ImageView can be garbage collected
220 mImageViewReference
= new WeakReference
<ImageView
>(imageView
);
224 protected Bitmap
doInBackground(Object
... params
) {
225 Bitmap thumbnail
= null
;
228 if (mAccount
!= null
) {
229 OwnCloudAccount ocAccount
= new OwnCloudAccount(mAccount
,
230 MainApp
.getAppContext());
231 mClient
= OwnCloudClientManagerFactory
.getDefaultSingleton().
232 getClientFor(ocAccount
, MainApp
.getAppContext());
236 mIsThumbnail
= (Boolean
) params
[1];
239 if (mFile
instanceof OCFile
) {
240 thumbnail
= doOCFileInBackground(mIsThumbnail
);
242 if (((OCFile
) mFile
).isVideo()){
243 thumbnail
= addVideoOverlay(thumbnail
);
245 } else if (mFile
instanceof File
) {
246 thumbnail
= doFileInBackground(mIsThumbnail
);
248 String url
= ((File
) mFile
).getAbsolutePath();
249 String mMimeType
= FileStorageUtils
.getMimeTypeFromName(url
);
251 if (mMimeType
!= null
&& mMimeType
.startsWith("video/")){
252 thumbnail
= addVideoOverlay(thumbnail
);
254 //} else { do nothing
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
) {
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
) {
274 if (mFile
instanceof OCFile
){
275 tagId
= String
.valueOf(((OCFile
)mFile
).getFileId());
276 } else if (mFile
instanceof File
){
277 tagId
= String
.valueOf(mFile
.hashCode());
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
);
286 imageView
.setImageBitmap(bitmap
);
287 // imageView.setVisibility(View.VISIBLE);
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
302 private Bitmap
addThumbnailToCache(String imageKey
, Bitmap bitmap
, String path
, int pxW
, int pxH
){
304 Bitmap thumbnail
= ThumbnailUtils
.extractThumbnail(bitmap
, pxW
, pxH
);
306 // Rotate image, obeying exif tag
307 thumbnail
= BitmapUtils
.rotateImage(thumbnail
,path
);
309 // Add thumbnail to cache
310 addBitmapToCache(imageKey
, thumbnail
);
316 * Converts size of file icon from dp to pixel
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
));
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
);
333 private Bitmap
doOCFileInBackground(Boolean isThumbnail
) {
334 Bitmap thumbnail
= null
;
335 OCFile file
= (OCFile
)mFile
;
337 // distinguish between thumbnail and resized image
338 String temp
= String
.valueOf(file
.getRemoteId());
345 final String imageKey
= temp
;
347 // Check disk cache in background thread
348 thumbnail
= getBitmapFromDiskCache(imageKey
);
350 // Not found in disk cache
351 if (thumbnail
== null
|| file
.needsUpdateThumbnail()) {
355 pxW
= pxH
= getThumbnailDimension();
357 Point p
= getScreenDimension();
363 Bitmap tempBitmap
= BitmapUtils
.decodeSampledBitmapFromFile(
364 file
.getStoragePath(), pxW
, pxH
);
365 Bitmap bitmap
= ThumbnailUtils
.extractThumbnail(tempBitmap
, pxW
, pxH
);
367 if (bitmap
!= null
) {
369 if (file
.getMimetype().equalsIgnoreCase("image/png")) {
370 bitmap
= handlePNG(bitmap
, pxW
);
373 thumbnail
= addThumbnailToCache(imageKey
, bitmap
,
374 file
.getStoragePath(), pxW
, pxH
);
376 file
.setNeedsUpdateThumbnail(false
);
377 mStorageManager
.saveFile(file
);
381 // Download thumbnail from server
382 OwnCloudVersion serverOCVersion
= AccountUtils
.getServerVersion(mAccount
);
383 if (mClient
!= null
&& serverOCVersion
!= null
) {
384 if (serverOCVersion
.supportsRemoteThumbnails()) {
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
);
398 Log_OC
.d(TAG
, "Status: " + status
);
402 if (serverOCVersion
.supportsNativeGallery()){
405 gallery
= "galleryplus";
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
423 if (thumbnail
!= null
&& file
.getMimetype().equalsIgnoreCase("image/png")) {
424 thumbnail
= handlePNG(thumbnail
, pxW
);
427 // Add thumbnail to cache
428 if (thumbnail
!= null
) {
429 addBitmapToCache(imageKey
, thumbnail
);
431 } catch (Exception e
) {
435 Log_OC
.d(TAG
, "Server too old");
445 private Bitmap
handlePNG(Bitmap bitmap
, int px
){
446 Bitmap resultBitmap
= Bitmap
.createBitmap(px
,
448 Bitmap
.Config
.ARGB_8888
);
449 Canvas c
= new Canvas(resultBitmap
);
451 c
.drawColor(MainApp
.getAppContext().getResources().
452 getColor(R
.color
.background_color
));
453 c
.drawBitmap(bitmap
, 0, 0, null
);
458 private Bitmap
doFileInBackground(Boolean mIsThumbnail
) {
459 File file
= (File
)mFile
;
461 // distinguish between thumbnail and resized image
462 String temp
= String
.valueOf(file
.hashCode());
469 final String imageKey
= temp
;
471 // Check disk cache in background thread
472 Bitmap thumbnail
= getBitmapFromDiskCache(imageKey
);
474 // Not found in disk cache
475 if (thumbnail
== null
) {
479 pxW
= pxH
= getThumbnailDimension();
481 Point p
= getScreenDimension();
486 Bitmap bitmap
= BitmapUtils
.decodeSampledBitmapFromFile(
487 file
.getAbsolutePath(), pxW
, pxH
);
489 if (bitmap
!= null
) {
490 thumbnail
= addThumbnailToCache(imageKey
, bitmap
, file
.getPath(), pxW
, pxH
);
498 public static boolean cancelPotentialWork(Object file
, ImageView imageView
) {
499 final ThumbnailGenerationTask bitmapWorkerTask
= getBitmapWorkerTask(imageView
);
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");
509 // The same work is already in progress
513 // No task associated with the ImageView, or an existing task was cancelled
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();
528 public static Bitmap
addVideoOverlay(Bitmap thumbnail
){
529 Bitmap playButton
= BitmapFactory
.decodeResource(MainApp
.getAppContext().getResources(),
530 R
.drawable
.view_play
);
532 Bitmap resizedPlayButton
= Bitmap
.createScaledBitmap(playButton
,
533 (int) (thumbnail
.getWidth() * 0.3),
534 (int) (thumbnail
.getHeight() * 0.3), true
);
536 Bitmap resultBitmap
= Bitmap
.createBitmap(thumbnail
.getWidth(),
537 thumbnail
.getHeight(),
538 Bitmap
.Config
.ARGB_8888
);
540 Canvas c
= new Canvas(resultBitmap
);
542 // compute visual center of play button, according to resized image
543 int x1
= resizedPlayButton
.getWidth();
544 int y1
= resizedPlayButton
.getHeight() / 2;
546 int y2
= resizedPlayButton
.getWidth();
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
));
557 // offset to top left
559 double oy
= thumbnail
.getHeight() - ym
;
562 c
.drawBitmap(thumbnail
, 0, 0, null
);
564 Paint p
= new Paint();
567 c
.drawBitmap(resizedPlayButton
, (float) ((thumbnail
.getWidth() / 2) + ox
),
568 (float) ((thumbnail
.getHeight() / 2) - ym
), p
);
573 public static class AsyncDrawable
extends BitmapDrawable
{
574 private final WeakReference
<ThumbnailGenerationTask
> bitmapWorkerTaskReference
;
576 public AsyncDrawable(
577 Resources res
, Bitmap bitmap
, ThumbnailGenerationTask bitmapWorkerTask
581 bitmapWorkerTaskReference
=
582 new WeakReference
<ThumbnailGenerationTask
>(bitmapWorkerTask
);
585 public ThumbnailGenerationTask
getBitmapWorkerTask() {
586 return bitmapWorkerTaskReference
.get();