check available beta version
[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_unshare_file: {
336 mContainerActivity.getFileOperationsHelper().unshareFileWithLink(getFile());
337 return true;
338 }
339 case R.id.action_open_file_with: {
340 openFile();
341 return true;
342 }
343 case R.id.action_remove_file: {
344 RemoveFileDialogFragment dialog = RemoveFileDialogFragment.newInstance(getFile());
345 dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
346 return true;
347 }
348 case R.id.action_see_details: {
349 seeDetails();
350 return true;
351 }
352 case R.id.action_send_file: {
353 if (getFile().isImage() && !getFile().isDown()){
354 mContainerActivity.getFileOperationsHelper().sendCachedImage(getFile());
355 return true;
356 } else {
357 mContainerActivity.getFileOperationsHelper().sendDownloadedFile(getFile());
358 return true;
359 }
360 }
361 case R.id.action_sync_file: {
362 mContainerActivity.getFileOperationsHelper().syncFile(getFile());
363 return true;
364 }
365 case R.id.action_favorite_file:{
366 mContainerActivity.getFileOperationsHelper().toggleFavorite(getFile(), true);
367 return true;
368 }
369 case R.id.action_unfavorite_file:{
370 mContainerActivity.getFileOperationsHelper().toggleFavorite(getFile(), false);
371 return true;
372 }
373 case R.id.action_set_as_wallpaper:{
374 mContainerActivity.getFileOperationsHelper().setPictureAs(getFile());
375 return true;
376 }
377 default:
378 return false;
379 }
380 }
381
382
383 private void seeDetails() {
384 mContainerActivity.showDetails(getFile());
385 }
386
387
388 @Override
389 public void onResume() {
390 super.onResume();
391 }
392
393
394 @Override
395 public void onPause() {
396 super.onPause();
397 }
398
399 @Override
400 public void onDestroy() {
401 if (mBitmap != null) {
402 mBitmap.recycle();
403 System.gc();
404 // putting this in onStop() is just the same; the fragment is always destroyed by
405 // {@link FragmentStatePagerAdapter} when the fragment in swiped further than the
406 // valid offscreen distance, and onStop() is never called before than that
407 }
408 super.onDestroy();
409 }
410
411
412 /**
413 * Opens the previewed image with an external application.
414 */
415 private void openFile() {
416 mContainerActivity.getFileOperationsHelper().openFile(getFile());
417 finish();
418 }
419
420
421 private class LoadBitmapTask extends AsyncTask<OCFile, Void, LoadImage> {
422
423 /**
424 * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
425 *
426 * Using a weak reference will avoid memory leaks if the target ImageView is retired from
427 * memory before the load finishes.
428 */
429 private final WeakReference<ImageViewCustom> mImageViewRef;
430
431 /**
432 * Weak reference to the target {@link TextView} where error messages will be written.
433 *
434 * Using a weak reference will avoid memory leaks if the target ImageView is retired from
435 * memory before the load finishes.
436 */
437 private final WeakReference<TextView> mMessageViewRef;
438
439
440 /**
441 * Weak reference to the target {@link ProgressBar} shown while the load is in progress.
442 *
443 * Using a weak reference will avoid memory leaks if the target ImageView is retired from
444 * memory before the load finishes.
445 */
446 private final WeakReference<ProgressBar> mProgressWheelRef;
447
448
449 /**
450 * Error message to show when a load fails
451 */
452 private int mErrorMessageId;
453
454
455 /**
456 * Constructor.
457 *
458 * @param imageView Target {@link ImageView} where the bitmap will be loaded into.
459 */
460 public LoadBitmapTask(ImageViewCustom imageView, TextView messageView,
461 ProgressBar progressWheel) {
462 mImageViewRef = new WeakReference<ImageViewCustom>(imageView);
463 mMessageViewRef = new WeakReference<TextView>(messageView);
464 mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
465 }
466
467 @Override
468 protected LoadImage doInBackground(OCFile... params) {
469 Bitmap result = null;
470 if (params.length != 1) return null;
471 OCFile ocFile = params[0];
472 String storagePath = ocFile.getStoragePath();
473 try {
474
475 int maxDownScale = 3; // could be a parameter passed to doInBackground(...)
476 Point screenSize = DisplayUtils.getScreenSize(getActivity());
477 int minWidth = screenSize.x;
478 int minHeight = screenSize.y;
479 for (int i = 0; i < maxDownScale && result == null; i++) {
480 if (isCancelled()) return null;
481 try {
482 result = BitmapUtils.decodeSampledBitmapFromFile(storagePath, minWidth,
483 minHeight);
484
485 if (isCancelled()) return new LoadImage(result, ocFile);
486
487 if (result == null) {
488 mErrorMessageId = R.string.preview_image_error_unknown_format;
489 Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
490 break;
491 } else {
492 // Rotate image, obeying exif tag.
493 result = BitmapUtils.rotateImage(result, storagePath);
494 }
495
496 } catch (OutOfMemoryError e) {
497 mErrorMessageId = R.string.common_error_out_memory;
498 if (i < maxDownScale - 1) {
499 Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
500 " ; scaling down");
501 minWidth = minWidth / 2;
502 minHeight = minHeight / 2;
503
504 } else {
505 Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
506 " ; failing");
507 }
508 if (result != null) {
509 result.recycle();
510 }
511 result = null;
512 }
513 }
514
515 } catch (NoSuchFieldError e) {
516 mErrorMessageId = R.string.common_error_unknown;
517 Log_OC.e(TAG, "Error from access to unexisting field despite protection; file "
518 + storagePath, e);
519
520 } catch (Throwable t) {
521 mErrorMessageId = R.string.common_error_unknown;
522 Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
523
524 }
525
526 return new LoadImage(result, ocFile);
527 }
528
529 @Override
530 protected void onCancelled(LoadImage result) {
531 if (result != null && result.bitmap != null) {
532 result.bitmap.recycle();
533 }
534 }
535
536 @Override
537 protected void onPostExecute(LoadImage result) {
538 hideProgressWheel();
539 if (result.bitmap != null) {
540 showLoadedImage(result);
541 }
542 else {
543 showErrorMessage();
544 }
545 if (result.bitmap != null && mBitmap != result.bitmap) {
546 // unused bitmap, release it! (just in case)
547 result.bitmap.recycle();
548 }
549 }
550
551 @SuppressLint("InlinedApi")
552 private void showLoadedImage(LoadImage result) {
553 final ImageViewCustom imageView = mImageViewRef.get();
554 Bitmap bitmap = result.bitmap;
555 if (imageView != null) {
556 Log_OC.d(TAG, "Showing image with resolution " + bitmap.getWidth() + "x" +
557 bitmap.getHeight());
558
559 if (result.ocFile.getMimetype().equalsIgnoreCase("image/png")){
560 Drawable backrepeat = getResources().getDrawable(R.drawable.backrepeat);
561 imageView.setBackground(backrepeat);
562 }
563
564 if (result.ocFile.getMimetype().equalsIgnoreCase("image/gif")){
565 imageView.setGifImage(result.ocFile);
566 } else {
567 imageView.setImageBitmap(bitmap);
568 }
569
570 imageView.setVisibility(View.VISIBLE);
571 mBitmap = bitmap; // needs to be kept for recycling when not useful
572 }
573
574 final TextView messageView = mMessageViewRef.get();
575 if (messageView != null) {
576 messageView.setVisibility(View.GONE);
577 } // else , silently finish, the fragment was destroyed
578 }
579
580 private void showErrorMessage() {
581 final ImageView imageView = mImageViewRef.get();
582 if (imageView != null) {
583 // shows the default error icon
584 imageView.setVisibility(View.VISIBLE);
585 } // else , silently finish, the fragment was destroyed
586
587 final TextView messageView = mMessageViewRef.get();
588 if (messageView != null) {
589 messageView.setText(mErrorMessageId);
590 messageView.setVisibility(View.VISIBLE);
591 } // else , silently finish, the fragment was destroyed
592 }
593
594 private void hideProgressWheel() {
595 final ProgressBar progressWheel = mProgressWheelRef.get();
596 if (progressWheel != null) {
597 progressWheel.setVisibility(View.GONE);
598 }
599 }
600
601 }
602
603 /**
604 * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment}
605 * to be previewed.
606 *
607 * @param file File to test if can be previewed.
608 * @return 'True' if the file can be handled by the fragment.
609 */
610 public static boolean canBePreviewed(OCFile file) {
611 return (file != null && file.isImage());
612 }
613
614
615 /**
616 * Finishes the preview
617 */
618 private void finish() {
619 Activity container = getActivity();
620 container.finish();
621 }
622
623 public TouchImageViewCustom getImageView() {
624 return mImageView;
625 }
626
627 private class LoadImage {
628 private Bitmap bitmap;
629 private OCFile ocFile;
630
631 public LoadImage(Bitmap bitmap, OCFile ocFile){
632 this.bitmap = bitmap;
633 this.ocFile = ocFile;
634 }
635
636 }
637
638 }