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