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
.Toast
;
40 import java
.io
.IOException
;
42 import com
.owncloud
.android
.AccountUtils
;
43 import com
.owncloud
.android
.R
;
44 import com
.owncloud
.android
.datamodel
.OCFile
;
45 import com
.owncloud
.android
.ui
.activity
.FileDetailActivity
;
46 import com
.owncloud
.android
.ui
.fragment
.FileDetailFragment
;
49 * Service that handles media playback, both audio and video.
51 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
54 * @author David A. Velasco
56 public class MediaService
extends Service
implements OnCompletionListener
, OnPreparedListener
,
57 OnErrorListener
, AudioManager
.OnAudioFocusChangeListener
{
59 private static final String TAG
= MediaService
.class.getSimpleName();
61 private static final String MY_PACKAGE
= MediaService
.class.getPackage() != null ? MediaService
.class.getPackage().getName() : "com.owncloud.android.media";
63 /// Intent actions that we are prepared to handle
64 public static final String ACTION_PLAY_FILE
= MY_PACKAGE
+ ".android.media.action.PLAY_FILE";
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";
70 /** Volume to set when audio focus is lost and ducking is allowed */
71 private static final float DUCK_VOLUME
= 0.1f
;
73 /** Media player instance */
74 private MediaPlayer mPlayer
= null
;
77 /** Reference to the system AudioManager */
78 private AudioManager mAudioManager
= null
;
81 /** Values to indicate the state of the service */
91 private State mState
= State
.STOPPED
;
93 /** Possible focus values */
100 /** Current focus state */
101 private AudioFocus mAudioFocus
= AudioFocus
.NO_FOCUS
;
104 /** 'True' when the current song is streaming from the network */
105 private boolean mIsStreaming
= false
;
107 /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
108 private WifiLock mWifiLock
;
110 private static final String MEDIA_WIFI_LOCK_TAG
= MY_PACKAGE
+ ".WIFI_LOCK";
112 /** Notification to keep in the notification bar while a song is playing */
113 private NotificationManager mNotificationManager
;
114 private Notification mNotification
= null
;
116 /** File being played */
117 private OCFile mFile
;
119 /** Account holding the file being played */
120 private Account mAccount
;
122 /** Interface to access the service through binding */
123 private IBinder mBinder
;
128 * Initialize a service instance
133 public void onCreate() {
134 Log
.d(TAG
, "Creating ownCloud media service");
136 mWifiLock
= ((WifiManager
) getSystemService(Context
.WIFI_SERVICE
)).
137 createWifiLock(WifiManager
.WIFI_MODE_FULL
, MEDIA_WIFI_LOCK_TAG
);
139 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
140 mAudioManager
= (AudioManager
) getSystemService(AUDIO_SERVICE
);
141 mBinder
= new MediaServiceBinder(this);
146 * Entry point for Intents requesting actions, sent here via startService.
151 public int onStartCommand(Intent intent
, int flags
, int startId
) {
152 String action
= intent
.getAction();
153 if (action
.equals(ACTION_PLAY_FILE
)) {
154 processPlayFileRequest(intent
);
157 return START_NOT_STICKY
; // don't want it to restart in case it's killed.
162 * Processes a request to play a media file received as a parameter
164 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
166 * @param intent Intent received in the request with the data to identify the file to play.
168 private void processPlayFileRequest(Intent intent
) {
169 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
170 mFile
= intent
.getExtras().getParcelable(EXTRA_FILE
);
171 mAccount
= intent
.getExtras().getParcelable(EXTRA_ACCOUNT
);
172 tryToGetAudioFocus();
179 * Processes a request to play a media file.
181 protected void processPlayRequest() {
182 // request audio focus
183 tryToGetAudioFocus();
185 // actually play the song
186 if (mState
== State
.STOPPED
) {
187 // (re)start playback
190 } else if (mState
== State
.PAUSED
) {
192 mState
= State
.PLAYING
;
193 setUpAsForeground(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
194 configAndStartMediaPlayer();
201 * Makes sure the media player exists and has been reset. This will create the media player
202 * if needed, or reset the existing media player if one already exists.
204 protected void createMediaPlayerIfNeeded() {
205 if (mPlayer
== null
) {
206 mPlayer
= new MediaPlayer();
208 // make sure the CPU won't go to sleep while media is playing
209 mPlayer
.setWakeMode(getApplicationContext(), PowerManager
.PARTIAL_WAKE_LOCK
);
211 // the media player will notify the service when it's ready preparing, and when it's done playing
212 mPlayer
.setOnPreparedListener(this);
213 mPlayer
.setOnCompletionListener(this);
214 mPlayer
.setOnErrorListener(this);
222 * Processes a request to pause the current playback
224 protected void processPauseRequest() {
225 if (mState
== State
.PLAYING
) {
226 mState
= State
.PAUSED
;
228 releaseResources(false
); // retain media player in pause
229 // TODO polite audio focus, instead of keep it owned; or not?
235 * Processes a request to stop the playback.
237 * @param force When 'true', the playback is stopped no matter the value of mState
239 protected void processStopRequest(boolean force
) {
240 if (mState
== State
.PLAYING
|| mState
== State
.PAUSED
|| mState
== State
.STOPPED
|| force
) {
241 mState
= State
.STOPPED
;
246 releaseResources(true
);
248 stopSelf(); // service is no longer necessary
254 * Releases resources used by the service for playback. This includes the "foreground service"
255 * status and notification, the wake locks and possibly the MediaPlayer.
257 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
259 protected void releaseResources(boolean releaseMediaPlayer
) {
260 // stop being a foreground service
261 stopForeground(true
);
263 // stop and release the Media Player, if it's available
264 if (releaseMediaPlayer
&& mPlayer
!= null
) {
270 // release the Wifi lock, if holding it
271 if (mWifiLock
.isHeld()) {
278 * Fully releases the audio focus.
280 private void giveUpAudioFocus() {
281 if (mAudioFocus
== AudioFocus
.FOCUS
282 && mAudioManager
!= null
283 && AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.abandonAudioFocus(this)) {
285 mAudioFocus
= AudioFocus
.NO_FOCUS
;
291 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
293 protected void configAndStartMediaPlayer() {
294 if (mPlayer
== null
) {
295 throw new IllegalStateException("mPlayer is NULL");
298 if (mAudioFocus
== AudioFocus
.NO_FOCUS
) {
299 if (mPlayer
.isPlaying()) {
300 mPlayer
.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
304 if (mAudioFocus
== AudioFocus
.NO_FOCUS_CAN_DUCK
) {
305 mPlayer
.setVolume(DUCK_VOLUME
, DUCK_VOLUME
);
308 mPlayer
.setVolume(1.0f
, 1.0f
); // full volume
311 if (!mPlayer
.isPlaying()) {
319 * Requests the audio focus to the Audio Manager
321 private void tryToGetAudioFocus() {
322 if (mAudioFocus
!= AudioFocus
.FOCUS
323 && mAudioManager
!= null
324 && (AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.requestAudioFocus( this,
325 AudioManager
.STREAM_MUSIC
,
326 AudioManager
.AUDIOFOCUS_GAIN
))
328 mAudioFocus
= AudioFocus
.FOCUS
;
334 * Starts playing the current media file.
336 protected void playMedia() {
337 mState
= State
.STOPPED
;
338 releaseResources(false
); // release everything except MediaPlayer
342 Toast
.makeText(this, R
.string
.media_err_nothing_to_play
, Toast
.LENGTH_LONG
).show();
343 processStopRequest(true
);
346 } else if (mAccount
== null
) {
347 Toast
.makeText(this, R
.string
.media_err_not_in_owncloud
, Toast
.LENGTH_LONG
).show();
348 processStopRequest(true
);
352 createMediaPlayerIfNeeded();
353 mPlayer
.setAudioStreamType(AudioManager
.STREAM_MUSIC
);
354 String url
= mFile
.getStoragePath();
355 if (url
== null
|| url
.length() <= 0) {
356 url
= AccountUtils
.constructFullURLForAccount(this, mAccount
) + mFile
.getRemotePath();
358 mIsStreaming
= url
.startsWith("http:") || url
.startsWith("https:");
360 mPlayer
.setDataSource(url
);
362 mState
= State
.PREPARING
;
363 setUpAsForeground(String
.format(getString(R
.string
.media_state_loading
), mFile
.getFileName()));
365 // starts preparing the media player in background
366 mPlayer
.prepareAsync();
368 // prevent the Wifi from going to sleep when streaming
371 } else if (mWifiLock
.isHeld()) {
375 } catch (SecurityException e
) {
376 Log
.e(TAG
, "SecurityException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
377 // TODO message to the user
379 } catch (IOException e
) {
380 Log
.e(TAG
, "IOException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
381 // TODO message to the user
383 } catch (IllegalStateException e
) {
384 Log
.e(TAG
, "IllegalStateException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
385 // TODO message to the user
387 } catch (IllegalArgumentException e
) {
388 Log
.e(TAG
, "IllegalArgumentException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
390 // TODO message to the user
395 /** Called when media player is done playing current song. */
396 public void onCompletion(MediaPlayer player
) {
397 Toast
.makeText(this, String
.format(getString(R
.string
.media_event_done
, mFile
.getFileName())), Toast
.LENGTH_LONG
).show();
398 processStopRequest(true
);
404 * Called when media player is done preparing.
408 public void onPrepared(MediaPlayer player
) {
409 mState
= State
.PLAYING
;
410 updateNotification(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
411 configAndStartMediaPlayer();
416 * Updates the status notification
418 @SuppressWarnings("deprecation")
419 private void updateNotification(String content
) {
420 // TODO check if updating the Intent is really necessary
421 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
422 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
423 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
424 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
425 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
426 (int)System
.currentTimeMillis(),
428 PendingIntent
.FLAG_UPDATE_CURRENT
);
429 mNotification
.when
= System
.currentTimeMillis();
430 //mNotification.contentView.setTextViewText(R.id.status_text, content);
431 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
432 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
433 mNotificationManager
.notify(R
.string
.media_notif_ticker
, mNotification
);
438 * Configures the service as a foreground service.
440 * The system will avoid finishing the service as much as possible when resources as low.
442 * A notification must be created to keep the user aware of the existance of the service.
444 @SuppressWarnings("deprecation")
445 private void setUpAsForeground(String content
) {
446 /// creates status notification
447 // TODO put a progress bar to follow the playback progress
448 mNotification
= new Notification();
449 mNotification
.icon
= android
.R
.drawable
.ic_media_play
;
450 //mNotification.tickerText = text;
451 mNotification
.when
= System
.currentTimeMillis();
452 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
453 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
454 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
457 /// includes a pending intent in the notification showing the details view of the file
458 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
459 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
460 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
461 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
462 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
463 (int)System
.currentTimeMillis(),
465 PendingIntent
.FLAG_UPDATE_CURRENT
);
468 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
469 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
470 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
471 startForeground(R
.string
.media_notif_ticker
, mNotification
);
476 * Called when there's an error playing media.
478 * Warns the user about the error and resets the media player.
480 public boolean onError(MediaPlayer mp
, int what
, int extra
) {
481 // TODO FOLLOW HERE!!!!!!
483 Toast
.makeText(getApplicationContext(), "Media player error! Resetting.",
484 Toast
.LENGTH_SHORT
).show();
485 Log
.e(TAG
, "Error: what=" + String
.valueOf(what
) + ", extra=" + String
.valueOf(extra
));
487 mState
= State
.STOPPED
;
488 releaseResources(true
);
495 * Called by the system when another app tries to play some sound.
500 public void onAudioFocusChange(int focusChange
) {
501 if (focusChange
> 0) {
502 // focus gain; check AudioManager.AUDIOFOCUS_* values
503 Toast
.makeText(getApplicationContext(), "gained audio focus.", Toast
.LENGTH_SHORT
).show();
504 mAudioFocus
= AudioFocus
.FOCUS
;
506 // restart media player with new focus settings
507 if (mState
== State
.PLAYING
)
508 configAndStartMediaPlayer();
510 } else if (focusChange
< 0) {
511 // focus loss; check AudioManager.AUDIOFOCUS_* values
512 boolean canDuck
= AudioManager
.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
== focusChange
;
513 Toast
.makeText(getApplicationContext(), "lost audio focus." + (canDuck ?
"can duck" :
514 "no duck"), Toast
.LENGTH_SHORT
).show();
515 mAudioFocus
= canDuck ? AudioFocus
.NO_FOCUS_CAN_DUCK
: AudioFocus
.NO_FOCUS
;
517 // start/restart/pause media player with new focus settings
518 if (mPlayer
!= null
&& mPlayer
.isPlaying())
519 configAndStartMediaPlayer();
525 * Called when the service is finished for final clean-up.
530 public void onDestroy() {
531 mState
= State
.STOPPED
;
532 releaseResources(true
);
538 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
541 public IBinder
onBind(Intent arg
) {
547 * Called when ALL the bound clients were onbound.
549 * The service is destroyed if playback stopped or paused
552 public boolean onUnbind(Intent intent
) {
553 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
554 processStopRequest(false
);
556 return false
; // not accepting rebinding (default behaviour)
561 * Accesses the current MediaPlayer instance in the service.
563 * To be handled carefully. Visibility is protected to be accessed only
565 * @return Current MediaPlayer instance handled by MediaService.
567 protected MediaPlayer
getPlayer() {
573 * Accesses the current OCFile loaded in the service.
575 * @return The current OCFile loaded in the service.
577 protected OCFile
getCurrentFile() {
583 * Accesses the current {@link State} of the MediaService.
585 * @return The current {@link State} of the MediaService.
587 protected State
getState() {