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