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