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