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