66cd7e9230cbc5abcd279b0a10752318d90c5188
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / preview / PreviewMediaFragment.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.util.ArrayList;
20 import java.util.List;
21
22 import android.accounts.Account;
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.ServiceConnection;
30 import android.content.res.Configuration;
31 import android.media.MediaPlayer;
32 import android.media.MediaPlayer.OnCompletionListener;
33 import android.media.MediaPlayer.OnErrorListener;
34 import android.media.MediaPlayer.OnPreparedListener;
35 import android.os.Build;
36 import android.os.Bundle;
37 import android.os.IBinder;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.View.OnTouchListener;
42 import android.view.ViewGroup;
43 import android.widget.ImageView;
44 import android.widget.Toast;
45 import android.widget.VideoView;
46
47 import com.actionbarsherlock.view.Menu;
48 import com.actionbarsherlock.view.MenuInflater;
49 import com.actionbarsherlock.view.MenuItem;
50 import com.owncloud.android.R;
51 import com.owncloud.android.datamodel.FileDataStorageManager;
52 import com.owncloud.android.datamodel.OCFile;
53 import com.owncloud.android.media.MediaControlView;
54 import com.owncloud.android.media.MediaService;
55 import com.owncloud.android.media.MediaServiceBinder;
56 import com.owncloud.android.ui.activity.FileActivity;
57 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
58 import com.owncloud.android.ui.fragment.FileFragment;
59 import com.owncloud.android.utils.Log_OC;
60
61
62 /**
63 * This fragment shows a preview of a downloaded media file (audio or video).
64 *
65 * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}.
66 *
67 * By now, if the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too.
68 *
69 * @author David A. Velasco
70 */
71 public class PreviewMediaFragment extends FileFragment implements
72 OnTouchListener,
73 ConfirmationDialogFragment.ConfirmationDialogFragmentListener {
74
75 public static final String EXTRA_FILE = "FILE";
76 public static final String EXTRA_ACCOUNT = "ACCOUNT";
77 private static final String EXTRA_PLAY_POSITION = "PLAY_POSITION";
78 private static final String EXTRA_PLAYING = "PLAYING";
79
80 private View mView;
81 private Account mAccount;
82 private ImageView mImagePreview;
83 private VideoView mVideoPreview;
84 private int mSavedPlaybackPosition;
85
86 private MediaServiceBinder mMediaServiceBinder = null;
87 private MediaControlView mMediaController = null;
88 private MediaServiceConnection mMediaServiceConnection = null;
89 private VideoHelper mVideoHelper;
90 private boolean mAutoplay;
91 public boolean mPrepared;
92
93 private static final String TAG = PreviewMediaFragment.class.getSimpleName();
94
95
96 /**
97 * Creates a fragment to preview a file.
98 *
99 * When 'fileToDetail' or 'ocAccount' are null
100 *
101 * @param fileToDetail An {@link OCFile} to preview in the fragment
102 * @param ocAccount An ownCloud account; needed to start downloads
103 */
104 public PreviewMediaFragment(OCFile fileToDetail, Account ocAccount, int startPlaybackPosition, boolean autoplay) {
105 super(fileToDetail);
106 mAccount = ocAccount;
107 mSavedPlaybackPosition = startPlaybackPosition;
108 mAutoplay = autoplay;
109 }
110
111
112 /**
113 * Creates an empty fragment for previews.
114 *
115 * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (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 construction
118 */
119 public PreviewMediaFragment() {
120 super();
121 mAccount = null;
122 mSavedPlaybackPosition = 0;
123 mAutoplay = true;
124 }
125
126
127 /**
128 * {@inheritDoc}
129 */
130 @Override
131 public void onCreate(Bundle savedInstanceState) {
132 super.onCreate(savedInstanceState);
133 setHasOptionsMenu(true);
134 }
135
136
137 /**
138 * {@inheritDoc}
139 */
140 @Override
141 public View onCreateView(LayoutInflater inflater, ViewGroup container,
142 Bundle savedInstanceState) {
143 super.onCreateView(inflater, container, savedInstanceState);
144 Log_OC.e(TAG, "onCreateView");
145
146
147 mView = inflater.inflate(R.layout.file_preview, container, false);
148
149 mImagePreview = (ImageView)mView.findViewById(R.id.image_preview);
150 mVideoPreview = (VideoView)mView.findViewById(R.id.video_preview);
151 mVideoPreview.setOnTouchListener(this);
152
153 mMediaController = (MediaControlView)mView.findViewById(R.id.media_controller);
154
155 return mView;
156 }
157
158
159 /**
160 * {@inheritDoc}
161 */
162 @Override
163 public void onActivityCreated(Bundle savedInstanceState) {
164 super.onActivityCreated(savedInstanceState);
165 Log_OC.e(TAG, "onActivityCreated");
166
167 if (savedInstanceState != null) {
168 setFile((OCFile)savedInstanceState.getParcelable(PreviewMediaFragment.EXTRA_FILE));
169 mAccount = savedInstanceState.getParcelable(PreviewMediaFragment.EXTRA_ACCOUNT);
170 mSavedPlaybackPosition = savedInstanceState.getInt(PreviewMediaFragment.EXTRA_PLAY_POSITION);
171 mAutoplay = savedInstanceState.getBoolean(PreviewMediaFragment.EXTRA_PLAYING);
172
173 }
174 OCFile file = getFile();
175 if (file == null) {
176 throw new IllegalStateException("Instanced with a NULL OCFile");
177 }
178 if (mAccount == null) {
179 throw new IllegalStateException("Instanced with a NULL ownCloud Account");
180 }
181 if (!file.isDown()) {
182 throw new IllegalStateException("There is no local file to preview");
183 }
184 if (file.isVideo()) {
185 mVideoPreview.setVisibility(View.VISIBLE);
186 mImagePreview.setVisibility(View.GONE);
187 prepareVideo();
188
189 } else {
190 mVideoPreview.setVisibility(View.GONE);
191 mImagePreview.setVisibility(View.VISIBLE);
192 }
193
194 }
195
196
197 /**
198 * {@inheritDoc}
199 */
200 @Override
201 public void onSaveInstanceState(Bundle outState) {
202 super.onSaveInstanceState(outState);
203 Log_OC.e(TAG, "onSaveInstanceState");
204
205 outState.putParcelable(PreviewMediaFragment.EXTRA_FILE, getFile());
206 outState.putParcelable(PreviewMediaFragment.EXTRA_ACCOUNT, mAccount);
207
208 if (getFile().isVideo()) {
209 mSavedPlaybackPosition = mVideoPreview.getCurrentPosition();
210 mAutoplay = mVideoPreview.isPlaying();
211 outState.putInt(PreviewMediaFragment.EXTRA_PLAY_POSITION , mSavedPlaybackPosition);
212 outState.putBoolean(PreviewMediaFragment.EXTRA_PLAYING , mAutoplay);
213 } else {
214 outState.putInt(PreviewMediaFragment.EXTRA_PLAY_POSITION , mMediaServiceBinder.getCurrentPosition());
215 outState.putBoolean(PreviewMediaFragment.EXTRA_PLAYING , mMediaServiceBinder.isPlaying());
216 }
217 }
218
219
220 @Override
221 public void onStart() {
222 super.onStart();
223 Log_OC.e(TAG, "onStart");
224
225 OCFile file = getFile();
226 if (file != null) {
227 if (file.isAudio()) {
228 bindMediaService();
229
230 } else if (file.isVideo()) {
231 stopAudio();
232 playVideo();
233 }
234 }
235 }
236
237
238 private void stopAudio() {
239 Intent i = new Intent(getSherlockActivity(), MediaService.class);
240 i.setAction(MediaService.ACTION_STOP_ALL);
241 getSherlockActivity().startService(i);
242 }
243
244
245 /**
246 * {@inheritDoc}
247 */
248 @Override
249 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
250 super.onCreateOptionsMenu(menu, inflater);
251
252 inflater.inflate(R.menu.file_actions_menu, menu);
253 List<Integer> toHide = new ArrayList<Integer>();
254
255 MenuItem item = null;
256 toHide.add(R.id.action_cancel_download);
257 toHide.add(R.id.action_cancel_upload);
258 toHide.add(R.id.action_download_file);
259 toHide.add(R.id.action_sync_file);
260 toHide.add(R.id.action_rename_file); // by now
261
262 // Options shareLink
263 if (!getFile().isShareByLink()) {
264 toHide.add(R.id.action_unshare_file);
265 }
266
267 // Send file
268 boolean sendEnabled = getString(R.string.send_files_to_other_apps).equalsIgnoreCase("on");
269 if (!sendEnabled) {
270 toHide.add(R.id.action_send_file);
271 }
272
273 for (int i : toHide) {
274 item = menu.findItem(i);
275 if (item != null) {
276 item.setVisible(false);
277 item.setEnabled(false);
278 }
279 }
280
281 }
282
283
284 /**
285 * {@inheritDoc}
286 */
287 @Override
288 public void onPrepareOptionsMenu(Menu menu) {
289 super.onPrepareOptionsMenu(menu);
290
291 MenuItem item = menu.findItem(R.id.action_unshare_file);
292 // Options shareLink
293 if (!getFile().isShareByLink()) {
294 item.setVisible(false);
295 item.setEnabled(false);
296 } else {
297 item.setVisible(true);
298 item.setEnabled(true);
299 }
300 }
301
302
303 /**
304 * {@inheritDoc}
305 */
306 @Override
307 public boolean onOptionsItemSelected(MenuItem item) {
308 switch (item.getItemId()) {
309 case R.id.action_share_file: {
310 shareFileWithLink();
311 return true;
312 }
313 case R.id.action_unshare_file: {
314 unshareFileWithLink();
315 return true;
316 }
317 case R.id.action_open_file_with: {
318 openFile();
319 return true;
320 }
321 case R.id.action_remove_file: {
322 removeFile();
323 return true;
324 }
325 case R.id.action_see_details: {
326 seeDetails();
327 return true;
328 }
329 case R.id.action_send_file: {
330 sendFile();
331 }
332
333 default:
334 return false;
335 }
336 }
337
338
339
340 /**
341 * Update the file of the fragment with file value
342 * @param file
343 */
344 public void updateFile(OCFile file){
345 setFile(file);
346 }
347
348 private void unshareFileWithLink() {
349 stopPreview(false);
350 mContainerActivity.getFileOperationsHelper().unshareFileWithLink(getFile());
351 }
352
353 private void shareFileWithLink() {
354 stopPreview(false);
355 mContainerActivity.getFileOperationsHelper().shareFileWithLink(getFile());
356
357 }
358
359 private void sendFile() {
360 stopPreview(false);
361 mContainerActivity.getFileOperationsHelper().sendDownloadedFile(getFile());
362
363 }
364
365 private void seeDetails() {
366 stopPreview(false);
367 mContainerActivity.showDetails(getFile());
368 }
369
370
371 private void prepareVideo() {
372 // create helper to get more control on the playback
373 mVideoHelper = new VideoHelper();
374 mVideoPreview.setOnPreparedListener(mVideoHelper);
375 mVideoPreview.setOnCompletionListener(mVideoHelper);
376 mVideoPreview.setOnErrorListener(mVideoHelper);
377 }
378
379 private void playVideo() {
380 // create and prepare control panel for the user
381 mMediaController.setMediaPlayer(mVideoPreview);
382
383 // load the video file in the video player ; when done, VideoHelper#onPrepared() will be called
384 mVideoPreview.setVideoPath(getFile().getStoragePath());
385 }
386
387
388 private class VideoHelper implements OnCompletionListener, OnPreparedListener, OnErrorListener {
389
390 /**
391 * Called when the file is ready to be played.
392 *
393 * Just starts the playback.
394 *
395 * @param mp {@link MediaPlayer} instance performing the playback.
396 */
397 @Override
398 public void onPrepared(MediaPlayer vp) {
399 Log_OC.e(TAG, "onPrepared");
400 mVideoPreview.seekTo(mSavedPlaybackPosition);
401 if (mAutoplay) {
402 mVideoPreview.start();
403 }
404 mMediaController.setEnabled(true);
405 mMediaController.updatePausePlay();
406 mPrepared = true;
407 }
408
409
410 /**
411 * Called when the file is finished playing.
412 *
413 * Finishes the activity.
414 *
415 * @param mp {@link MediaPlayer} instance performing the playback.
416 */
417 @Override
418 public void onCompletion(MediaPlayer mp) {
419 Log_OC.e(TAG, "completed");
420 if (mp != null) {
421 mVideoPreview.seekTo(0);
422 // next lines are necessary to work around undesired video loops
423 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.GINGERBREAD) {
424 mVideoPreview.pause();
425
426 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.GINGERBREAD_MR1) {
427 // mVideePreview.pause() is not enough
428
429 mMediaController.setEnabled(false);
430 mVideoPreview.stopPlayback();
431 mAutoplay = false;
432 mSavedPlaybackPosition = 0;
433 mVideoPreview.setVideoPath(getFile().getStoragePath());
434 }
435 } // else : called from onError()
436 mMediaController.updatePausePlay();
437 }
438
439
440 /**
441 * Called when an error in playback occurs.
442 *
443 * @param mp {@link MediaPlayer} instance performing the playback.
444 * @param what Type of error
445 * @param extra Extra code specific to the error
446 */
447 @Override
448 public boolean onError(MediaPlayer mp, int what, int extra) {
449 if (mVideoPreview.getWindowToken() != null) {
450 String message = MediaService.getMessageForMediaError(getSherlockActivity(), what, extra);
451 new AlertDialog.Builder(getSherlockActivity())
452 .setMessage(message)
453 .setPositiveButton(android.R.string.VideoView_error_button,
454 new DialogInterface.OnClickListener() {
455 public void onClick(DialogInterface dialog, int whichButton) {
456 dialog.dismiss();
457 VideoHelper.this.onCompletion(null);
458 }
459 })
460 .setCancelable(false)
461 .show();
462 }
463 return true;
464 }
465
466 }
467
468
469 @Override
470 public void onPause() {
471 super.onPause();
472 Log_OC.e(TAG, "onPause");
473 }
474
475 @Override
476 public void onResume() {
477 super.onResume();
478 Log_OC.e(TAG, "onResume");
479 }
480
481 @Override
482 public void onDestroy() {
483 super.onDestroy();
484 Log_OC.e(TAG, "onDestroy");
485 }
486
487 @Override
488 public void onStop() {
489 Log_OC.e(TAG, "onStop");
490 super.onStop();
491
492 mPrepared = false;
493 if (mMediaServiceConnection != null) {
494 Log_OC.d(TAG, "Unbinding from MediaService ...");
495 if (mMediaServiceBinder != null && mMediaController != null) {
496 mMediaServiceBinder.unregisterMediaController(mMediaController);
497 }
498 getSherlockActivity().unbindService(mMediaServiceConnection);
499 mMediaServiceConnection = null;
500 mMediaServiceBinder = null;
501 }
502 }
503
504 @Override
505 public boolean onTouch(View v, MotionEvent event) {
506 if (event.getAction() == MotionEvent.ACTION_DOWN && v == mVideoPreview) {
507 startFullScreenVideo();
508 return true;
509 }
510 return false;
511 }
512
513
514 private void startFullScreenVideo() {
515 Intent i = new Intent(getSherlockActivity(), PreviewVideoActivity.class);
516 i.putExtra(FileActivity.EXTRA_ACCOUNT, mAccount);
517 i.putExtra(FileActivity.EXTRA_FILE, getFile());
518 i.putExtra(PreviewVideoActivity.EXTRA_AUTOPLAY, mVideoPreview.isPlaying());
519 mVideoPreview.pause();
520 i.putExtra(PreviewVideoActivity.EXTRA_START_POSITION, mVideoPreview.getCurrentPosition());
521 startActivityForResult(i, 0);
522 }
523
524 @Override
525 public void onConfigurationChanged (Configuration newConfig) {
526 Log_OC.e(TAG, "onConfigurationChanged " + this);
527 }
528
529 @Override
530 public void onActivityResult (int requestCode, int resultCode, Intent data) {
531 Log_OC.e(TAG, "onActivityResult " + this);
532 super.onActivityResult(requestCode, resultCode, data);
533 if (resultCode == Activity.RESULT_OK) {
534 mSavedPlaybackPosition = data.getExtras().getInt(PreviewVideoActivity.EXTRA_START_POSITION);
535 mAutoplay = data.getExtras().getBoolean(PreviewVideoActivity.EXTRA_AUTOPLAY);
536 }
537 }
538
539
540 private void playAudio() {
541 OCFile file = getFile();
542 if (!mMediaServiceBinder.isPlaying(file)) {
543 Log_OC.d(TAG, "starting playback of " + file.getStoragePath());
544 mMediaServiceBinder.start(mAccount, file, mAutoplay, mSavedPlaybackPosition);
545
546 } else {
547 if (!mMediaServiceBinder.isPlaying() && mAutoplay) {
548 mMediaServiceBinder.start();
549 mMediaController.updatePausePlay();
550 }
551 }
552 }
553
554
555 private void bindMediaService() {
556 Log_OC.d(TAG, "Binding to MediaService...");
557 if (mMediaServiceConnection == null) {
558 mMediaServiceConnection = new MediaServiceConnection();
559 }
560 getSherlockActivity().bindService( new Intent(getSherlockActivity(),
561 MediaService.class),
562 mMediaServiceConnection,
563 Context.BIND_AUTO_CREATE);
564 // follow the flow in MediaServiceConnection#onServiceConnected(...)
565 }
566
567 /** Defines callbacks for service binding, passed to bindService() */
568 private class MediaServiceConnection implements ServiceConnection {
569
570 @Override
571 public void onServiceConnected(ComponentName component, IBinder service) {
572 if (getSherlockActivity() != null) {
573 if (component.equals(new ComponentName(getSherlockActivity(), MediaService.class))) {
574 Log_OC.d(TAG, "Media service connected");
575 mMediaServiceBinder = (MediaServiceBinder) service;
576 if (mMediaServiceBinder != null) {
577 prepareMediaController();
578 playAudio(); // do not wait for the touch of nobody to play audio
579
580 Log_OC.d(TAG, "Successfully bound to MediaService, MediaController ready");
581
582 } else {
583 Log_OC.e(TAG, "Unexpected response from MediaService while binding");
584 }
585 }
586 }
587 }
588
589 private void prepareMediaController() {
590 mMediaServiceBinder.registerMediaController(mMediaController);
591 if (mMediaController != null) {
592 mMediaController.setMediaPlayer(mMediaServiceBinder);
593 mMediaController.setEnabled(true);
594 mMediaController.updatePausePlay();
595 }
596 }
597
598 @Override
599 public void onServiceDisconnected(ComponentName component) {
600 if (component.equals(new ComponentName(getSherlockActivity(), MediaService.class))) {
601 Log_OC.e(TAG, "Media service suddenly disconnected");
602 if (mMediaController != null) {
603 mMediaController.setMediaPlayer(null);
604 } else {
605 Toast.makeText(getSherlockActivity(), "No media controller to release when disconnected from media service", Toast.LENGTH_SHORT).show();
606 }
607 mMediaServiceBinder = null;
608 mMediaServiceConnection = null;
609 }
610 }
611 }
612
613
614
615 /**
616 * Opens the previewed file with an external application.
617 */
618 private void openFile() {
619 stopPreview(true);
620 mContainerActivity.getFileOperationsHelper().openFile(getFile());
621 finish();
622 }
623
624 /**
625 * Starts a the removal of the previewed file.
626 *
627 * Shows a confirmation dialog. The action continues in {@link #onConfirmation(String)} , {@link #onNeutral(String)} or {@link #onCancel(String)},
628 * depending upon the user selection in the dialog.
629 */
630 private void removeFile() {
631 ConfirmationDialogFragment confDialog = ConfirmationDialogFragment.newInstance(
632 R.string.confirmation_remove_alert,
633 new String[]{getFile().getFileName()},
634 R.string.confirmation_remove_remote_and_local,
635 R.string.confirmation_remove_local,
636 R.string.common_cancel);
637 confDialog.setOnConfirmationListener(this);
638 confDialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
639 }
640
641
642 /**
643 * Performs the removal of the previewed file, both locally and in the server.
644 */
645 @Override
646 public void onConfirmation(String callerTag) {
647 OCFile file = getFile();
648 FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
649 if (storageManager.getFileById(file.getFileId()) != null) { // check that the file is still there;
650 stopPreview(true);
651 mContainerActivity.getFileOperationsHelper().removeFile(file, true);
652 }
653 }
654
655
656 /**
657 * Removes the file from local storage
658 */
659 @Override
660 public void onNeutral(String callerTag) {
661 OCFile file = getFile();
662 stopPreview(true);
663 mContainerActivity.getStorageManager().removeFile(file, false, true); // TODO perform in background task / new thread
664 finish();
665 }
666
667 /**
668 * User cancelled the removal action.
669 */
670 @Override
671 public void onCancel(String callerTag) {
672 // nothing to do here
673 }
674
675
676 /**
677 * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewMediaFragment} to be previewed.
678 *
679 * @param file File to test if can be previewed.
680 * @return 'True' if the file can be handled by the fragment.
681 */
682 public static boolean canBePreviewed(OCFile file) {
683 return (file != null && (file.isAudio() || file.isVideo()));
684 }
685
686
687 private void stopPreview(boolean stopAudio) {
688 OCFile file = getFile();
689 if (file.isAudio() && stopAudio) {
690 mMediaServiceBinder.pause();
691
692 } else if (file.isVideo()) {
693 mVideoPreview.stopPlayback();
694 }
695 }
696
697
698
699 /**
700 * Finishes the preview
701 */
702 private void finish() {
703 getSherlockActivity().onBackPressed();
704 }
705
706
707 public int getPosition() {
708 if (mPrepared) {
709 mSavedPlaybackPosition = mVideoPreview.getCurrentPosition();
710 }
711 Log_OC.e(TAG, "getting position: " + mSavedPlaybackPosition);
712 return mSavedPlaybackPosition;
713 }
714
715 public boolean isPlaying() {
716 if (mPrepared) {
717 mAutoplay = mVideoPreview.isPlaying();
718 }
719 return mAutoplay;
720 }
721
722 }