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