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
.RemoteViews
;
41 import android
.widget
.Toast
;
44 import java
.io
.IOException
;
46 import com
.owncloud
.android
.AccountUtils
;
47 import com
.owncloud
.android
.R
;
48 import com
.owncloud
.android
.datamodel
.OCFile
;
49 import com
.owncloud
.android
.ui
.activity
.FileDetailActivity
;
50 import com
.owncloud
.android
.ui
.fragment
.FileDetailFragment
;
53 * Service that handles media playback, both audio and video.
55 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
58 * @author David A. Velasco
60 public class MediaService
extends Service
implements OnCompletionListener
, OnPreparedListener
,
61 OnErrorListener
, AudioManager
.OnAudioFocusChangeListener
{
63 private static final String TAG
= MediaService
.class.getSimpleName();
65 private static final String MY_PACKAGE
= MediaService
.class.getPackage() != null ? MediaService
.class.getPackage().getName() : "com.owncloud.android.media";
67 /// Intent actions that we are prepared to handle
68 public static final String ACTION_TOGGLE_PLAYBACK
= MY_PACKAGE
+ ".action.TOGGLE_PLAYBACK";
69 public static final String ACTION_PLAY
= MY_PACKAGE
+ ".action.PLAY";
70 public static final String ACTION_PAUSE
= MY_PACKAGE
+ ".android.media.action.PAUSE";
71 public static final String ACTION_STOP
= MY_PACKAGE
+ ".android.media.action.STOP";
72 public static final String ACTION_REWIND
= MY_PACKAGE
+ ".android.media.action.REWIND";
73 public static final String ACTION_PLAY_FILE
= MY_PACKAGE
+ ".android.media.action.URL";
75 /// Keys to add extras to the action
76 public static final String EXTRA_FILE
= MY_PACKAGE
+ ".extra.FILE";
77 public static final String EXTRA_ACCOUNT
= MY_PACKAGE
+ ".extra.ACCOUNT";
80 * Volume to set when audio focus is lost and ducking is allowed
82 private static final float DUCK_VOLUME
= 0.1f
;
85 * Media player instance
87 private MediaPlayer mPlayer
= null
;
90 // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8)
91 // If not available, this will be null. Always check for null before using!
92 //AudioFocusHelper mAudioFocusHelper = null;
94 * Reference to the system AudioManager
96 private AudioManager mAudioManager
= null
;
100 * Values to indicate the state of the service
113 private State mState
= State
.STOPPED
;
117 UserRequest
, // paused by user request
118 FocusLoss
, // paused because of audio focus loss
123 * Possible focus values
132 * Current focus state
134 private AudioFocus mAudioFocus
= AudioFocus
.NO_FOCUS
;
138 * 'True' when the current song is streaming from the network
140 private boolean mIsStreaming
= false
;
143 * Wifi lock kept to prevents the device from shutting off the radio when streaming a file.
145 private WifiLock mWifiLock
;
146 private static final String MEDIA_WIFI_LOCK_TAG
= MY_PACKAGE
+ ".WIFI_LOCK";
151 * Id for the notification to keep in the notification bar while a song is playing
153 private final int NOTIFICATION_ID
= 1;
154 private NotificationManager mNotificationManager
;
155 private Notification mNotification
= null
;
157 private OCFile mFile
;
158 private Account mAccount
;
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
);
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 * @param intent Intent received in the request with the data to identify the file to play.
211 private void processPlayFileRequest(Intent intent
) {
212 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
213 mFile
= intent
.getExtras().getParcelable(EXTRA_FILE
);
214 mAccount
= intent
.getExtras().getParcelable(EXTRA_ACCOUNT
);
215 tryToGetAudioFocus();
218 // TODO think what happens if mState == State.PREPARING
223 * Processes a request to play a media file.
225 void processPlayRequest() {
226 // request audio focus
227 tryToGetAudioFocus();
229 // actually play the song
230 if (mState
== State
.STOPPED
) {
231 // (re)start playback
234 } else if (mState
== State
.PAUSED
) {
236 mState
= State
.PLAYING
;
237 setUpAsForeground(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
238 configAndStartMediaPlayer();
245 * Makes sure the media player exists and has been reset. This will create the media player
246 * if needed, or reset the existing media player if one already exists.
248 void createMediaPlayerIfNeeded() {
249 if (mPlayer
== null
) {
250 mPlayer
= new MediaPlayer();
252 // make sure the CPU won't go to sleep while media is playing
253 mPlayer
.setWakeMode(getApplicationContext(), PowerManager
.PARTIAL_WAKE_LOCK
);
255 // the media player will notify the service when it's ready preparing, and when it's done playing
256 mPlayer
.setOnPreparedListener(this);
257 mPlayer
.setOnCompletionListener(this);
258 mPlayer
.setOnErrorListener(this);
266 * Processes a request to toggle from PLAY to PAUSE, or from PAUSE to PLAY
268 private void processTogglePlaybackRequest() {
269 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
270 processPlayRequest();
273 processPauseRequest();
278 * Processes a request to pause the current playback
280 private void processPauseRequest() {
281 if (mState
== State
.PLAYING
) {
282 mState
= State
.PAUSED
;
284 releaseResources(false
); // retain media player in pause
285 // TODO polite audio focus, instead of keep it owned; or not?
291 * Process a request to rewind the current media playback to the start point.
293 private void processRewindRequest() {
294 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
) {
300 * Processes a request to stop the playback
302 private void processStopRequest() {
303 processStopRequest(false
);
308 * Processes a request to stop the playback.
310 * @param force When 'true', the playback is stopped no matter the value of mState
312 void processStopRequest(boolean force
) {
313 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
|| force
) {
314 mState
= State
.STOPPED
;
316 releaseResources(true
);
318 stopSelf(); // service is no longer necessary
324 * Releases resources used by the service for playback. This includes the "foreground service"
325 * status and notification, the wake locks and possibly the MediaPlayer.
327 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
329 void releaseResources(boolean releaseMediaPlayer
) {
330 // stop being a foreground service
331 stopForeground(true
);
333 // stop and release the Media Player, if it's available
334 if (releaseMediaPlayer
&& mPlayer
!= null
) {
340 // release the Wifi lock, if holding it
341 if (mWifiLock
.isHeld()) {
348 * Fully releases the audio focus.
350 private void giveUpAudioFocus() {
351 if (mAudioFocus
== AudioFocus
.FOCUS
352 && mAudioManager
!= null
353 && AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.abandonAudioFocus(this)) {
355 mAudioFocus
= AudioFocus
.NO_FOCUS
;
361 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
363 void configAndStartMediaPlayer() {
364 if (mPlayer
== null
) {
365 throw new IllegalStateException("mPlayer is NULL");
368 if (mAudioFocus
== AudioFocus
.NO_FOCUS
) {
369 if (mPlayer
.isPlaying()) {
370 mPlayer
.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
374 if (mAudioFocus
== AudioFocus
.NO_FOCUS_CAN_DUCK
) {
375 mPlayer
.setVolume(DUCK_VOLUME
, DUCK_VOLUME
);
378 mPlayer
.setVolume(1.0f
, 1.0f
); // full volume
381 if (!mPlayer
.isPlaying()) {
389 * Requests the audio focus to the Audio Manager
391 private void tryToGetAudioFocus() {
392 if (mAudioFocus
!= AudioFocus
.FOCUS
393 && mAudioManager
!= null
394 && (AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.requestAudioFocus( this,
395 AudioManager
.STREAM_MUSIC
,
396 AudioManager
.AUDIOFOCUS_GAIN
))
398 mAudioFocus
= AudioFocus
.FOCUS
;
403 public static class Item
{
410 public Item(long id
, String artist
, String title
, String album
, long duration
) {
412 this.artist
= artist
;
415 this.duration
= duration
;
418 public long getId() {
422 public String
getArtist() {
426 public String
getTitle() {
430 public String
getAlbum() {
434 public long getDuration() {
438 public Uri
getURI() {
439 return ContentUris
.withAppendedId(
440 android
.provider
.MediaStore
.Audio
.Media
.EXTERNAL_CONTENT_URI
, id
);
447 * Starts playing the current media file.
450 mState
= State
.STOPPED
;
451 releaseResources(false
); // release everything except MediaPlayer
455 Toast
.makeText(this, R
.string
.media_err_nothing_to_play
, Toast
.LENGTH_LONG
).show();
456 processStopRequest(true
);
459 } else if (mAccount
== null
) {
460 Toast
.makeText(this, R
.string
.media_err_not_in_owncloud
, Toast
.LENGTH_LONG
).show();
461 processStopRequest(true
);
465 createMediaPlayerIfNeeded();
466 mPlayer
.setAudioStreamType(AudioManager
.STREAM_MUSIC
);
467 String url
= mFile
.getStoragePath();
468 if (url
== null
|| url
.length() <= 0) {
469 url
= AccountUtils
.constructFullURLForAccount(this, mAccount
) + mFile
.getRemotePath();
471 mIsStreaming
= url
.startsWith("http:") || url
.startsWith("https:");
473 mPlayer
.setDataSource(url
);
475 mState
= State
.PREPARING
;
476 setUpAsForeground(String
.format(getString(R
.string
.media_state_loading
), mFile
.getFileName()));
478 // starts preparing the media player in background
479 mPlayer
.prepareAsync();
481 // prevent the Wifi from going to sleep when streaming
484 } else if (mWifiLock
.isHeld()) {
488 } catch (SecurityException e
) {
489 Log
.e(TAG
, "SecurityException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
490 // TODO message to the user
492 } catch (IOException e
) {
493 Log
.e(TAG
, "IOException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
494 // TODO message to the user
496 } catch (IllegalStateException e
) {
497 Log
.e(TAG
, "IllegalStateException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
499 } catch (IllegalArgumentException e
) {
500 Log
.e(TAG
, "IllegalArgumentException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
506 /** Called when media player is done playing current song. */
507 public void onCompletion(MediaPlayer player
) {
508 Toast
.makeText(this, String
.format(getString(R
.string
.media_event_done
, mFile
.getFileName())), Toast
.LENGTH_LONG
).show();
509 processStopRequest(true
);
515 * Called when media player is done preparing.
519 public void onPrepared(MediaPlayer player
) {
520 mState
= State
.PLAYING
;
521 updateNotification(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
522 configAndStartMediaPlayer();
527 * Updates the status notification
529 @SuppressWarnings("deprecation")
530 private void updateNotification(String content
) {
531 // TODO check if updating the Intent is really necessary
532 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
533 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
534 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
535 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
536 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
537 (int)System
.currentTimeMillis(),
539 PendingIntent
.FLAG_UPDATE_CURRENT
);
540 mNotification
.when
= System
.currentTimeMillis();
541 //mNotification.contentView.setTextViewText(R.id.status_text, content);
542 String ticker
= "ownCloud MusicPlayer";
543 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
544 mNotificationManager
.notify(NOTIFICATION_ID
, mNotification
);
549 * Configures the service as a foreground service.
551 * The system will avoid finishing the service as much as possible when resources as low.
553 * A notification must be created to keep the user aware of the existance of the service.
555 @SuppressWarnings("deprecation")
556 private void setUpAsForeground(String content
) {
557 /// creates status notification
558 // TODO put a progress bar to follow the playback progress
559 mNotification
= new Notification();
560 mNotification
.icon
= android
.R
.drawable
.ic_media_play
;
561 //mNotification.tickerText = text;
562 mNotification
.when
= System
.currentTimeMillis();
563 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
564 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
565 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
568 /// includes a pending intent in the notification showing the details view of the file
569 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
570 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
571 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
572 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
573 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
574 (int)System
.currentTimeMillis(),
576 PendingIntent
.FLAG_UPDATE_CURRENT
);
579 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
580 String ticker
= "ownCloud MusicPlayer";
581 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
582 startForeground(NOTIFICATION_ID
, mNotification
);
587 * Called when there's an error playing media.
589 * Warns the user about the error and resets the media player.
591 public boolean onError(MediaPlayer mp
, int what
, int extra
) {
592 // TODO FOLLOW HERE!!!!!!
594 Toast
.makeText(getApplicationContext(), "Media player error! Resetting.",
595 Toast
.LENGTH_SHORT
).show();
596 Log
.e(TAG
, "Error: what=" + String
.valueOf(what
) + ", extra=" + String
.valueOf(extra
));
598 mState
= State
.STOPPED
;
599 releaseResources(true
);
606 * Called by the system when another app tries to play some sound.
611 public void onAudioFocusChange(int focusChange
) {
612 if (focusChange
> 0) {
613 // focus gain; check AudioManager.AUDIOFOCUS_* values
614 Toast
.makeText(getApplicationContext(), "gained audio focus.", Toast
.LENGTH_SHORT
).show();
615 mAudioFocus
= AudioFocus
.FOCUS
;
617 // restart media player with new focus settings
618 if (mState
== State
.PLAYING
)
619 configAndStartMediaPlayer();
621 } else if (focusChange
< 0) {
622 // focus loss; check AudioManager.AUDIOFOCUS_* values
623 boolean canDuck
= AudioManager
.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
== focusChange
;
624 Toast
.makeText(getApplicationContext(), "lost audio focus." + (canDuck ?
"can duck" :
625 "no duck"), Toast
.LENGTH_SHORT
).show();
626 mAudioFocus
= canDuck ? AudioFocus
.NO_FOCUS_CAN_DUCK
: AudioFocus
.NO_FOCUS
;
628 // start/restart/pause media player with new focus settings
629 if (mPlayer
!= null
&& mPlayer
.isPlaying())
630 configAndStartMediaPlayer();
636 * Called when the service is finished for final clean-up.
641 public void onDestroy() {
642 mState
= State
.STOPPED
;
643 releaseResources(true
);
649 public IBinder
onBind(Intent arg0
) {
650 // TODO provide a binding API? may we use a service to play VIDEO?