Merge remote-tracking branch 'upstream/video_thumbnail' into video_thumbnail
[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.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.Paint;
40 import android.graphics.drawable.BitmapDrawable;
41 import android.graphics.drawable.Drawable;
42 import android.media.ThumbnailUtils;
43 import android.net.Uri;
44 import android.os.AsyncTask;
45 import android.widget.ImageView;
46
47 import com.owncloud.android.MainApp;
48 import com.owncloud.android.R;
49 import com.owncloud.android.authentication.AccountUtils;
50 import com.owncloud.android.lib.common.OwnCloudAccount;
51 import com.owncloud.android.lib.common.OwnCloudClient;
52 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
53 import com.owncloud.android.lib.common.utils.Log_OC;
54 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
55 import com.owncloud.android.ui.adapter.DiskLruImageCache;
56 import com.owncloud.android.utils.BitmapUtils;
57 import com.owncloud.android.utils.DisplayUtils;
58 import com.owncloud.android.utils.FileStorageUtils;
59
60 /**
61 * Manager for concurrent access to thumbnails cache.
62 */
63 public class ThumbnailsCacheManager {
64
65 private static final String TAG = ThumbnailsCacheManager.class.getSimpleName();
66
67 private static final String CACHE_FOLDER = "thumbnailCache";
68
69 private static final Object mThumbnailsDiskCacheLock = new Object();
70 private static DiskLruImageCache mThumbnailCache = null;
71 private static boolean mThumbnailCacheStarting = true;
72
73 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
74 private static final CompressFormat mCompressFormat = CompressFormat.JPEG;
75 private static final int mCompressQuality = 70;
76 private static OwnCloudClient mClient = null;
77
78 public static Bitmap mDefaultImg =
79 BitmapFactory.decodeResource(
80 MainApp.getAppContext().getResources(),
81 DisplayUtils.getFileTypeIconId("image/png", "default.png")
82 );
83
84
85 public static class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
86
87 @Override
88 protected Void doInBackground(File... params) {
89 synchronized (mThumbnailsDiskCacheLock) {
90 mThumbnailCacheStarting = true;
91
92 if (mThumbnailCache == null) {
93 try {
94 // Check if media is mounted or storage is built-in, if so,
95 // try and use external cache dir; otherwise use internal cache dir
96 final String cachePath =
97 MainApp.getAppContext().getExternalCacheDir().getPath() +
98 File.separator + CACHE_FOLDER;
99 Log_OC.d(TAG, "create dir: " + cachePath);
100 final File diskCacheDir = new File(cachePath);
101 mThumbnailCache = new DiskLruImageCache(
102 diskCacheDir,
103 DISK_CACHE_SIZE,
104 mCompressFormat,
105 mCompressQuality
106 );
107 } catch (Exception e) {
108 Log_OC.d(TAG, "Thumbnail cache could not be opened ", e);
109 mThumbnailCache = null;
110 }
111 }
112 mThumbnailCacheStarting = false; // Finished initialization
113 mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads
114 }
115 return null;
116 }
117 }
118
119
120 public static void addBitmapToCache(String key, Bitmap bitmap) {
121 synchronized (mThumbnailsDiskCacheLock) {
122 if (mThumbnailCache != null) {
123 mThumbnailCache.put(key, bitmap);
124 }
125 }
126 }
127
128
129 public static Bitmap getBitmapFromDiskCache(String key) {
130 synchronized (mThumbnailsDiskCacheLock) {
131 // Wait while disk cache is started from background thread
132 while (mThumbnailCacheStarting) {
133 try {
134 mThumbnailsDiskCacheLock.wait();
135 } catch (InterruptedException e) {
136 Log_OC.e(TAG, "Wait in mThumbnailsDiskCacheLock was interrupted", e);
137 }
138 }
139 if (mThumbnailCache != null) {
140 return mThumbnailCache.getBitmap(key);
141 }
142 }
143 return null;
144 }
145
146 public static class ThumbnailGenerationTask extends AsyncTask<Object, Void, Bitmap> {
147 private final WeakReference<ImageView> mImageViewReference;
148 private static Account mAccount;
149 private Object mFile;
150 private FileDataStorageManager mStorageManager;
151
152
153 public ThumbnailGenerationTask(ImageView imageView, FileDataStorageManager storageManager,
154 Account account) {
155 // Use a WeakReference to ensure the ImageView can be garbage collected
156 mImageViewReference = new WeakReference<ImageView>(imageView);
157 if (storageManager == null)
158 throw new IllegalArgumentException("storageManager must not be NULL");
159 mStorageManager = storageManager;
160 mAccount = account;
161 }
162
163 public ThumbnailGenerationTask(ImageView imageView) {
164 // Use a WeakReference to ensure the ImageView can be garbage collected
165 mImageViewReference = new WeakReference<ImageView>(imageView);
166 }
167
168 @Override
169 protected Bitmap doInBackground(Object... params) {
170 Bitmap thumbnail = null;
171
172 try {
173 if (mAccount != null) {
174 OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount,
175 MainApp.getAppContext());
176 mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
177 getClientFor(ocAccount, MainApp.getAppContext());
178 }
179
180 mFile = params[0];
181
182 if (mFile instanceof OCFile) {
183 thumbnail = doOCFileInBackground();
184
185 if (((OCFile) mFile).isVideo()){
186 thumbnail = addVideoOverlay(thumbnail);
187 }
188 } else if (mFile instanceof File) {
189 thumbnail = doFileInBackground();
190
191 String url = ((File) mFile).getAbsolutePath();
192 String mMimeType = FileStorageUtils.getMimeTypeFromName(url);
193
194 if (mMimeType != null && mMimeType.startsWith("video/")){
195 thumbnail = addVideoOverlay(thumbnail);
196 }
197 //} else { do nothing
198 }
199
200 }catch(Throwable t){
201 // the app should never break due to a problem with thumbnails
202 Log_OC.e(TAG, "Generation of thumbnail for " + mFile + " failed", t);
203 if (t instanceof OutOfMemoryError) {
204 System.gc();
205 }
206 }
207
208 return thumbnail;
209 }
210
211 protected void onPostExecute(Bitmap bitmap){
212 if (isCancelled()) {
213 bitmap = null;
214 }
215
216 if (bitmap != null) {
217 final ImageView imageView = mImageViewReference.get();
218 final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
219 if (this == bitmapWorkerTask) {
220 String tagId = "";
221 if (mFile instanceof OCFile){
222 tagId = String.valueOf(((OCFile)mFile).getFileId());
223 } else if (mFile instanceof File){
224 tagId = String.valueOf(mFile.hashCode());
225 }
226 if (String.valueOf(imageView.getTag()).equals(tagId)) {
227 imageView.setImageBitmap(bitmap);
228 }
229 }
230 }
231 }
232
233 /**
234 * Add thumbnail to cache
235 * @param imageKey: thumb key
236 * @param bitmap: image for extracting thumbnail
237 * @param path: image path
238 * @param px: thumbnail dp
239 * @return Bitmap
240 */
241 private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int px){
242
243 Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
244
245 // Rotate image, obeying exif tag
246 thumbnail = BitmapUtils.rotateImage(thumbnail,path);
247
248 // Add thumbnail to cache
249 addBitmapToCache(imageKey, thumbnail);
250
251 return thumbnail;
252 }
253
254 /**
255 * Converts size of file icon from dp to pixel
256 * @return int
257 */
258 private int getThumbnailDimension(){
259 // Converts dp to pixel
260 Resources r = MainApp.getAppContext().getResources();
261 return Math.round(r.getDimension(R.dimen.file_icon_size_grid));
262 }
263
264 private Bitmap doOCFileInBackground() {
265 OCFile file = (OCFile)mFile;
266
267 final String imageKey = String.valueOf(file.getRemoteId());
268
269 // Check disk cache in background thread
270 Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
271
272 // Not found in disk cache
273 if (thumbnail == null || file.needsUpdateThumbnail()) {
274
275 int px = getThumbnailDimension();
276
277 if (file.isDown()) {
278 Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
279 file.getStoragePath(), px, px);
280
281 if (bitmap != null) {
282 thumbnail = addThumbnailToCache(imageKey, bitmap, file.getStoragePath(), px);
283
284 file.setNeedsUpdateThumbnail(false);
285 mStorageManager.saveFile(file);
286 }
287
288 } else {
289 // Download thumbnail from server
290 OwnCloudVersion serverOCVersion = AccountUtils.getServerVersion(mAccount);
291 if (mClient != null && serverOCVersion != null) {
292 if (serverOCVersion.supportsRemoteThumbnails()) {
293 try {
294 String uri = mClient.getBaseUri() + "" +
295 "/index.php/apps/files/api/v1/thumbnail/" +
296 px + "/" + px + Uri.encode(file.getRemotePath(), "/");
297 Log_OC.d("Thumbnail", "URI: " + uri);
298 GetMethod get = new GetMethod(uri);
299 int status = mClient.executeMethod(get);
300 if (status == HttpStatus.SC_OK) {
301 // byte[] bytes = get.getResponseBody();
302 // Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0,
303 // bytes.length);
304 InputStream inputStream = get.getResponseBodyAsStream();
305 Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
306 thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
307
308 // Add thumbnail to cache
309 if (thumbnail != null) {
310 addBitmapToCache(imageKey, thumbnail);
311 }
312 }
313 } catch (Exception e) {
314 e.printStackTrace();
315 }
316 } else {
317 Log_OC.d(TAG, "Server too old");
318 }
319 }
320 }
321 }
322
323 return thumbnail;
324
325 }
326
327 private Bitmap doFileInBackground() {
328 File file = (File)mFile;
329
330 final String imageKey = String.valueOf(file.hashCode());
331
332 // Check disk cache in background thread
333 Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
334
335 // Not found in disk cache
336 if (thumbnail == null) {
337
338 int px = getThumbnailDimension();
339
340 Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
341 file.getAbsolutePath(), px, px);
342
343 if (bitmap != null) {
344 thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px);
345 }
346 }
347 return thumbnail;
348 }
349
350 }
351
352 public static boolean cancelPotentialWork(Object file, ImageView imageView) {
353 final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
354
355 if (bitmapWorkerTask != null) {
356 final Object bitmapData = bitmapWorkerTask.mFile;
357 // If bitmapData is not yet set or it differs from the new data
358 if (bitmapData == null || bitmapData != file) {
359 // Cancel previous task
360 bitmapWorkerTask.cancel(true);
361 } else {
362 // The same work is already in progress
363 return false;
364 }
365 }
366 // No task associated with the ImageView, or an existing task was cancelled
367 return true;
368 }
369
370 public static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) {
371 if (imageView != null) {
372 final Drawable drawable = imageView.getDrawable();
373 if (drawable instanceof AsyncDrawable) {
374 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
375 return asyncDrawable.getBitmapWorkerTask();
376 }
377 }
378 return null;
379 }
380
381 public static Bitmap addVideoOverlay(Bitmap thumbnail){
382 Bitmap playButton = BitmapFactory.decodeResource(MainApp.getAppContext().getResources(),
383 R.drawable.view_play);
384
385 Bitmap resizedPlayButton = Bitmap.createScaledBitmap(playButton,
386 (int) (thumbnail.getWidth() * 0.3),
387 (int) (thumbnail.getHeight() * 0.3), true);
388
389 Bitmap resultBitmap = Bitmap.createBitmap(thumbnail.getWidth(),
390 thumbnail.getHeight(),
391 Bitmap.Config.ARGB_8888);
392
393 Canvas c = new Canvas(resultBitmap);
394
395 // compute visual center of play button, according to resized image
396 int x1 = resizedPlayButton.getWidth();
397 int y1 = resizedPlayButton.getHeight() / 2;
398 int x2 = 0;
399 int y2 = resizedPlayButton.getWidth();
400 int x3 = 0;
401 int y3 = 0;
402
403 double ym = ( ((Math.pow(x3,2) - Math.pow(x1,2) + Math.pow(y3,2) - Math.pow(y1,2)) *
404 (x2 - x1)) - (Math.pow(x2,2) - Math.pow(x1,2) + Math.pow(y2,2) -
405 Math.pow(y1,2)) * (x3 - x1) ) / (2 * ( ((y3 - y1) * (x2 - x1)) -
406 ((y2 - y1) * (x3 - x1)) ));
407 double xm = ( (Math.pow(x2,2) - Math.pow(x1,2)) + (Math.pow(y2,2) - Math.pow(y1,2)) -
408 (2*ym*(y2 - y1)) ) / (2*(x2 - x1));
409
410 // offset to top left
411 double ox = - xm;
412 double oy = thumbnail.getHeight() - ym;
413
414
415 c.drawBitmap(thumbnail, 0, 0, null);
416
417 Paint p = new Paint();
418 p.setAlpha(230);
419
420 c.drawBitmap(resizedPlayButton, (float) ((thumbnail.getWidth() / 2) + ox),
421 (float) ((thumbnail.getHeight() / 2) - ym), p);
422
423 return resultBitmap;
424 }
425
426 public static class AsyncDrawable extends BitmapDrawable {
427 private final WeakReference<ThumbnailGenerationTask> bitmapWorkerTaskReference;
428
429 public AsyncDrawable(
430 Resources res, Bitmap bitmap, ThumbnailGenerationTask bitmapWorkerTask
431 ) {
432
433 super(res, bitmap);
434 bitmapWorkerTaskReference =
435 new WeakReference<ThumbnailGenerationTask>(bitmapWorkerTask);
436 }
437
438 public ThumbnailGenerationTask getBitmapWorkerTask() {
439 return bitmapWorkerTaskReference.get();
440 }
441 }
442 }