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