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
.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
;
41 import java
.io
.IOException
;
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
;
50 * Service that handles media playback, both audio and video.
52 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
55 * @author David A. Velasco
57 public class MediaService
extends Service
implements OnCompletionListener
, OnPreparedListener
,
58 OnErrorListener
, AudioManager
.OnAudioFocusChangeListener
{
60 private static final String TAG
= MediaService
.class.getSimpleName();
62 private static final String MY_PACKAGE
= MediaService
.class.getPackage() != null ? MediaService
.class.getPackage().getName() : "com.owncloud.android.media";
64 /// Intent actions that we are prepared to handle
65 public static final String ACTION_PLAY_FILE
= MY_PACKAGE
+ ".android.media.action.PLAY_FILE";
67 /// Keys to add extras to the action
68 public static final String EXTRA_FILE
= MY_PACKAGE
+ ".extra.FILE";
69 public static final String EXTRA_ACCOUNT
= MY_PACKAGE
+ ".extra.ACCOUNT";
71 /** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
72 public static final int OC_MEDIA_ERROR
= 0;
74 /** Time To keep the control panel visible when the user does not use it */
75 public static final int MEDIA_CONTROL_LIFE
= 5000;
77 /** Volume to set when audio focus is lost and ducking is allowed */
78 private static final float DUCK_VOLUME
= 0.1f
;
80 /** Media player instance */
81 private MediaPlayer mPlayer
= null
;
83 /** Reference to the system AudioManager */
84 private AudioManager mAudioManager
= null
;
87 /** Values to indicate the state of the service */
97 private State mState
= State
.STOPPED
;
99 /** Possible focus values */
106 /** Current focus state */
107 private AudioFocus mAudioFocus
= AudioFocus
.NO_FOCUS
;
110 /** 'True' when the current song is streaming from the network */
111 private boolean mIsStreaming
= false
;
113 /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
114 private WifiLock mWifiLock
;
116 private static final String MEDIA_WIFI_LOCK_TAG
= MY_PACKAGE
+ ".WIFI_LOCK";
118 /** Notification to keep in the notification bar while a song is playing */
119 private NotificationManager mNotificationManager
;
120 private Notification mNotification
= null
;
122 /** File being played */
123 private OCFile mFile
;
125 /** Account holding the file being played */
126 private Account mAccount
;
128 /** Interface to access the service through binding */
129 private IBinder mBinder
;
131 /** Control panel shown to the user to control the playback, to register through binding */
132 private MediaController mMediaController
;
136 * Initialize a service instance
141 public void onCreate() {
142 Log
.d(TAG
, "Creating ownCloud media service");
144 mWifiLock
= ((WifiManager
) getSystemService(Context
.WIFI_SERVICE
)).
145 createWifiLock(WifiManager
.WIFI_MODE_FULL
, MEDIA_WIFI_LOCK_TAG
);
147 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
148 mAudioManager
= (AudioManager
) getSystemService(AUDIO_SERVICE
);
149 mBinder
= new MediaServiceBinder(this);
154 * Entry point for Intents requesting actions, sent here via startService.
159 public int onStartCommand(Intent intent
, int flags
, int startId
) {
160 String action
= intent
.getAction();
161 if (action
.equals(ACTION_PLAY_FILE
)) {
162 processPlayFileRequest(intent
);
165 return START_NOT_STICKY
; // don't want it to restart in case it's killed.
170 * Processes a request to play a media file received as a parameter
172 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
174 * @param intent Intent received in the request with the data to identify the file to play.
176 private void processPlayFileRequest(Intent intent
) {
177 if (mState
!= State
.PREPARING
) {
178 mFile
= intent
.getExtras().getParcelable(EXTRA_FILE
);
179 mAccount
= intent
.getExtras().getParcelable(EXTRA_ACCOUNT
);
180 tryToGetAudioFocus();
187 * Processes a request to play a media file.
189 protected void processPlayRequest() {
190 // request audio focus
191 tryToGetAudioFocus();
193 // actually play the song
194 if (mState
== State
.STOPPED
) {
195 // (re)start playback
198 } else if (mState
== State
.PAUSED
) {
200 mState
= State
.PLAYING
;
201 setUpAsForeground(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
202 configAndStartMediaPlayer();
209 * Makes sure the media player exists and has been reset. This will create the media player
210 * if needed, or reset the existing media player if one already exists.
212 protected void createMediaPlayerIfNeeded() {
213 if (mPlayer
== null
) {
214 mPlayer
= new MediaPlayer();
216 // make sure the CPU won't go to sleep while media is playing
217 mPlayer
.setWakeMode(getApplicationContext(), PowerManager
.PARTIAL_WAKE_LOCK
);
219 // the media player will notify the service when it's ready preparing, and when it's done playing
220 mPlayer
.setOnPreparedListener(this);
221 mPlayer
.setOnCompletionListener(this);
222 mPlayer
.setOnErrorListener(this);
230 * Processes a request to pause the current playback
232 protected void processPauseRequest() {
233 if (mState
== State
.PLAYING
) {
234 mState
= State
.PAUSED
;
236 releaseResources(false
); // retain media player in pause
237 // TODO polite audio focus, instead of keep it owned; or not?
243 * Processes a request to stop the playback.
245 * @param force When 'true', the playback is stopped no matter the value of mState
247 protected void processStopRequest(boolean force
) {
248 if (mState
!= State
.PREPARING
|| force
) {
249 mState
= State
.STOPPED
;
252 releaseResources(true
);
254 stopSelf(); // service is no longer necessary
260 * Releases resources used by the service for playback. This includes the "foreground service"
261 * status and notification, the wake locks and possibly the MediaPlayer.
263 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
265 protected void releaseResources(boolean releaseMediaPlayer
) {
266 // stop being a foreground service
267 stopForeground(true
);
269 // stop and release the Media Player, if it's available
270 if (releaseMediaPlayer
&& mPlayer
!= null
) {
276 // release the Wifi lock, if holding it
277 if (mWifiLock
.isHeld()) {
284 * Fully releases the audio focus.
286 private void giveUpAudioFocus() {
287 if (mAudioFocus
== AudioFocus
.FOCUS
288 && mAudioManager
!= null
289 && AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.abandonAudioFocus(this)) {
291 mAudioFocus
= AudioFocus
.NO_FOCUS
;
297 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
299 protected void configAndStartMediaPlayer() {
300 if (mPlayer
== null
) {
301 throw new IllegalStateException("mPlayer is NULL");
304 if (mAudioFocus
== AudioFocus
.NO_FOCUS
) {
305 if (mPlayer
.isPlaying()) {
306 mPlayer
.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
310 if (mAudioFocus
== AudioFocus
.NO_FOCUS_CAN_DUCK
) {
311 mPlayer
.setVolume(DUCK_VOLUME
, DUCK_VOLUME
);
314 mPlayer
.setVolume(1.0f
, 1.0f
); // full volume
317 if (!mPlayer
.isPlaying()) {
325 * Requests the audio focus to the Audio Manager
327 private void tryToGetAudioFocus() {
328 if (mAudioFocus
!= AudioFocus
.FOCUS
329 && mAudioManager
!= null
330 && (AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.requestAudioFocus( this,
331 AudioManager
.STREAM_MUSIC
,
332 AudioManager
.AUDIOFOCUS_GAIN
))
334 mAudioFocus
= AudioFocus
.FOCUS
;
340 * Starts playing the current media file.
342 protected void playMedia() {
343 mState
= State
.STOPPED
;
344 releaseResources(false
); // release everything except MediaPlayer
348 Toast
.makeText(this, R
.string
.media_err_nothing_to_play
, Toast
.LENGTH_LONG
).show();
349 processStopRequest(true
);
352 } else if (mAccount
== null
) {
353 Toast
.makeText(this, R
.string
.media_err_not_in_owncloud
, Toast
.LENGTH_LONG
).show();
354 processStopRequest(true
);
358 createMediaPlayerIfNeeded();
359 mPlayer
.setAudioStreamType(AudioManager
.STREAM_MUSIC
);
360 String url
= mFile
.getStoragePath();
361 if (url
== null
|| url
.length() <= 0) {
362 url
= AccountUtils
.constructFullURLForAccount(this, mAccount
) + mFile
.getRemotePath();
364 mIsStreaming
= url
.startsWith("http:") || url
.startsWith("https:");
366 mPlayer
.setDataSource(url
);
368 mState
= State
.PREPARING
;
369 setUpAsForeground(String
.format(getString(R
.string
.media_state_loading
), mFile
.getFileName()));
371 // starts preparing the media player in background
372 mPlayer
.prepareAsync();
374 // prevent the Wifi from going to sleep when streaming
377 } else if (mWifiLock
.isHeld()) {
381 } catch (SecurityException e
) {
382 Log
.e(TAG
, "SecurityException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
383 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_security_ex
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
384 processStopRequest(true
);
386 } catch (IOException e
) {
387 Log
.e(TAG
, "IOException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
388 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_io_ex
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
389 processStopRequest(true
);
391 } catch (IllegalStateException e
) {
392 Log
.e(TAG
, "IllegalStateException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
393 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_unexpected
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
394 processStopRequest(true
);
396 } catch (IllegalArgumentException e
) {
397 Log
.e(TAG
, "IllegalArgumentException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
398 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_unexpected
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
399 processStopRequest(true
);
404 /** Called when media player is done playing current song. */
405 public void onCompletion(MediaPlayer player
) {
406 if (mMediaController
!= null
) {
407 mMediaController
.hide();
409 Toast
.makeText(this, String
.format(getString(R
.string
.media_event_done
, mFile
.getFileName())), Toast
.LENGTH_LONG
).show();
410 processStopRequest(true
);
416 * Called when media player is done preparing.
420 public void onPrepared(MediaPlayer player
) {
421 mState
= State
.PLAYING
;
422 updateNotification(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
423 if (mMediaController
!= null
) {
424 mMediaController
.setEnabled(true
);
426 configAndStartMediaPlayer();
427 if (mMediaController
!= null
) {
428 mMediaController
.show(MEDIA_CONTROL_LIFE
);
434 * Updates the status notification
436 @SuppressWarnings("deprecation")
437 private void updateNotification(String content
) {
438 // TODO check if updating the Intent is really necessary
439 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
440 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
441 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
442 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
443 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
444 (int)System
.currentTimeMillis(),
446 PendingIntent
.FLAG_UPDATE_CURRENT
);
447 mNotification
.when
= System
.currentTimeMillis();
448 //mNotification.contentView.setTextViewText(R.id.status_text, content);
449 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
450 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
451 mNotificationManager
.notify(R
.string
.media_notif_ticker
, mNotification
);
456 * Configures the service as a foreground service.
458 * The system will avoid finishing the service as much as possible when resources as low.
460 * A notification must be created to keep the user aware of the existance of the service.
462 @SuppressWarnings("deprecation")
463 private void setUpAsForeground(String content
) {
464 /// creates status notification
465 // TODO put a progress bar to follow the playback progress
466 mNotification
= new Notification();
467 mNotification
.icon
= android
.R
.drawable
.ic_media_play
;
468 //mNotification.tickerText = text;
469 mNotification
.when
= System
.currentTimeMillis();
470 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
471 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
472 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
475 /// includes a pending intent in the notification showing the details view of the file
476 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
477 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
478 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
479 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
480 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
481 (int)System
.currentTimeMillis(),
483 PendingIntent
.FLAG_UPDATE_CURRENT
);
486 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
487 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
488 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
489 startForeground(R
.string
.media_notif_ticker
, mNotification
);
494 * Called when there's an error playing media.
496 * Warns the user about the error and resets the media player.
498 public boolean onError(MediaPlayer mp
, int what
, int extra
) {
499 Log
.e(TAG
, "Error in audio playback, what = " + what
+ ", extra = " + extra
);
501 if (mMediaController
!= null
) {
502 mMediaController
.hide();
506 if (what
== OC_MEDIA_ERROR
) {
509 } else if (what
== MediaPlayer
.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK
) {
510 messageId
= R
.string
.media_err_invalid_progressive_playback
;
513 // what == MediaPlayer.MEDIA_ERROR_UNKNOWN or MEDIA_ERROR_SERVER_DIED
514 messageId
= R
.string
.media_err_unknown
;
517 Toast
.makeText(getApplicationContext(), messageId
, Toast
.LENGTH_SHORT
).show();
519 processStopRequest(true
);
525 * Called by the system when another app tries to play some sound.
530 public void onAudioFocusChange(int focusChange
) {
531 if (focusChange
> 0) {
532 // focus gain; check AudioManager.AUDIOFOCUS_* values
533 mAudioFocus
= AudioFocus
.FOCUS
;
534 // restart media player with new focus settings
535 if (mState
== State
.PLAYING
)
536 configAndStartMediaPlayer();
538 } else if (focusChange
< 0) {
539 // focus loss; check AudioManager.AUDIOFOCUS_* values
540 boolean canDuck
= AudioManager
.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
== focusChange
;
541 mAudioFocus
= canDuck ? AudioFocus
.NO_FOCUS_CAN_DUCK
: AudioFocus
.NO_FOCUS
;
542 // start/restart/pause media player with new focus settings
543 if (mPlayer
!= null
&& mPlayer
.isPlaying())
544 configAndStartMediaPlayer();
550 * Called when the service is finished for final clean-up.
555 public void onDestroy() {
556 mState
= State
.STOPPED
;
557 releaseResources(true
);
563 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
566 public IBinder
onBind(Intent arg
) {
572 * Called when ALL the bound clients were onbound.
574 * The service is destroyed if playback stopped or paused
577 public boolean onUnbind(Intent intent
) {
578 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
579 processStopRequest(false
);
581 return false
; // not accepting rebinding (default behaviour)
586 * Accesses the current MediaPlayer instance in the service.
588 * To be handled carefully. Visibility is protected to be accessed only
590 * @return Current MediaPlayer instance handled by MediaService.
592 protected MediaPlayer
getPlayer() {
598 * Accesses the current OCFile loaded in the service.
600 * @return The current OCFile loaded in the service.
602 protected OCFile
getCurrentFile() {
608 * Accesses the current {@link State} of the MediaService.
610 * @return The current {@link State} of the MediaService.
612 protected State
getState() {
617 protected void setMediaContoller(MediaController mediaController
) {
618 if (mMediaController
!= null
) {
619 mMediaController
.hide();
621 mMediaController
= mediaController
;
625 protected MediaController
getMediaController() {
626 return mMediaController
;