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