1 /* ownCloud Android client application
2 * Copyright 2013 ownCloud Inc.
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.
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.
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/>.
19 package com
.owncloud
.android
.media
;
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
.ContentUris
;
27 import android
.content
.Context
;
28 import android
.content
.Intent
;
29 import android
.media
.AudioManager
;
30 import android
.media
.MediaPlayer
;
31 import android
.media
.MediaPlayer
.OnCompletionListener
;
32 import android
.media
.MediaPlayer
.OnErrorListener
;
33 import android
.media
.MediaPlayer
.OnPreparedListener
;
34 import android
.net
.Uri
;
35 import android
.net
.wifi
.WifiManager
;
36 import android
.net
.wifi
.WifiManager
.WifiLock
;
37 import android
.os
.IBinder
;
38 import android
.os
.PowerManager
;
39 import android
.util
.Log
;
40 import android
.widget
.Toast
;
42 import java
.io
.IOException
;
44 import com
.owncloud
.android
.AccountUtils
;
45 import com
.owncloud
.android
.R
;
46 import com
.owncloud
.android
.datamodel
.OCFile
;
47 import com
.owncloud
.android
.ui
.activity
.FileDetailActivity
;
48 import com
.owncloud
.android
.ui
.fragment
.FileDetailFragment
;
51 * Service that handles media playback, both audio and video.
53 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
56 * @author David A. Velasco
58 public class MediaService
extends Service
implements OnCompletionListener
, OnPreparedListener
,
59 OnErrorListener
, AudioManager
.OnAudioFocusChangeListener
{
61 private static final String TAG
= MediaService
.class.getSimpleName();
63 private static final String MY_PACKAGE
= MediaService
.class.getPackage() != null ? MediaService
.class.getPackage().getName() : "com.owncloud.android.media";
65 /// Intent actions that we are prepared to handle
66 public static final String ACTION_TOGGLE_PLAYBACK
= MY_PACKAGE
+ ".action.TOGGLE_PLAYBACK";
67 public static final String ACTION_PLAY
= MY_PACKAGE
+ ".action.PLAY";
68 public static final String ACTION_PAUSE
= MY_PACKAGE
+ ".android.media.action.PAUSE";
69 public static final String ACTION_STOP
= MY_PACKAGE
+ ".android.media.action.STOP";
70 public static final String ACTION_REWIND
= MY_PACKAGE
+ ".android.media.action.REWIND";
71 public static final String ACTION_PLAY_FILE
= MY_PACKAGE
+ ".android.media.action.URL";
73 /// Keys to add extras to the action
74 public static final String EXTRA_FILE
= MY_PACKAGE
+ ".extra.FILE";
75 public static final String EXTRA_ACCOUNT
= MY_PACKAGE
+ ".extra.ACCOUNT";
78 * Volume to set when audio focus is lost and ducking is allowed
80 private static final float DUCK_VOLUME
= 0.1f
;
83 * Media player instance
85 private MediaPlayer mPlayer
= null
;
88 // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8)
89 // If not available, this will be null. Always check for null before using!
90 //AudioFocusHelper mAudioFocusHelper = null;
92 * Reference to the system AudioManager
94 private AudioManager mAudioManager
= null
;
98 * Values to indicate the state of the service
111 private State mState
= State
.STOPPED
;
115 UserRequest
, // paused by user request
116 FocusLoss
, // paused because of audio focus loss
121 * Possible focus values
130 * Current focus state
132 private AudioFocus mAudioFocus
= AudioFocus
.NO_FOCUS
;
136 * 'True' when the current song is streaming from the network
138 private boolean mIsStreaming
= false
;
141 * Wifi lock kept to prevents the device from shutting off the radio when streaming a file.
143 private WifiLock mWifiLock
;
144 private static final String MEDIA_WIFI_LOCK_TAG
= MY_PACKAGE
+ ".WIFI_LOCK";
149 * Id for the notification to keep in the notification bar while a song is playing
151 private final int NOTIFICATION_ID
= 1;
152 private NotificationManager mNotificationManager
;
153 private Notification mNotification
= null
;
155 private OCFile mFile
;
156 private Account mAccount
;
158 private IBinder mBinder
;
163 * Initialize a service instance
168 public void onCreate() {
169 Log
.d(TAG
, "Creating ownCloud media service");
171 mWifiLock
= ((WifiManager
) getSystemService(Context
.WIFI_SERVICE
)).
172 createWifiLock(WifiManager
.WIFI_MODE_FULL
, MEDIA_WIFI_LOCK_TAG
);
174 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
175 mAudioManager
= (AudioManager
) getSystemService(AUDIO_SERVICE
);
176 mBinder
= new MediaServiceBinder(this);
181 * Entry point for Intents requesting actions, sent here via startService.
183 * TODO maybe, replace by an API based in binding
186 public int onStartCommand(Intent intent
, int flags
, int startId
) {
187 String action
= intent
.getAction();
188 if (action
.equals(ACTION_PLAY_FILE
)) {
189 processPlayFileRequest(intent
);
190 } else if (action
.equals(ACTION_PLAY
)) {
191 processPlayRequest();
192 } else if (action
.equals(ACTION_TOGGLE_PLAYBACK
)) {
193 processTogglePlaybackRequest();
194 } else if (action
.equals(ACTION_PAUSE
)) {
195 processPauseRequest();
196 } else if (action
.equals(ACTION_STOP
)) {
197 processStopRequest();
198 } else if (action
.equals(ACTION_REWIND
)) {
199 processRewindRequest();
202 return START_NOT_STICKY
; // don't want it to restart in case it's killed.
207 * Processes a request to play a media file received as a parameter
209 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
211 * @param intent Intent received in the request with the data to identify the file to play.
213 private void processPlayFileRequest(Intent intent
) {
214 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
215 mFile
= intent
.getExtras().getParcelable(EXTRA_FILE
);
216 mAccount
= intent
.getExtras().getParcelable(EXTRA_ACCOUNT
);
217 tryToGetAudioFocus();
224 * Processes a request to play a media file.
226 protected void processPlayRequest() {
227 // request audio focus
228 tryToGetAudioFocus();
230 // actually play the song
231 if (mState
== State
.STOPPED
) {
232 // (re)start playback
235 } else if (mState
== State
.PAUSED
) {
237 mState
= State
.PLAYING
;
238 setUpAsForeground(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
239 configAndStartMediaPlayer();
246 * Makes sure the media player exists and has been reset. This will create the media player
247 * if needed, or reset the existing media player if one already exists.
249 protected void createMediaPlayerIfNeeded() {
250 if (mPlayer
== null
) {
251 mPlayer
= new MediaPlayer();
253 // make sure the CPU won't go to sleep while media is playing
254 mPlayer
.setWakeMode(getApplicationContext(), PowerManager
.PARTIAL_WAKE_LOCK
);
256 // the media player will notify the service when it's ready preparing, and when it's done playing
257 mPlayer
.setOnPreparedListener(this);
258 mPlayer
.setOnCompletionListener(this);
259 mPlayer
.setOnErrorListener(this);
267 * Processes a request to toggle from PLAY to PAUSE, or from PAUSE to PLAY
269 private void processTogglePlaybackRequest() {
270 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
271 processPlayRequest();
274 processPauseRequest();
279 * Processes a request to pause the current playback
281 protected void processPauseRequest() {
282 if (mState
== State
.PLAYING
) {
283 mState
= State
.PAUSED
;
285 releaseResources(false
); // retain media player in pause
286 // TODO polite audio focus, instead of keep it owned; or not?
292 * Process a request to rewind the current media playback to the start point.
294 private void processRewindRequest() {
295 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
) {
301 * Processes a request to stop the playback
303 private void processStopRequest() {
304 processStopRequest(false
);
309 * Processes a request to stop the playback.
311 * @param force When 'true', the playback is stopped no matter the value of mState
313 void processStopRequest(boolean force
) {
314 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
|| mState
== State
.STOPPED
|| force
) {
315 mState
= State
.STOPPED
;
320 releaseResources(true
);
322 stopSelf(); // service is no longer necessary
328 * Releases resources used by the service for playback. This includes the "foreground service"
329 * status and notification, the wake locks and possibly the MediaPlayer.
331 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
333 void releaseResources(boolean releaseMediaPlayer
) {
334 // stop being a foreground service
335 stopForeground(true
);
337 // stop and release the Media Player, if it's available
338 if (releaseMediaPlayer
&& mPlayer
!= null
) {
344 // release the Wifi lock, if holding it
345 if (mWifiLock
.isHeld()) {
352 * Fully releases the audio focus.
354 private void giveUpAudioFocus() {
355 if (mAudioFocus
== AudioFocus
.FOCUS
356 && mAudioManager
!= null
357 && AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.abandonAudioFocus(this)) {
359 mAudioFocus
= AudioFocus
.NO_FOCUS
;
365 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
367 void configAndStartMediaPlayer() {
368 if (mPlayer
== null
) {
369 throw new IllegalStateException("mPlayer is NULL");
372 if (mAudioFocus
== AudioFocus
.NO_FOCUS
) {
373 if (mPlayer
.isPlaying()) {
374 mPlayer
.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
378 if (mAudioFocus
== AudioFocus
.NO_FOCUS_CAN_DUCK
) {
379 mPlayer
.setVolume(DUCK_VOLUME
, DUCK_VOLUME
);
382 mPlayer
.setVolume(1.0f
, 1.0f
); // full volume
385 if (!mPlayer
.isPlaying()) {
393 * Requests the audio focus to the Audio Manager
395 private void tryToGetAudioFocus() {
396 if (mAudioFocus
!= AudioFocus
.FOCUS
397 && mAudioManager
!= null
398 && (AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.requestAudioFocus( this,
399 AudioManager
.STREAM_MUSIC
,
400 AudioManager
.AUDIOFOCUS_GAIN
))
402 mAudioFocus
= AudioFocus
.FOCUS
;
407 public static class Item
{
414 public Item(long id
, String artist
, String title
, String album
, long duration
) {
416 this.artist
= artist
;
419 this.duration
= duration
;
422 public long getId() {
426 public String
getArtist() {
430 public String
getTitle() {
434 public String
getAlbum() {
438 public long getDuration() {
442 public Uri
getURI() {
443 return ContentUris
.withAppendedId(
444 android
.provider
.MediaStore
.Audio
.Media
.EXTERNAL_CONTENT_URI
, id
);
451 * Starts playing the current media file.
454 mState
= State
.STOPPED
;
455 releaseResources(false
); // release everything except MediaPlayer
459 Toast
.makeText(this, R
.string
.media_err_nothing_to_play
, Toast
.LENGTH_LONG
).show();
460 processStopRequest(true
);
463 } else if (mAccount
== null
) {
464 Toast
.makeText(this, R
.string
.media_err_not_in_owncloud
, Toast
.LENGTH_LONG
).show();
465 processStopRequest(true
);
469 createMediaPlayerIfNeeded();
470 mPlayer
.setAudioStreamType(AudioManager
.STREAM_MUSIC
);
471 String url
= mFile
.getStoragePath();
472 if (url
== null
|| url
.length() <= 0) {
473 url
= AccountUtils
.constructFullURLForAccount(this, mAccount
) + mFile
.getRemotePath();
475 mIsStreaming
= url
.startsWith("http:") || url
.startsWith("https:");
477 mPlayer
.setDataSource(url
);
479 mState
= State
.PREPARING
;
480 setUpAsForeground(String
.format(getString(R
.string
.media_state_loading
), mFile
.getFileName()));
482 // starts preparing the media player in background
483 mPlayer
.prepareAsync();
485 // prevent the Wifi from going to sleep when streaming
488 } else if (mWifiLock
.isHeld()) {
492 } catch (SecurityException e
) {
493 Log
.e(TAG
, "SecurityException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
494 // TODO message to the user
496 } catch (IOException e
) {
497 Log
.e(TAG
, "IOException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
498 // TODO message to the user
500 } catch (IllegalStateException e
) {
501 Log
.e(TAG
, "IllegalStateException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
503 } catch (IllegalArgumentException e
) {
504 Log
.e(TAG
, "IllegalArgumentException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
510 /** Called when media player is done playing current song. */
511 public void onCompletion(MediaPlayer player
) {
512 Toast
.makeText(this, String
.format(getString(R
.string
.media_event_done
, mFile
.getFileName())), Toast
.LENGTH_LONG
).show();
513 processStopRequest(true
);
519 * Called when media player is done preparing.
523 public void onPrepared(MediaPlayer player
) {
524 mState
= State
.PLAYING
;
525 updateNotification(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
526 configAndStartMediaPlayer();
531 * Updates the status notification
533 @SuppressWarnings("deprecation")
534 private void updateNotification(String content
) {
535 // TODO check if updating the Intent is really necessary
536 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
537 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
538 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
539 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
540 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
541 (int)System
.currentTimeMillis(),
543 PendingIntent
.FLAG_UPDATE_CURRENT
);
544 mNotification
.when
= System
.currentTimeMillis();
545 //mNotification.contentView.setTextViewText(R.id.status_text, content);
546 String ticker
= "ownCloud MusicPlayer";
547 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
548 mNotificationManager
.notify(NOTIFICATION_ID
, mNotification
);
553 * Configures the service as a foreground service.
555 * The system will avoid finishing the service as much as possible when resources as low.
557 * A notification must be created to keep the user aware of the existance of the service.
559 @SuppressWarnings("deprecation")
560 private void setUpAsForeground(String content
) {
561 /// creates status notification
562 // TODO put a progress bar to follow the playback progress
563 mNotification
= new Notification();
564 mNotification
.icon
= android
.R
.drawable
.ic_media_play
;
565 //mNotification.tickerText = text;
566 mNotification
.when
= System
.currentTimeMillis();
567 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
568 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
569 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
572 /// includes a pending intent in the notification showing the details view of the file
573 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
574 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
575 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
576 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
577 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
578 (int)System
.currentTimeMillis(),
580 PendingIntent
.FLAG_UPDATE_CURRENT
);
583 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
584 String ticker
= "ownCloud MusicPlayer";
585 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
586 startForeground(NOTIFICATION_ID
, mNotification
);
591 * Called when there's an error playing media.
593 * Warns the user about the error and resets the media player.
595 public boolean onError(MediaPlayer mp
, int what
, int extra
) {
596 // TODO FOLLOW HERE!!!!!!
598 Toast
.makeText(getApplicationContext(), "Media player error! Resetting.",
599 Toast
.LENGTH_SHORT
).show();
600 Log
.e(TAG
, "Error: what=" + String
.valueOf(what
) + ", extra=" + String
.valueOf(extra
));
602 mState
= State
.STOPPED
;
603 releaseResources(true
);
610 * Called by the system when another app tries to play some sound.
615 public void onAudioFocusChange(int focusChange
) {
616 if (focusChange
> 0) {
617 // focus gain; check AudioManager.AUDIOFOCUS_* values
618 Toast
.makeText(getApplicationContext(), "gained audio focus.", Toast
.LENGTH_SHORT
).show();
619 mAudioFocus
= AudioFocus
.FOCUS
;
621 // restart media player with new focus settings
622 if (mState
== State
.PLAYING
)
623 configAndStartMediaPlayer();
625 } else if (focusChange
< 0) {
626 // focus loss; check AudioManager.AUDIOFOCUS_* values
627 boolean canDuck
= AudioManager
.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
== focusChange
;
628 Toast
.makeText(getApplicationContext(), "lost audio focus." + (canDuck ?
"can duck" :
629 "no duck"), Toast
.LENGTH_SHORT
).show();
630 mAudioFocus
= canDuck ? AudioFocus
.NO_FOCUS_CAN_DUCK
: AudioFocus
.NO_FOCUS
;
632 // start/restart/pause media player with new focus settings
633 if (mPlayer
!= null
&& mPlayer
.isPlaying())
634 configAndStartMediaPlayer();
640 * Called when the service is finished for final clean-up.
645 public void onDestroy() {
646 mState
= State
.STOPPED
;
647 releaseResources(true
);
653 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
656 public IBinder
onBind(Intent arg
) {
662 * Called when ALL the bound clients were onbound.
664 * The service is destroyed if playback stopped or paused
667 public boolean onUnbind(Intent intent
) {
668 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
669 Log
.d(TAG
, "Stopping service due to unbind in pause");
670 processStopRequest(false
);
672 return false
; // not accepting rebinding (default behaviour)
677 * Accesses the current MediaPlayer instance in the service.
679 * To be handled carefully. Visibility is protected to be accessed only
681 * @return Current MediaPlayer instance handled by MediaService.
683 protected MediaPlayer
getPlayer() {
689 * Accesses the current OCFile loaded in the service.
691 * @return The current OCFile loaded in the service.
693 protected OCFile
getCurrentFile() {
699 * Accesses the current {@link State} of the MediaService.
701 * @return The current {@link State} of the MediaService.
703 public State
getState() {