67cac5f3f38e15e79dbde240913376f06276bdf5
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / preview / PreviewImageFragment.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012-2014 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
21 import android.accounts.Account;
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.os.AsyncTask;
27 import android.os.Build;
28 import android.os.Bundle;
29 import android.support.v4.app.FragmentStatePagerAdapter;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.View.OnClickListener;
33 import android.view.ViewGroup;
34 import android.widget.ImageView;
35 import android.widget.ProgressBar;
36 import android.widget.TextView;
37
38 import com.actionbarsherlock.view.Menu;
39 import com.actionbarsherlock.view.MenuInflater;
40 import com.actionbarsherlock.view.MenuItem;
41 import com.ortiz.touch.TouchImageView;
42 import com.owncloud.android.R;
43 import com.owncloud.android.datamodel.OCFile;
44 import com.owncloud.android.files.FileMenuFilter;
45 import com.owncloud.android.lib.common.utils.Log_OC;
46 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
47 import com.owncloud.android.ui.dialog.RemoveFileDialogFragment;
48 import com.owncloud.android.ui.fragment.FileFragment;
49
50
51 /**
52 * This fragment shows a preview of a downloaded image.
53 *
54 * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}.
55 *
56 * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too.
57 *
58 * @author David A. Velasco
59 */
60 public class PreviewImageFragment extends FileFragment {
61
62 private static final boolean IS_HONEYCOMB_OR_HIGHER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
63
64 public static final String EXTRA_FILE = "FILE";
65 public static final String EXTRA_ACCOUNT = "ACCOUNT";
66
67 public static final int MAX_OPENGL_TEXTURE_WIDTH = 2048;
68 public static final int MAX_OPENGL_TEXTURE_HEIGHT = 2048;
69
70 private View mView;
71 private Account mAccount;
72 private TouchImageView mImageView;
73 private TextView mMessageView;
74 private ProgressBar mProgressWheel;
75
76 public Bitmap mBitmap = null;
77
78 private static final String TAG = PreviewImageFragment.class.getSimpleName();
79
80 private boolean mIgnoreFirstSavedState;
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 = (TouchImageView) mView.findViewById(R.id.image);
132 mImageView.setVisibility(View.GONE);
133 mImageView.setOnClickListener(new OnClickListener() {
134 @Override
135 public void onClick(View v) {
136 ((PreviewImageActivity) getActivity()).toggleFullScreen();
137 }
138
139 });
140 mMessageView = (TextView)mView.findViewById(R.id.message);
141 mMessageView.setVisibility(View.GONE);
142 mProgressWheel = (ProgressBar)mView.findViewById(R.id.progressWheel);
143 mProgressWheel.setVisibility(View.VISIBLE);
144 return mView;
145 }
146
147 /**
148 * {@inheritDoc}
149 */
150 @Override
151 public void onActivityCreated(Bundle savedInstanceState) {
152 super.onActivityCreated(savedInstanceState);
153 if (savedInstanceState != null) {
154 if (!mIgnoreFirstSavedState) {
155 OCFile file = (OCFile)savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE);
156 setFile(file);
157 mAccount = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_ACCOUNT);
158 } else {
159 mIgnoreFirstSavedState = false;
160 }
161 }
162 if (getFile() == null) {
163 throw new IllegalStateException("Instanced with a NULL OCFile");
164 }
165 if (mAccount == null) {
166 throw new IllegalStateException("Instanced with a NULL ownCloud Account");
167 }
168 if (!getFile().isDown()) {
169 throw new IllegalStateException("There is no local file to preview");
170 }
171 }
172
173
174 /**
175 * {@inheritDoc}
176 */
177 @Override
178 public void onSaveInstanceState(Bundle outState) {
179 super.onSaveInstanceState(outState);
180 outState.putParcelable(PreviewImageFragment.EXTRA_FILE, getFile());
181 outState.putParcelable(PreviewImageFragment.EXTRA_ACCOUNT, mAccount);
182 }
183
184
185 @Override
186 public void onStart() {
187 super.onStart();
188 if (getFile() != null) {
189 BitmapLoader bl = new BitmapLoader(mImageView, mMessageView, mProgressWheel);
190 bl.execute(new String[]{getFile().getStoragePath()});
191 }
192 }
193
194
195 /**
196 * {@inheritDoc}
197 */
198 @Override
199 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
200 super.onCreateOptionsMenu(menu, inflater);
201 inflater.inflate(R.menu.file_actions_menu, menu);
202 }
203
204 /**
205 * {@inheritDoc}
206 */
207 @Override
208 public void onPrepareOptionsMenu(Menu menu) {
209 super.onPrepareOptionsMenu(menu);
210
211 if (mContainerActivity.getStorageManager() != null) {
212 // Update the file
213 setFile(mContainerActivity.getStorageManager().getFileById(getFile().getFileId()));
214
215 FileMenuFilter mf = new FileMenuFilter(
216 getFile(),
217 mContainerActivity.getStorageManager().getAccount(),
218 mContainerActivity,
219 getSherlockActivity()
220 );
221 mf.filter(menu);
222 }
223
224 // additional restriction for this fragment
225 // TODO allow renaming in PreviewImageFragment
226 MenuItem item = menu.findItem(R.id.action_rename_file);
227 if (item != null) {
228 item.setVisible(false);
229 item.setEnabled(false);
230 }
231
232 // additional restriction for this fragment
233 // TODO allow refresh file in PreviewImageFragment
234 item = menu.findItem(R.id.action_sync_file);
235 if (item != null) {
236 item.setVisible(false);
237 item.setEnabled(false);
238 }
239
240 // additional restriction for this fragment
241 item = menu.findItem(R.id.action_move);
242 if (item != null) {
243 item.setVisible(false);
244 item.setEnabled(false);
245 }
246
247 }
248
249
250
251 /**
252 * {@inheritDoc}
253 */
254 @Override
255 public boolean onOptionsItemSelected(MenuItem item) {
256 switch (item.getItemId()) {
257 case R.id.action_share_file: {
258 mContainerActivity.getFileOperationsHelper().shareFileWithLink(getFile());
259 return true;
260 }
261 case R.id.action_unshare_file: {
262 mContainerActivity.getFileOperationsHelper().unshareFileWithLink(getFile());
263 return true;
264 }
265 case R.id.action_open_file_with: {
266 openFile();
267 return true;
268 }
269 case R.id.action_remove_file: {
270 RemoveFileDialogFragment dialog = RemoveFileDialogFragment.newInstance(getFile());
271 dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
272 return true;
273 }
274 case R.id.action_see_details: {
275 seeDetails();
276 return true;
277 }
278 case R.id.action_send_file: {
279 mContainerActivity.getFileOperationsHelper().sendDownloadedFile(getFile());
280 return true;
281 }
282 case R.id.action_sync_file: {
283 mContainerActivity.getFileOperationsHelper().syncFile(getFile());
284 return true;
285 }
286
287 default:
288 return false;
289 }
290 }
291
292
293 private void seeDetails() {
294 mContainerActivity.showDetails(getFile());
295 }
296
297
298 @Override
299 public void onResume() {
300 super.onResume();
301 }
302
303
304 @Override
305 public void onPause() {
306 super.onPause();
307 }
308
309 @Override
310 public void onDestroy() {
311 if (mBitmap != null) {
312 mBitmap.recycle();
313 }
314 super.onDestroy();
315 }
316
317
318 /**
319 * Opens the previewed image with an external application.
320 */
321 private void openFile() {
322 mContainerActivity.getFileOperationsHelper().openFile(getFile());
323 finish();
324 }
325
326
327 private class BitmapLoader extends AsyncTask<String, Void, Bitmap> {
328
329 /**
330 * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
331 *
332 * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
333 */
334 private final WeakReference<ImageView> mImageViewRef;
335
336 /**
337 * Weak reference to the target {@link TextView} where error messages will be written.
338 *
339 * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
340 */
341 private final WeakReference<TextView> mMessageViewRef;
342
343
344 /**
345 * Weak reference to the target {@link Progressbar} shown while the load is in progress.
346 *
347 * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
348 */
349 private final WeakReference<ProgressBar> mProgressWheelRef;
350
351
352 /**
353 * Error message to show when a load fails
354 */
355 private int mErrorMessageId;
356
357
358 /**
359 * Constructor.
360 *
361 * @param imageView Target {@link ImageView} where the bitmap will be loaded into.
362 */
363 public BitmapLoader(ImageView imageView, TextView messageView, ProgressBar progressWheel) {
364 mImageViewRef = new WeakReference<ImageView>(imageView);
365 mMessageViewRef = new WeakReference<TextView>(messageView);
366 mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
367 }
368
369
370 @Override
371 protected Bitmap doInBackground(String... params) {
372 Bitmap result = null;
373 if (params.length != 1) return result;
374 String storagePath = params[0];
375 try {
376 //Decode file into a bitmap in real size for being able to make zoom on the image
377 result = BitmapFactory.decodeFile(storagePath);
378
379 if (result == null) {
380 mErrorMessageId = R.string.preview_image_error_unknown_format;
381 Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
382 }
383
384 } catch (OutOfMemoryError e) {
385 mErrorMessageId = R.string.preview_image_error_unknown_format;
386 Log_OC.e(TAG, "Out of memory occured for file " + storagePath, e);
387
388 } catch (NoSuchFieldError e) {
389 mErrorMessageId = R.string.common_error_unknown;
390 Log_OC.e(TAG, "Error from access to unexisting field despite protection; file " + storagePath, e);
391
392 } catch (Throwable t) {
393 mErrorMessageId = R.string.common_error_unknown;
394 Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
395
396 }
397 return result;
398 }
399
400 @Override
401 protected void onPostExecute(Bitmap result) {
402 hideProgressWheel();
403 if (result != null) {
404 showLoadedImage(result);
405 } else {
406 showErrorMessage();
407 }
408 }
409
410 @SuppressLint("InlinedApi")
411 private void showLoadedImage(Bitmap result) {
412 if (mImageViewRef != null) {
413 final ImageView imageView = mImageViewRef.get();
414 if (imageView != null) {
415 if(IS_HONEYCOMB_OR_HIGHER && checkIfMaximumBitmapExceed(result)) {
416 // Set layer type to software one for avoiding exceed
417 // and problems in visualization
418 imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
419 }
420 imageView.setImageBitmap(result);
421 imageView.setVisibility(View.VISIBLE);
422 mBitmap = result;
423 } // else , silently finish, the fragment was destroyed
424 }
425 if (mMessageViewRef != null) {
426 final TextView messageView = mMessageViewRef.get();
427 if (messageView != null) {
428 messageView.setVisibility(View.GONE);
429 } // else , silently finish, the fragment was destroyed
430 }
431 }
432
433 private void showErrorMessage() {
434 if (mImageViewRef != null) {
435 final ImageView imageView = mImageViewRef.get();
436 if (imageView != null) {
437 // shows the default error icon
438 imageView.setVisibility(View.VISIBLE);
439 } // else , silently finish, the fragment was destroyed
440 }
441 if (mMessageViewRef != null) {
442 final TextView messageView = mMessageViewRef.get();
443 if (messageView != null) {
444 messageView.setText(mErrorMessageId);
445 messageView.setVisibility(View.VISIBLE);
446 } // else , silently finish, the fragment was destroyed
447 }
448 }
449
450 private void hideProgressWheel() {
451 if (mProgressWheelRef != null) {
452 final ProgressBar progressWheel = mProgressWheelRef.get();
453 if (progressWheel != null) {
454 progressWheel.setVisibility(View.GONE);
455 }
456 }
457 }
458
459 }
460
461 /**
462 * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} to be previewed.
463 *
464 * @param file File to test if can be previewed.
465 * @return 'True' if the file can be handled by the fragment.
466 */
467 public static boolean canBePreviewed(OCFile file) {
468 return (file != null && file.isImage());
469 }
470
471
472 /**
473 * Finishes the preview
474 */
475 private void finish() {
476 Activity container = getActivity();
477 container.finish();
478 }
479
480 public TouchImageView getImageView() {
481 return mImageView;
482 }
483
484 /**
485 * Checks if current bitmaps exceed the maximum OpenGL texture size limit
486 * @param bitmap
487 * @return boolean
488 */
489 private boolean checkIfMaximumBitmapExceed(Bitmap bitmap) {
490 if (bitmap.getWidth() > MAX_OPENGL_TEXTURE_WIDTH
491 || bitmap.getHeight() > MAX_OPENGL_TEXTURE_HEIGHT) {
492 return true;
493 }
494 return false;
495 }
496 }