Merge remote-tracking branch 'remotes/upstream/master' into switchListVsGridMaster
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / preview / PreviewImageFragment.java
1 /**
2 * ownCloud Android client application
3 *
4 * @author David A. Velasco
5 * Copyright (C) 2015 ownCloud Inc.
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License version 2,
9 * as published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 */
20 package com.owncloud.android.ui.preview;
21
22 import java.lang.ref.WeakReference;
23
24 import android.accounts.Account;
25 import android.annotation.SuppressLint;
26 import android.app.Activity;
27 import android.graphics.Bitmap;
28 import android.graphics.Point;
29 import android.graphics.drawable.Drawable;
30 import android.os.AsyncTask;
31 import android.os.Bundle;
32 import android.support.v4.app.FragmentStatePagerAdapter;
33 import android.view.LayoutInflater;
34 import android.view.Menu;
35 import android.view.MenuInflater;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.View.OnClickListener;
39 import android.view.ViewGroup;
40 import android.widget.ImageView;
41 import android.widget.ProgressBar;
42 import android.widget.TextView;
43
44 import com.owncloud.android.MainApp;
45 import com.owncloud.android.R;
46 import com.owncloud.android.datamodel.OCFile;
47 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
48 import com.owncloud.android.files.FileMenuFilter;
49 import com.owncloud.android.lib.common.utils.Log_OC;
50 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
51 import com.owncloud.android.ui.dialog.RemoveFileDialogFragment;
52 import com.owncloud.android.ui.fragment.FileFragment;
53 import com.owncloud.android.utils.BitmapUtils;
54 import com.owncloud.android.utils.DisplayUtils;
55
56 import third_parties.michaelOrtiz.TouchImageViewCustom;
57
58
59 /**
60 * This fragment shows a preview of a downloaded image.
61 *
62 * Trying to get an instance with a NULL {@link OCFile} will produce an
63 * {@link IllegalStateException}.
64 *
65 * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on
66 * instantiation too.
67 */
68 public class PreviewImageFragment extends FileFragment {
69
70 public static final String EXTRA_FILE = "FILE";
71
72 private static final String ARG_FILE = "FILE";
73 private static final String ARG_IGNORE_FIRST = "IGNORE_FIRST";
74
75 private TouchImageViewCustom mImageView;
76 private TextView mMessageView;
77 private ProgressBar mProgressWheel;
78
79 private Boolean mShowResizedImage = false;
80
81 public Bitmap mBitmap = null;
82
83 private static final String TAG = PreviewImageFragment.class.getSimpleName();
84
85 private boolean mIgnoreFirstSavedState;
86
87 private LoadBitmapTask mLoadBitmapTask = null;
88
89
90 /**
91 * Public factory method to create a new fragment that previews an image.
92 *
93 * Android strongly recommends keep the empty constructor of fragments as the only public
94 * constructor, and
95 * use {@link #setArguments(Bundle)} to set the needed arguments.
96 *
97 * This method hides to client objects the need of doing the construction in two steps.
98 *
99 * @param imageFile An {@link OCFile} to preview as an image in the fragment
100 * @param ignoreFirstSavedState Flag to work around an unexpected behaviour of
101 * {@link FragmentStatePagerAdapter}
102 * ; TODO better solution
103 */
104 public static PreviewImageFragment newInstance(OCFile imageFile, boolean ignoreFirstSavedState,
105 boolean showResizedImage){
106 PreviewImageFragment frag = new PreviewImageFragment();
107 frag.mShowResizedImage = showResizedImage;
108 Bundle args = new Bundle();
109 args.putParcelable(ARG_FILE, imageFile);
110 args.putBoolean(ARG_IGNORE_FIRST, ignoreFirstSavedState);
111 frag.setArguments(args);
112 return frag;
113 }
114
115
116
117 /**
118 * Creates an empty fragment for image previews.
119 *
120 * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically
121 * (for instance, when the device is turned a aside).
122 *
123 * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful
124 * construction
125 */
126 public PreviewImageFragment() {
127 mIgnoreFirstSavedState = false;
128 }
129
130
131 /**
132 * {@inheritDoc}
133 */
134 @Override
135 public void onCreate(Bundle savedInstanceState) {
136 super.onCreate(savedInstanceState);
137 Bundle args = getArguments();
138 setFile((OCFile)args.getParcelable(ARG_FILE));
139 // TODO better in super, but needs to check ALL the class extending FileFragment;
140 // not right now
141
142 mIgnoreFirstSavedState = args.getBoolean(ARG_IGNORE_FIRST);
143 setHasOptionsMenu(true);
144 }
145
146
147 /**
148 * {@inheritDoc}
149 */
150 @Override
151 public View onCreateView(LayoutInflater inflater, ViewGroup container,
152 Bundle savedInstanceState) {
153 super.onCreateView(inflater, container, savedInstanceState);
154 View view = inflater.inflate(R.layout.preview_image_fragment, container, false);
155 mImageView = (TouchImageViewCustom) view.findViewById(R.id.image);
156 mImageView.setVisibility(View.GONE);
157 mImageView.setOnClickListener(new OnClickListener() {
158 @Override
159 public void onClick(View v) {
160 ((PreviewImageActivity) getActivity()).toggleFullScreen();
161 }
162
163 });
164 mMessageView = (TextView)view.findViewById(R.id.message);
165 mMessageView.setVisibility(View.GONE);
166 mProgressWheel = (ProgressBar)view.findViewById(R.id.progressWheel);
167 mProgressWheel.setVisibility(View.VISIBLE);
168 return view;
169 }
170
171 /**
172 * {@inheritDoc}
173 */
174 @Override
175 public void onActivityCreated(Bundle savedInstanceState) {
176 super.onActivityCreated(savedInstanceState);
177 if (savedInstanceState != null) {
178 if (!mIgnoreFirstSavedState) {
179 OCFile file = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE);
180 setFile(file);
181 } else {
182 mIgnoreFirstSavedState = false;
183 }
184 }
185 if (getFile() == null) {
186 throw new IllegalStateException("Instanced with a NULL OCFile");
187 }
188 }
189
190
191 /**
192 * {@inheritDoc}
193 */
194 @Override
195 public void onSaveInstanceState(Bundle outState) {
196 super.onSaveInstanceState(outState);
197 outState.putParcelable(PreviewImageFragment.EXTRA_FILE, getFile());
198 }
199
200
201 @Override
202 public void onStart() {
203 super.onStart();
204 if (getFile() != null) {
205 mImageView.setTag(getFile().getFileId());
206
207 if (mShowResizedImage){
208 Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
209 String.valueOf("r" + getFile().getRemoteId())
210 );
211
212 if (thumbnail != null && !getFile().needsUpdateThumbnail()){
213 mProgressWheel.setVisibility(View.GONE);
214 mImageView.setImageBitmap(thumbnail);
215 mImageView.setVisibility(View.VISIBLE);
216 mBitmap = thumbnail;
217 } else {
218 // generate new Thumbnail
219 if (ThumbnailsCacheManager.cancelPotentialWork(getFile(), mImageView)) {
220 final ThumbnailsCacheManager.ThumbnailGenerationTask task =
221 new ThumbnailsCacheManager.ThumbnailGenerationTask(
222 mImageView, mContainerActivity.getStorageManager(),
223 mContainerActivity.getStorageManager().getAccount(),
224 mProgressWheel);
225 if (thumbnail == null) {
226 thumbnail = ThumbnailsCacheManager.mDefaultImg;
227 }
228 final ThumbnailsCacheManager.AsyncDrawable asyncDrawable =
229 new ThumbnailsCacheManager.AsyncDrawable(
230 MainApp.getAppContext().getResources(),
231 thumbnail,
232 task
233 );
234 mImageView.setImageDrawable(asyncDrawable);
235 task.execute(getFile(), false);
236 }
237 }
238 } else {
239 mLoadBitmapTask = new LoadBitmapTask(mImageView, mMessageView, mProgressWheel);
240 mLoadBitmapTask.execute(getFile());
241 }
242 }
243 }
244
245
246 @Override
247 public void onStop() {
248 Log_OC.d(TAG, "onStop starts");
249 if (mLoadBitmapTask != null) {
250 mLoadBitmapTask.cancel(true);
251 mLoadBitmapTask = null;
252 }
253 super.onStop();
254 }
255
256 /**
257 * {@inheritDoc}
258 */
259 @Override
260 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
261 super.onCreateOptionsMenu(menu, inflater);
262 inflater.inflate(R.menu.file_actions_menu, menu);
263 }
264
265 /**
266 * {@inheritDoc}
267 */
268 @Override
269 public void onPrepareOptionsMenu(Menu menu) {
270 super.onPrepareOptionsMenu(menu);
271
272 if (mContainerActivity.getStorageManager() != null) {
273 // Update the file
274 setFile(mContainerActivity.getStorageManager().getFileById(getFile().getFileId()));
275
276 FileMenuFilter mf = new FileMenuFilter(
277 getFile(),
278 mContainerActivity.getStorageManager().getAccount(),
279 mContainerActivity,
280 getActivity()
281 );
282 mf.filter(menu);
283 }
284
285 // additional restriction for this fragment
286 // TODO allow renaming in PreviewImageFragment
287 MenuItem item = menu.findItem(R.id.action_rename_file);
288 if (item != null) {
289 item.setVisible(false);
290 item.setEnabled(false);
291 }
292
293 // additional restriction for this fragment
294 // TODO allow refresh file in PreviewImageFragment
295 item = menu.findItem(R.id.action_sync_file);
296 if (item != null) {
297 item.setVisible(false);
298 item.setEnabled(false);
299 }
300
301 // additional restriction for this fragment
302 item = menu.findItem(R.id.action_move);
303 if (item != null) {
304 item.setVisible(false);
305 item.setEnabled(false);
306 }
307
308 // additional restriction for this fragment
309 item = menu.findItem(R.id.action_copy);
310 if (item != null) {
311 item.setVisible(false);
312 item.setEnabled(false);
313 }
314
315 }
316
317
318 /**
319 * {@inheritDoc}
320 */
321 @Override
322 public boolean onOptionsItemSelected(MenuItem item) {
323 switch (item.getItemId()) {
324 case R.id.action_share_file: {
325 mContainerActivity.getFileOperationsHelper().shareFileWithLink(getFile());
326 return true;
327 }
328 case R.id.action_unshare_file: {
329 mContainerActivity.getFileOperationsHelper().unshareFileWithLink(getFile());
330 return true;
331 }
332 case R.id.action_open_file_with: {
333 openFile();
334 return true;
335 }
336 case R.id.action_remove_file: {
337 RemoveFileDialogFragment dialog = RemoveFileDialogFragment.newInstance(getFile());
338 dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
339 return true;
340 }
341 case R.id.action_see_details: {
342 seeDetails();
343 return true;
344 }
345 case R.id.action_send_file: {
346 if (getFile().isImage() && !getFile().isDown()){
347 mContainerActivity.getFileOperationsHelper().sendCachedImage(getFile());
348 return true;
349 } else {
350 mContainerActivity.getFileOperationsHelper().sendDownloadedFile(getFile());
351 return true;
352 }
353 }
354 case R.id.action_sync_file: {
355 mContainerActivity.getFileOperationsHelper().syncFile(getFile());
356 return true;
357 }
358 case R.id.action_favorite_file:{
359 mContainerActivity.getFileOperationsHelper().toggleFavorite(getFile(), true);
360 return true;
361 }
362 case R.id.action_unfavorite_file:{
363 mContainerActivity.getFileOperationsHelper().toggleFavorite(getFile(), false);
364 return true;
365 }
366 default:
367 return false;
368 }
369 }
370
371
372 private void seeDetails() {
373 mContainerActivity.showDetails(getFile());
374 }
375
376
377 @Override
378 public void onResume() {
379 super.onResume();
380 }
381
382
383 @Override
384 public void onPause() {
385 super.onPause();
386 }
387
388 @Override
389 public void onDestroy() {
390 if (mBitmap != null) {
391 mBitmap.recycle();
392 System.gc();
393 // putting this in onStop() is just the same; the fragment is always destroyed by
394 // {@link FragmentStatePagerAdapter} when the fragment in swiped further than the
395 // valid offscreen distance, and onStop() is never called before than that
396 }
397 super.onDestroy();
398 }
399
400
401 /**
402 * Opens the previewed image with an external application.
403 */
404 private void openFile() {
405 mContainerActivity.getFileOperationsHelper().openFile(getFile());
406 finish();
407 }
408
409
410 private class LoadBitmapTask extends AsyncTask<OCFile, Void, LoadImage> {
411
412 /**
413 * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
414 *
415 * Using a weak reference will avoid memory leaks if the target ImageView is retired from
416 * memory before the load finishes.
417 */
418 private final WeakReference<ImageViewCustom> mImageViewRef;
419
420 /**
421 * Weak reference to the target {@link TextView} where error messages will be written.
422 *
423 * Using a weak reference will avoid memory leaks if the target ImageView is retired from
424 * memory before the load finishes.
425 */
426 private final WeakReference<TextView> mMessageViewRef;
427
428
429 /**
430 * Weak reference to the target {@link ProgressBar} shown while the load is in progress.
431 *
432 * Using a weak reference will avoid memory leaks if the target ImageView is retired from
433 * memory before the load finishes.
434 */
435 private final WeakReference<ProgressBar> mProgressWheelRef;
436
437
438 /**
439 * Error message to show when a load fails
440 */
441 private int mErrorMessageId;
442
443
444 /**
445 * Constructor.
446 *
447 * @param imageView Target {@link ImageView} where the bitmap will be loaded into.
448 */
449 public LoadBitmapTask(ImageViewCustom imageView, TextView messageView,
450 ProgressBar progressWheel) {
451 mImageViewRef = new WeakReference<ImageViewCustom>(imageView);
452 mMessageViewRef = new WeakReference<TextView>(messageView);
453 mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
454 }
455
456 @Override
457 protected LoadImage doInBackground(OCFile... params) {
458 Bitmap result = null;
459 if (params.length != 1) return null;
460 OCFile ocFile = params[0];
461 String storagePath = ocFile.getStoragePath();
462 try {
463
464 int maxDownScale = 3; // could be a parameter passed to doInBackground(...)
465 Point screenSize = DisplayUtils.getScreenSize(getActivity());
466 int minWidth = screenSize.x;
467 int minHeight = screenSize.y;
468 for (int i = 0; i < maxDownScale && result == null; i++) {
469 if (isCancelled()) return null;
470 try {
471 result = BitmapUtils.decodeSampledBitmapFromFile(storagePath, minWidth,
472 minHeight);
473
474 if (isCancelled()) return new LoadImage(result, ocFile);
475
476 if (result == null) {
477 mErrorMessageId = R.string.preview_image_error_unknown_format;
478 Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
479 break;
480 } else {
481 // Rotate image, obeying exif tag.
482 result = BitmapUtils.rotateImage(result, storagePath);
483 }
484
485 } catch (OutOfMemoryError e) {
486 mErrorMessageId = R.string.common_error_out_memory;
487 if (i < maxDownScale - 1) {
488 Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
489 " ; scaling down");
490 minWidth = minWidth / 2;
491 minHeight = minHeight / 2;
492
493 } else {
494 Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
495 " ; failing");
496 }
497 if (result != null) {
498 result.recycle();
499 }
500 result = null;
501 }
502 }
503
504 } catch (NoSuchFieldError e) {
505 mErrorMessageId = R.string.common_error_unknown;
506 Log_OC.e(TAG, "Error from access to unexisting field despite protection; file "
507 + storagePath, e);
508
509 } catch (Throwable t) {
510 mErrorMessageId = R.string.common_error_unknown;
511 Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
512
513 }
514
515 return new LoadImage(result, ocFile);
516 }
517
518 @Override
519 protected void onCancelled(LoadImage result) {
520 if (result != null && result.bitmap != null) {
521 result.bitmap.recycle();
522 }
523 }
524
525 @Override
526 protected void onPostExecute(LoadImage result) {
527 hideProgressWheel();
528 if (result.bitmap != null) {
529 showLoadedImage(result);
530 }
531 else {
532 showErrorMessage();
533 }
534 if (result.bitmap != null && mBitmap != result.bitmap) {
535 // unused bitmap, release it! (just in case)
536 result.bitmap.recycle();
537 }
538 }
539
540 @SuppressLint("InlinedApi")
541 private void showLoadedImage(LoadImage result) {
542 final ImageViewCustom imageView = mImageViewRef.get();
543 Bitmap bitmap = result.bitmap;
544 if (imageView != null) {
545 Log_OC.d(TAG, "Showing image with resolution " + bitmap.getWidth() + "x" +
546 bitmap.getHeight());
547
548 if (result.ocFile.getMimetype().equalsIgnoreCase("image/png")){
549 Drawable backrepeat = getResources().getDrawable(R.drawable.backrepeat);
550 imageView.setBackground(backrepeat);
551 }
552
553 if (result.ocFile.getMimetype().equalsIgnoreCase("image/gif")){
554 imageView.setGifImage(result.ocFile);
555 } else {
556 imageView.setImageBitmap(bitmap);
557 }
558
559 imageView.setVisibility(View.VISIBLE);
560 mBitmap = bitmap; // needs to be kept for recycling when not useful
561 }
562
563 final TextView messageView = mMessageViewRef.get();
564 if (messageView != null) {
565 messageView.setVisibility(View.GONE);
566 } // else , silently finish, the fragment was destroyed
567 }
568
569 private void showErrorMessage() {
570 final ImageView imageView = mImageViewRef.get();
571 if (imageView != null) {
572 // shows the default error icon
573 imageView.setVisibility(View.VISIBLE);
574 } // else , silently finish, the fragment was destroyed
575
576 final TextView messageView = mMessageViewRef.get();
577 if (messageView != null) {
578 messageView.setText(mErrorMessageId);
579 messageView.setVisibility(View.VISIBLE);
580 } // else , silently finish, the fragment was destroyed
581 }
582
583 private void hideProgressWheel() {
584 final ProgressBar progressWheel = mProgressWheelRef.get();
585 if (progressWheel != null) {
586 progressWheel.setVisibility(View.GONE);
587 }
588 }
589
590 }
591
592 /**
593 * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment}
594 * to be previewed.
595 *
596 * @param file File to test if can be previewed.
597 * @return 'True' if the file can be handled by the fragment.
598 */
599 public static boolean canBePreviewed(OCFile file) {
600 return (file != null && file.isImage());
601 }
602
603
604 /**
605 * Finishes the preview
606 */
607 private void finish() {
608 Activity container = getActivity();
609 container.finish();
610 }
611
612 public TouchImageViewCustom getImageView() {
613 return mImageView;
614 }
615
616 private class LoadImage {
617 private Bitmap bitmap;
618 private OCFile ocFile;
619
620 public LoadImage(Bitmap bitmap, OCFile ocFile){
621 this.bitmap = bitmap;
622 this.ocFile = ocFile;
623 }
624
625 }
626
627 }