379b404a98aa0626b0ff1adff990583675adb6d3
[pub/Android/ownCloud.git] / src / com / owncloud / android / media / MediaService.java
1 /* ownCloud Android client application
2 * Copyright 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 as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.media;
20
21 import android.accounts.Account;
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.app.Service;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.media.AudioManager;
29 import android.media.MediaPlayer;
30 import android.media.MediaPlayer.OnCompletionListener;
31 import android.media.MediaPlayer.OnErrorListener;
32 import android.media.MediaPlayer.OnPreparedListener;
33 import android.net.wifi.WifiManager;
34 import android.net.wifi.WifiManager.WifiLock;
35 import android.os.IBinder;
36 import android.os.PowerManager;
37 import android.util.Log;
38 import android.widget.MediaController;
39 import android.widget.Toast;
40
41 import java.io.IOException;
42
43 import com.owncloud.android.AccountUtils;
44 import com.owncloud.android.R;
45 import com.owncloud.android.datamodel.OCFile;
46 import com.owncloud.android.ui.activity.FileDetailActivity;
47 import com.owncloud.android.ui.fragment.FileDetailFragment;
48
49 /**
50 * Service that handles media playback, both audio and video.
51 *
52 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
53 * Rewind, etc.
54 *
55 * @author David A. Velasco
56 */
57 public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
58 OnErrorListener, AudioManager.OnAudioFocusChangeListener {
59
60 private static final String TAG = MediaService.class.getSimpleName();
61
62 private static final String MY_PACKAGE = MediaService.class.getPackage() != null ? MediaService.class.getPackage().getName() : "com.owncloud.android.media";
63
64 /// Intent actions that we are prepared to handle
65 public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".action.PLAY_FILE";
66 public static final String ACTION_STOP_ALL = MY_PACKAGE + ".action.STOP_ALL";
67
68 /// Keys to add extras to the action
69 public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
70 public static final String EXTRA_ACCOUNT = MY_PACKAGE + ".extra.ACCOUNT";
71
72 /** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
73 public static final int OC_MEDIA_ERROR = 0;
74
75 /** Time To keep the control panel visible when the user does not use it */
76 public static final int MEDIA_CONTROL_SHORT_LIFE = 4000;
77
78 /** Time To keep the control panel visible when the user does not use it */
79 public static final int MEDIA_CONTROL_PERMANENT = 0;
80
81 /** Volume to set when audio focus is lost and ducking is allowed */
82 private static final float DUCK_VOLUME = 0.1f;
83
84 /** Media player instance */
85 private MediaPlayer mPlayer = null;
86
87 /** Reference to the system AudioManager */
88 private AudioManager mAudioManager = null;
89
90
91 /** Values to indicate the state of the service */
92 enum State {
93 STOPPED,
94 PREPARING,
95 PLAYING,
96 PAUSED
97 };
98
99
100 /** Current state */
101 private State mState = State.STOPPED;
102
103 /** Possible focus values */
104 enum AudioFocus {
105 NO_FOCUS,
106 NO_FOCUS_CAN_DUCK,
107 FOCUS
108 }
109
110 /** Current focus state */
111 private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
112
113
114 /** 'True' when the current song is streaming from the network */
115 private boolean mIsStreaming = false;
116
117 /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
118 private WifiLock mWifiLock;
119
120 private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
121
122 /** Notification to keep in the notification bar while a song is playing */
123 private NotificationManager mNotificationManager;
124 private Notification mNotification = null;
125
126 /** File being played */
127 private OCFile mFile;
128
129 /** Account holding the file being played */
130 private Account mAccount;
131
132 /** Interface to access the service through binding */
133 private IBinder mBinder;
134
135 /** Control panel shown to the user to control the playback, to register through binding */
136 private MediaControlView mMediaController;
137
138
139
140 /**
141 * Helper method to get an error message suitable to show to users for errors occurred in media playback,
142 *
143 * @param context A context to access string resources.
144 * @param what See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
145 * @param extra See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
146 * @return Message suitable to users.
147 */
148 public static String getMessageForMediaError(Context context, int what, int extra) {
149 int messageId;
150
151 if (what == OC_MEDIA_ERROR) {
152 messageId = extra;
153
154 } else if (extra == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
155 /* Added in API level 17
156 Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature.
157 Constant Value: -1010 (0xfffffc0e)
158 */
159 messageId = R.string.media_err_unsupported;
160
161 } else if (extra == MediaPlayer.MEDIA_ERROR_IO) {
162 /* Added in API level 17
163 File or network related operation errors.
164 Constant Value: -1004 (0xfffffc14)
165 */
166 messageId = R.string.media_err_io;
167
168 } else if (extra == MediaPlayer.MEDIA_ERROR_MALFORMED) {
169 /* Added in API level 17
170 Bitstream is not conforming to the related coding standard or file spec.
171 Constant Value: -1007 (0xfffffc11)
172 */
173 messageId = R.string.media_err_malformed;
174
175 } else if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
176 /* Added in API level 17
177 Some operation takes too long to complete, usually more than 3-5 seconds.
178 Constant Value: -110 (0xffffff92)
179 */
180 messageId = R.string.media_err_timeout;
181
182 } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
183 /* Added in API level 3
184 The video is streamed and its container is not valid for progressive playback i.e the video's index (e.g moov atom) is not at the start of the file.
185 Constant Value: 200 (0x000000c8)
186 */
187 messageId = R.string.media_err_invalid_progressive_playback;
188
189 } else {
190 /* MediaPlayer.MEDIA_ERROR_UNKNOWN
191 Added in API level 1
192 Unspecified media player error.
193 Constant Value: 1 (0x00000001)
194 */
195 /* MediaPlayer.MEDIA_ERROR_SERVER_DIED)
196 Added in API level 1
197 Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.
198 Constant Value: 100 (0x00000064)
199 */
200 messageId = R.string.media_err_unknown;
201 }
202 return context.getString(messageId);
203 }
204
205
206
207 /**
208 * Initialize a service instance
209 *
210 * {@inheritDoc}
211 */
212 @Override
213 public void onCreate() {
214 Log.d(TAG, "Creating ownCloud media service");
215
216 mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).
217 createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
218
219 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
220 mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
221 mBinder = new MediaServiceBinder(this);
222 }
223
224
225 /**
226 * Entry point for Intents requesting actions, sent here via startService.
227 *
228 * {@inheritDoc}
229 */
230 @Override
231 public int onStartCommand(Intent intent, int flags, int startId) {
232 String action = intent.getAction();
233 if (action.equals(ACTION_PLAY_FILE)) {
234 processPlayFileRequest(intent);
235
236 } else if (action.equals(ACTION_STOP_ALL)) {
237 processStopRequest(true);
238 }
239
240 return START_NOT_STICKY; // don't want it to restart in case it's killed.
241 }
242
243
244 /**
245 * Processes a request to play a media file received as a parameter
246 *
247 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
248 *
249 * @param intent Intent received in the request with the data to identify the file to play.
250 */
251 private void processPlayFileRequest(Intent intent) {
252 if (mState != State.PREPARING) {
253 mFile = intent.getExtras().getParcelable(EXTRA_FILE);
254 mAccount = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
255 tryToGetAudioFocus();
256 playMedia();
257 }
258 }
259
260
261 /**
262 * Processes a request to play a media file.
263 */
264 protected void processPlayRequest() {
265 // request audio focus
266 tryToGetAudioFocus();
267
268 // actually play the song
269 if (mState == State.STOPPED) {
270 // (re)start playback
271 playMedia();
272
273 } else if (mState == State.PAUSED) {
274 // continue playback
275 mState = State.PLAYING;
276 setUpAsForeground(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
277 configAndStartMediaPlayer();
278
279 }
280 }
281
282
283 /**
284 * Makes sure the media player exists and has been reset. This will create the media player
285 * if needed, or reset the existing media player if one already exists.
286 */
287 protected void createMediaPlayerIfNeeded() {
288 if (mPlayer == null) {
289 mPlayer = new MediaPlayer();
290
291 // make sure the CPU won't go to sleep while media is playing
292 mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
293
294 // the media player will notify the service when it's ready preparing, and when it's done playing
295 mPlayer.setOnPreparedListener(this);
296 mPlayer.setOnCompletionListener(this);
297 mPlayer.setOnErrorListener(this);
298
299 } else {
300 mPlayer.reset();
301 }
302 }
303
304 /**
305 * Processes a request to pause the current playback
306 */
307 protected void processPauseRequest() {
308 if (mState == State.PLAYING) {
309 mState = State.PAUSED;
310 mPlayer.pause();
311 releaseResources(false); // retain media player in pause
312 // TODO polite audio focus, instead of keep it owned; or not?
313 }
314 }
315
316
317 /**
318 * Processes a request to stop the playback.
319 *
320 * @param force When 'true', the playback is stopped no matter the value of mState
321 */
322 protected void processStopRequest(boolean force) {
323 if (mState != State.PREPARING || force) {
324 mState = State.STOPPED;
325 mFile = null;
326 mAccount = null;
327 releaseResources(true);
328 giveUpAudioFocus();
329 stopSelf(); // service is no longer necessary
330 }
331 }
332
333
334 /**
335 * Releases resources used by the service for playback. This includes the "foreground service"
336 * status and notification, the wake locks and possibly the MediaPlayer.
337 *
338 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
339 */
340 protected void releaseResources(boolean releaseMediaPlayer) {
341 // stop being a foreground service
342 stopForeground(true);
343
344 // stop and release the Media Player, if it's available
345 if (releaseMediaPlayer && mPlayer != null) {
346 mPlayer.reset();
347 mPlayer.release();
348 mPlayer = null;
349 }
350
351 // release the Wifi lock, if holding it
352 if (mWifiLock.isHeld()) {
353 mWifiLock.release();
354 }
355 }
356
357
358 /**
359 * Fully releases the audio focus.
360 */
361 private void giveUpAudioFocus() {
362 if (mAudioFocus == AudioFocus.FOCUS
363 && mAudioManager != null
364 && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
365
366 mAudioFocus = AudioFocus.NO_FOCUS;
367 }
368 }
369
370
371 /**
372 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
373 */
374 protected void configAndStartMediaPlayer() {
375 if (mPlayer == null) {
376 throw new IllegalStateException("mPlayer is NULL");
377 }
378
379 if (mAudioFocus == AudioFocus.NO_FOCUS) {
380 if (mPlayer.isPlaying()) {
381 mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
382 }
383
384 } else {
385 if (mAudioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
386 mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME);
387
388 } else {
389 mPlayer.setVolume(1.0f, 1.0f); // full volume
390 }
391
392 if (!mPlayer.isPlaying()) {
393 mPlayer.start();
394 }
395 }
396 }
397
398
399 /**
400 * Requests the audio focus to the Audio Manager
401 */
402 private void tryToGetAudioFocus() {
403 if (mAudioFocus != AudioFocus.FOCUS
404 && mAudioManager != null
405 && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus( this,
406 AudioManager.STREAM_MUSIC,
407 AudioManager.AUDIOFOCUS_GAIN))
408 ) {
409 mAudioFocus = AudioFocus.FOCUS;
410 }
411 }
412
413
414 /**
415 * Starts playing the current media file.
416 */
417 protected void playMedia() {
418 mState = State.STOPPED;
419 releaseResources(false); // release everything except MediaPlayer
420
421 try {
422 if (mFile == null) {
423 Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
424 processStopRequest(true);
425 return;
426
427 } else if (mAccount == null) {
428 Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
429 processStopRequest(true);
430 return;
431 }
432
433 createMediaPlayerIfNeeded();
434 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
435 String url = mFile.getStoragePath();
436 /* Streaming is not possible right now
437 if (url == null || url.length() <= 0) {
438 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
439 }
440 mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
441 */
442 mIsStreaming = false;
443
444 mPlayer.setDataSource(url);
445
446 mState = State.PREPARING;
447 setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
448
449 // starts preparing the media player in background
450 mPlayer.prepareAsync();
451
452 // prevent the Wifi from going to sleep when streaming
453 if (mIsStreaming) {
454 mWifiLock.acquire();
455 } else if (mWifiLock.isHeld()) {
456 mWifiLock.release();
457 }
458
459 } catch (SecurityException e) {
460 Log.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
461 Toast.makeText(this, String.format(getString(R.string.media_err_security_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
462 processStopRequest(true);
463
464 } catch (IOException e) {
465 Log.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
466 Toast.makeText(this, String.format(getString(R.string.media_err_io_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
467 processStopRequest(true);
468
469 } catch (IllegalStateException e) {
470 Log.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
471 Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
472 processStopRequest(true);
473
474 } catch (IllegalArgumentException e) {
475 Log.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
476 Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
477 processStopRequest(true);
478 }
479 }
480
481
482 /** Called when media player is done playing current song. */
483 public void onCompletion(MediaPlayer player) {
484 Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
485 processStopRequest(true);
486 return;
487 }
488
489
490 /**
491 * Called when media player is done preparing.
492 *
493 * Time to start.
494 */
495 public void onPrepared(MediaPlayer player) {
496 mState = State.PLAYING;
497 updateNotification(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
498 if (mMediaController != null) {
499 mMediaController.setEnabled(true);
500 }
501 configAndStartMediaPlayer();
502 if (mMediaController != null) {
503 mMediaController.updatePausePlay();
504 }
505 }
506
507
508 /**
509 * Updates the status notification
510 */
511 @SuppressWarnings("deprecation")
512 private void updateNotification(String content) {
513 // TODO check if updating the Intent is really necessary
514 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
515 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
516 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
517 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
518 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
519 (int)System.currentTimeMillis(),
520 showDetailsIntent,
521 PendingIntent.FLAG_UPDATE_CURRENT);
522 mNotification.when = System.currentTimeMillis();
523 //mNotification.contentView.setTextViewText(R.id.status_text, content);
524 String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
525 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
526 mNotificationManager.notify(R.string.media_notif_ticker, mNotification);
527 }
528
529
530 /**
531 * Configures the service as a foreground service.
532 *
533 * The system will avoid finishing the service as much as possible when resources as low.
534 *
535 * A notification must be created to keep the user aware of the existance of the service.
536 */
537 @SuppressWarnings("deprecation")
538 private void setUpAsForeground(String content) {
539 /// creates status notification
540 // TODO put a progress bar to follow the playback progress
541 mNotification = new Notification();
542 mNotification.icon = android.R.drawable.ic_media_play;
543 //mNotification.tickerText = text;
544 mNotification.when = System.currentTimeMillis();
545 mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
546 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
547 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
548
549
550 /// includes a pending intent in the notification showing the details view of the file
551 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
552 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
553 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
554 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
555 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(),
556 (int)System.currentTimeMillis(),
557 showDetailsIntent,
558 PendingIntent.FLAG_UPDATE_CURRENT);
559
560
561 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
562 String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
563 mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
564 startForeground(R.string.media_notif_ticker, mNotification);
565
566 }
567
568 /**
569 * Called when there's an error playing media.
570 *
571 * Warns the user about the error and resets the media player.
572 */
573 public boolean onError(MediaPlayer mp, int what, int extra) {
574 Log.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
575
576 String message = getMessageForMediaError(this, what, extra);
577 Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
578
579 processStopRequest(true);
580 return true;
581 }
582
583 /**
584 * Called by the system when another app tries to play some sound.
585 *
586 * {@inheritDoc}
587 */
588 @Override
589 public void onAudioFocusChange(int focusChange) {
590 if (focusChange > 0) {
591 // focus gain; check AudioManager.AUDIOFOCUS_* values
592 mAudioFocus = AudioFocus.FOCUS;
593 // restart media player with new focus settings
594 if (mState == State.PLAYING)
595 configAndStartMediaPlayer();
596
597 } else if (focusChange < 0) {
598 // focus loss; check AudioManager.AUDIOFOCUS_* values
599 boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
600 mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
601 // start/restart/pause media player with new focus settings
602 if (mPlayer != null && mPlayer.isPlaying())
603 configAndStartMediaPlayer();
604 }
605
606 }
607
608 /**
609 * Called when the service is finished for final clean-up.
610 *
611 * {@inheritDoc}
612 */
613 @Override
614 public void onDestroy() {
615 mState = State.STOPPED;
616 releaseResources(true);
617 giveUpAudioFocus();
618 }
619
620
621 /**
622 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
623 */
624 @Override
625 public IBinder onBind(Intent arg) {
626 return mBinder;
627 }
628
629
630 /**
631 * Called when ALL the bound clients were onbound.
632 *
633 * The service is destroyed if playback stopped or paused
634 */
635 @Override
636 public boolean onUnbind(Intent intent) {
637 if (mState == State.PAUSED || mState == State.STOPPED) {
638 processStopRequest(false);
639 }
640 return false; // not accepting rebinding (default behaviour)
641 }
642
643
644 /**
645 * Accesses the current MediaPlayer instance in the service.
646 *
647 * To be handled carefully. Visibility is protected to be accessed only
648 *
649 * @return Current MediaPlayer instance handled by MediaService.
650 */
651 protected MediaPlayer getPlayer() {
652 return mPlayer;
653 }
654
655
656 /**
657 * Accesses the current OCFile loaded in the service.
658 *
659 * @return The current OCFile loaded in the service.
660 */
661 protected OCFile getCurrentFile() {
662 return mFile;
663 }
664
665
666 /**
667 * Accesses the current {@link State} of the MediaService.
668 *
669 * @return The current {@link State} of the MediaService.
670 */
671 protected State getState() {
672 return mState;
673 }
674
675
676 protected void setMediaContoller(MediaControlView mediaController) {
677 mMediaController = mediaController;
678 }
679
680 protected MediaControlView getMediaController() {
681 return mMediaController;
682 }
683
684 }