Refactoring relationship between FileActivities and FileFragments (WIP)
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / preview / PreviewImageFragment.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012-2013 ownCloud Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2,
6 * as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17 package com.owncloud.android.ui.preview;
18
19 import java.lang.ref.WeakReference;
20 import java.util.ArrayList;
21 import java.util.List;
22
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.BitmapFactory;
29 import android.graphics.BitmapFactory.Options;
30 import android.graphics.Point;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.support.v4.app.FragmentStatePagerAdapter;
34 import android.view.Display;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.View.OnTouchListener;
38 import android.view.ViewGroup;
39 import android.widget.ImageView;
40 import android.widget.ProgressBar;
41 import android.widget.TextView;
42
43 import com.actionbarsherlock.view.Menu;
44 import com.actionbarsherlock.view.MenuInflater;
45 import com.actionbarsherlock.view.MenuItem;
46 import com.owncloud.android.R;
47 import com.owncloud.android.datamodel.FileDataStorageManager;
48 import com.owncloud.android.datamodel.OCFile;
49 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
50 import com.owncloud.android.ui.fragment.FileFragment;
51 import com.owncloud.android.utils.Log_OC;
52
53
54 /**
55 * This fragment shows a preview of a downloaded image.
56 *
57 * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}.
58 *
59 * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too.
60 *
61 * @author David A. Velasco
62 */
63 public class PreviewImageFragment extends FileFragment implements
64 ConfirmationDialogFragment.ConfirmationDialogFragmentListener {
65 public static final String EXTRA_FILE = "FILE";
66 public static final String EXTRA_ACCOUNT = "ACCOUNT";
67
68 private View mView;
69 private Account mAccount;
70 private ImageView 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 FileFragment.ContainerActivity mContainerActivity;
81
82
83 /**
84 * Creates a fragment to preview an image.
85 *
86 * When 'imageFile' or 'ocAccount' are null
87 *
88 * @param imageFile An {@link OCFile} to preview as an image in the fragment
89 * @param ocAccount An ownCloud account; needed to start downloads
90 * @param ignoreFirstSavedState Flag to work around an unexpected behaviour of {@link FragmentStatePagerAdapter}; TODO better solution
91 */
92 public PreviewImageFragment(OCFile fileToDetail, Account ocAccount, boolean ignoreFirstSavedState) {
93 super(fileToDetail);
94 mAccount = ocAccount;
95 mIgnoreFirstSavedState = ignoreFirstSavedState;
96 }
97
98
99 /**
100 * Creates an empty fragment for image previews.
101 *
102 * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the device is turned a aside).
103 *
104 * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction
105 */
106 public PreviewImageFragment() {
107 super();
108 mAccount = null;
109 mIgnoreFirstSavedState = false;
110 }
111
112
113 /**
114 * {@inheritDoc}
115 */
116 @Override
117 public void onCreate(Bundle savedInstanceState) {
118 super.onCreate(savedInstanceState);
119 setHasOptionsMenu(true);
120 }
121
122
123 /**
124 * {@inheritDoc}
125 */
126 @Override
127 public View onCreateView(LayoutInflater inflater, ViewGroup container,
128 Bundle savedInstanceState) {
129 super.onCreateView(inflater, container, savedInstanceState);
130 mView = inflater.inflate(R.layout.preview_image_fragment, container, false);
131 mImageView = (ImageView)mView.findViewById(R.id.image);
132 mImageView.setVisibility(View.GONE);
133 mView.setOnTouchListener((OnTouchListener)getActivity());
134 mMessageView = (TextView)mView.findViewById(R.id.message);
135 mMessageView.setVisibility(View.GONE);
136 mProgressWheel = (ProgressBar)mView.findViewById(R.id.progressWheel);
137 mProgressWheel.setVisibility(View.VISIBLE);
138 return mView;
139 }
140
141
142 /**
143 * {@inheritDoc}
144 */
145 @Override
146 public void onAttach(Activity activity) {
147 super.onAttach(activity);
148 if (!(activity instanceof OnTouchListener)) {
149 throw new ClassCastException(activity.toString() +
150 " must implement " + OnTouchListener.class.getSimpleName());
151 }
152 }
153
154
155 /**
156 * {@inheritDoc}
157 */
158 @Override
159 public void onActivityCreated(Bundle savedInstanceState) {
160 super.onActivityCreated(savedInstanceState);
161 if (savedInstanceState != null) {
162 if (!mIgnoreFirstSavedState) {
163 OCFile file = (OCFile)savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE);
164 mAccount = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_ACCOUNT);
165
166 // Update the file
167 if (mAccount!= null) {
168 OCFile updatedFile = mContainerActivity.getStorageManager().getFileByPath(file.getRemotePath());
169 if (updatedFile != null) {
170 setFile(updatedFile);
171 } else {
172 setFile(file);
173 }
174 } else {
175 setFile(file);
176 }
177
178 } else {
179 mIgnoreFirstSavedState = false;
180 }
181 }
182 if (getFile() == null) {
183 throw new IllegalStateException("Instanced with a NULL OCFile");
184 }
185 if (mAccount == null) {
186 throw new IllegalStateException("Instanced with a NULL ownCloud Account");
187 }
188 if (!getFile().isDown()) {
189 throw new IllegalStateException("There is no local file to preview");
190 }
191 }
192
193
194 /**
195 * {@inheritDoc}
196 */
197 @Override
198 public void onSaveInstanceState(Bundle outState) {
199 super.onSaveInstanceState(outState);
200 outState.putParcelable(PreviewImageFragment.EXTRA_FILE, getFile());
201 outState.putParcelable(PreviewImageFragment.EXTRA_ACCOUNT, mAccount);
202 }
203
204
205 @Override
206 public void onStart() {
207 super.onStart();
208 if (getFile() != null) {
209 BitmapLoader bl = new BitmapLoader(mImageView, mMessageView, mProgressWheel);
210 bl.execute(new String[]{getFile().getStoragePath()});
211 }
212 }
213
214
215 /**
216 * {@inheritDoc}
217 */
218 @Override
219 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
220 super.onCreateOptionsMenu(menu, inflater);
221
222 inflater.inflate(R.menu.file_actions_menu, menu);
223 List<Integer> toHide = new ArrayList<Integer>();
224
225 MenuItem item = null;
226 toHide.add(R.id.action_cancel_download);
227 toHide.add(R.id.action_cancel_upload);
228 toHide.add(R.id.action_download_file);
229 toHide.add(R.id.action_rename_file); // by now
230
231 // Options shareLink
232 if (!getFile().isShareByLink()) {
233 toHide.add(R.id.action_unshare_file);
234 }
235
236 // Send file
237 boolean sendEnabled = getString(R.string.send_files_to_other_apps).equalsIgnoreCase("on");
238 if (!sendEnabled) {
239 toHide.add(R.id.action_send_file);
240 }
241
242 for (int i : toHide) {
243 item = menu.findItem(i);
244 if (item != null) {
245 item.setVisible(false);
246 item.setEnabled(false);
247 }
248 }
249
250 }
251
252 /**
253 * {@inheritDoc}
254 */
255 @Override
256 public void onPrepareOptionsMenu(Menu menu) {
257 super.onPrepareOptionsMenu(menu);
258
259 MenuItem item = menu.findItem(R.id.action_unshare_file);
260 // Options shareLink
261 if (!getFile().isShareByLink()) {
262 item.setVisible(false);
263 item.setEnabled(false);
264 } else {
265 item.setVisible(true);
266 item.setEnabled(true);
267 }
268
269 }
270
271
272
273 /**
274 * {@inheritDoc}
275 */
276 @Override
277 public boolean onOptionsItemSelected(MenuItem item) {
278 switch (item.getItemId()) {
279 case R.id.action_share_file: {
280 mContainerActivity.getFileOperationsHelper().shareFileWithLink(getFile());
281 return true;
282 }
283 case R.id.action_unshare_file: {
284 mContainerActivity.getFileOperationsHelper().unshareFileWithLink(getFile());
285 return true;
286 }
287 case R.id.action_open_file_with: {
288 openFile();
289 return true;
290 }
291 case R.id.action_remove_file: {
292 removeFile();
293 return true;
294 }
295 case R.id.action_see_details: {
296 seeDetails();
297 return true;
298 }
299 case R.id.action_send_file: {
300 mContainerActivity.getFileOperationsHelper().sendDownloadedFile(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
327 @Override
328 public void onDestroy() {
329 super.onDestroy();
330 if (mBitmap != null) {
331 mBitmap.recycle();
332 }
333 }
334
335
336 /**
337 * Opens the previewed image with an external application.
338 */
339 private void openFile() {
340 mContainerActivity.getFileOperationsHelper().openFile(getFile());
341 finish();
342 }
343
344
345 /**
346 * Starts a the removal of the previewed file.
347 *
348 * Shows a confirmation dialog. The action continues in {@link #onConfirmation(String)} , {@link #onNeutral(String)} or {@link #onCancel(String)},
349 * depending upon the user selection in the dialog.
350 */
351 private void removeFile() {
352 ConfirmationDialogFragment confDialog = ConfirmationDialogFragment.newInstance(
353 R.string.confirmation_remove_alert,
354 new String[]{getFile().getFileName()},
355 R.string.confirmation_remove_remote_and_local,
356 R.string.confirmation_remove_local,
357 R.string.common_cancel);
358 confDialog.setOnConfirmationListener(this);
359 confDialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
360 }
361
362
363 /**
364 * Performs the removal of the previewed file, both locally and in the server.
365 */
366 @Override
367 public void onConfirmation(String callerTag) {
368 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
369 if (storageManager.getFileById(getFile().getFileId()) != null) { // check that the file is still there;
370 mContainerActivity.getFileOperationsHelper().removeFile(getFile(), true);
371 }
372 }
373
374
375 /**
376 * Removes the file from local storage
377 */
378 @Override
379 public void onNeutral(String callerTag) {
380 OCFile file = getFile();
381 mContainerActivity.getStorageManager().removeFile(file, false, true); // TODO perform in background task / new thread
382 finish();
383 }
384
385 /**
386 * User cancelled the removal action.
387 */
388 @Override
389 public void onCancel(String callerTag) {
390 // nothing to do here
391 }
392
393
394 private class BitmapLoader extends AsyncTask<String, Void, Bitmap> {
395
396 /**
397 * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
398 *
399 * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
400 */
401 private final WeakReference<ImageView> mImageViewRef;
402
403 /**
404 * Weak reference to the target {@link TextView} where error messages will be written.
405 *
406 * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
407 */
408 private final WeakReference<TextView> mMessageViewRef;
409
410
411 /**
412 * Weak reference to the target {@link Progressbar} shown while the load is in progress.
413 *
414 * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
415 */
416 private final WeakReference<ProgressBar> mProgressWheelRef;
417
418
419 /**
420 * Error message to show when a load fails
421 */
422 private int mErrorMessageId;
423
424
425 /**
426 * Constructor.
427 *
428 * @param imageView Target {@link ImageView} where the bitmap will be loaded into.
429 */
430 public BitmapLoader(ImageView imageView, TextView messageView, ProgressBar progressWheel) {
431 mImageViewRef = new WeakReference<ImageView>(imageView);
432 mMessageViewRef = new WeakReference<TextView>(messageView);
433 mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
434 }
435
436
437 @SuppressWarnings("deprecation")
438 @SuppressLint({ "NewApi", "NewApi", "NewApi" }) // to avoid Lint errors since Android SDK r20
439 @Override
440 protected Bitmap doInBackground(String... params) {
441 Bitmap result = null;
442 if (params.length != 1) return result;
443 String storagePath = params[0];
444 try {
445 // set desired options that will affect the size of the bitmap
446 BitmapFactory.Options options = new Options();
447 options.inScaled = true;
448 options.inPurgeable = true;
449 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
450 options.inPreferQualityOverSpeed = false;
451 }
452 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
453 options.inMutable = false;
454 }
455 // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType
456 options.inJustDecodeBounds = true;
457 BitmapFactory.decodeFile(storagePath, options);
458
459 int width = options.outWidth;
460 int height = options.outHeight;
461 int scale = 1;
462
463 Display display = getActivity().getWindowManager().getDefaultDisplay();
464 Point size = new Point();
465 int screenWidth;
466 int screenHeight;
467 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) {
468 display.getSize(size);
469 screenWidth = size.x;
470 screenHeight = size.y;
471 } else {
472 screenWidth = display.getWidth();
473 screenHeight = display.getHeight();
474 }
475
476 if (width > screenWidth) {
477 // second try to scale down the image , this time depending upon the screen size
478 scale = (int) Math.floor((float)width / screenWidth);
479 }
480 if (height > screenHeight) {
481 scale = Math.max(scale, (int) Math.floor((float)height / screenHeight));
482 }
483 options.inSampleSize = scale;
484
485 // really load the bitmap
486 options.inJustDecodeBounds = false; // the next decodeFile call will be real
487 result = BitmapFactory.decodeFile(storagePath, options);
488 //Log_OC.d(TAG, "Image loaded - width: " + options.outWidth + ", loaded height: " + options.outHeight);
489
490 if (result == null) {
491 mErrorMessageId = R.string.preview_image_error_unknown_format;
492 Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
493 }
494
495 } catch (OutOfMemoryError e) {
496 mErrorMessageId = R.string.preview_image_error_unknown_format;
497 Log_OC.e(TAG, "Out of memory occured for file " + storagePath, e);
498
499 } catch (NoSuchFieldError e) {
500 mErrorMessageId = R.string.common_error_unknown;
501 Log_OC.e(TAG, "Error from access to unexisting field despite protection; file " + storagePath, e);
502
503 } catch (Throwable t) {
504 mErrorMessageId = R.string.common_error_unknown;
505 Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
506
507 }
508 return result;
509 }
510
511 @Override
512 protected void onPostExecute(Bitmap result) {
513 hideProgressWheel();
514 if (result != null) {
515 showLoadedImage(result);
516 } else {
517 showErrorMessage();
518 }
519 }
520
521 private void showLoadedImage(Bitmap result) {
522 if (mImageViewRef != null) {
523 final ImageView imageView = mImageViewRef.get();
524 if (imageView != null) {
525 imageView.setImageBitmap(result);
526 imageView.setVisibility(View.VISIBLE);
527 mBitmap = result;
528 } // else , silently finish, the fragment was destroyed
529 }
530 if (mMessageViewRef != null) {
531 final TextView messageView = mMessageViewRef.get();
532 if (messageView != null) {
533 messageView.setVisibility(View.GONE);
534 } // else , silently finish, the fragment was destroyed
535 }
536 }
537
538 private void showErrorMessage() {
539 if (mImageViewRef != null) {
540 final ImageView imageView = mImageViewRef.get();
541 if (imageView != null) {
542 // shows the default error icon
543 imageView.setVisibility(View.VISIBLE);
544 } // else , silently finish, the fragment was destroyed
545 }
546 if (mMessageViewRef != null) {
547 final TextView messageView = mMessageViewRef.get();
548 if (messageView != null) {
549 messageView.setText(mErrorMessageId);
550 messageView.setVisibility(View.VISIBLE);
551 } // else , silently finish, the fragment was destroyed
552 }
553 }
554
555 private void hideProgressWheel() {
556 if (mProgressWheelRef != null) {
557 final ProgressBar progressWheel = mProgressWheelRef.get();
558 if (progressWheel != null) {
559 progressWheel.setVisibility(View.GONE);
560 }
561 }
562 }
563
564 }
565
566 /**
567 * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} to be previewed.
568 *
569 * @param file File to test if can be previewed.
570 * @return 'True' if the file can be handled by the fragment.
571 */
572 public static boolean canBePreviewed(OCFile file) {
573 return (file != null && file.isImage());
574 }
575
576
577 /**
578 * Finishes the preview
579 */
580 private void finish() {
581 Activity container = getActivity();
582 container.finish();
583 }
584
585
586 }