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