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