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