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