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
;
137 * Helper method to get an error message suitable to show to users for errors occurred in media playback,
139 * @param context A context to access string resources.
140 * @param what See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
141 * @param extra See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
142 * @return Message suitable to users.
144 public static String
getMessageForMediaError(Context context
, int what
, int extra
) {
147 if (what
== OC_MEDIA_ERROR
) {
150 } else if (extra
== MediaPlayer
.MEDIA_ERROR_UNSUPPORTED
) {
151 /* Added in API level 17
152 Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature.
153 Constant Value: -1010 (0xfffffc0e)
155 messageId
= R
.string
.media_err_unsupported
;
157 } else if (extra
== MediaPlayer
.MEDIA_ERROR_IO
) {
158 /* Added in API level 17
159 File or network related operation errors.
160 Constant Value: -1004 (0xfffffc14)
162 messageId
= R
.string
.media_err_io
;
164 } else if (extra
== MediaPlayer
.MEDIA_ERROR_MALFORMED
) {
165 /* Added in API level 17
166 Bitstream is not conforming to the related coding standard or file spec.
167 Constant Value: -1007 (0xfffffc11)
169 messageId
= R
.string
.media_err_malformed
;
171 } else if (extra
== MediaPlayer
.MEDIA_ERROR_TIMED_OUT
) {
172 /* Added in API level 17
173 Some operation takes too long to complete, usually more than 3-5 seconds.
174 Constant Value: -110 (0xffffff92)
176 messageId
= R
.string
.media_err_timeout
;
178 } else if (what
== MediaPlayer
.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK
) {
179 /* Added in API level 3
180 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.
181 Constant Value: 200 (0x000000c8)
183 messageId
= R
.string
.media_err_invalid_progressive_playback
;
186 /* MediaPlayer.MEDIA_ERROR_UNKNOWN
188 Unspecified media player error.
189 Constant Value: 1 (0x00000001)
191 /* MediaPlayer.MEDIA_ERROR_SERVER_DIED)
193 Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.
194 Constant Value: 100 (0x00000064)
196 messageId
= R
.string
.media_err_unknown
;
198 return context
.getString(messageId
);
204 * Initialize a service instance
209 public void onCreate() {
210 Log
.d(TAG
, "Creating ownCloud media service");
212 mWifiLock
= ((WifiManager
) getSystemService(Context
.WIFI_SERVICE
)).
213 createWifiLock(WifiManager
.WIFI_MODE_FULL
, MEDIA_WIFI_LOCK_TAG
);
215 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
216 mAudioManager
= (AudioManager
) getSystemService(AUDIO_SERVICE
);
217 mBinder
= new MediaServiceBinder(this);
222 * Entry point for Intents requesting actions, sent here via startService.
227 public int onStartCommand(Intent intent
, int flags
, int startId
) {
228 String action
= intent
.getAction();
229 if (action
.equals(ACTION_PLAY_FILE
)) {
230 processPlayFileRequest(intent
);
233 return START_NOT_STICKY
; // don't want it to restart in case it's killed.
238 * Processes a request to play a media file received as a parameter
240 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
242 * @param intent Intent received in the request with the data to identify the file to play.
244 private void processPlayFileRequest(Intent intent
) {
245 if (mState
!= State
.PREPARING
) {
246 mFile
= intent
.getExtras().getParcelable(EXTRA_FILE
);
247 mAccount
= intent
.getExtras().getParcelable(EXTRA_ACCOUNT
);
248 tryToGetAudioFocus();
255 * Processes a request to play a media file.
257 protected void processPlayRequest() {
258 // request audio focus
259 tryToGetAudioFocus();
261 // actually play the song
262 if (mState
== State
.STOPPED
) {
263 // (re)start playback
266 } else if (mState
== State
.PAUSED
) {
268 mState
= State
.PLAYING
;
269 setUpAsForeground(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
270 configAndStartMediaPlayer();
277 * Makes sure the media player exists and has been reset. This will create the media player
278 * if needed, or reset the existing media player if one already exists.
280 protected void createMediaPlayerIfNeeded() {
281 if (mPlayer
== null
) {
282 mPlayer
= new MediaPlayer();
284 // make sure the CPU won't go to sleep while media is playing
285 mPlayer
.setWakeMode(getApplicationContext(), PowerManager
.PARTIAL_WAKE_LOCK
);
287 // the media player will notify the service when it's ready preparing, and when it's done playing
288 mPlayer
.setOnPreparedListener(this);
289 mPlayer
.setOnCompletionListener(this);
290 mPlayer
.setOnErrorListener(this);
298 * Processes a request to pause the current playback
300 protected void processPauseRequest() {
301 if (mState
== State
.PLAYING
) {
302 mState
= State
.PAUSED
;
304 releaseResources(false
); // retain media player in pause
305 // TODO polite audio focus, instead of keep it owned; or not?
311 * Processes a request to stop the playback.
313 * @param force When 'true', the playback is stopped no matter the value of mState
315 protected void processStopRequest(boolean force
) {
316 if (mState
!= State
.PREPARING
|| force
) {
317 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 protected 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 protected 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
;
408 * Starts playing the current media file.
410 protected void playMedia() {
411 mState
= State
.STOPPED
;
412 releaseResources(false
); // release everything except MediaPlayer
416 Toast
.makeText(this, R
.string
.media_err_nothing_to_play
, Toast
.LENGTH_LONG
).show();
417 processStopRequest(true
);
420 } else if (mAccount
== null
) {
421 Toast
.makeText(this, R
.string
.media_err_not_in_owncloud
, Toast
.LENGTH_LONG
).show();
422 processStopRequest(true
);
426 createMediaPlayerIfNeeded();
427 mPlayer
.setAudioStreamType(AudioManager
.STREAM_MUSIC
);
428 String url
= mFile
.getStoragePath();
429 /* Streaming is not possible right now
430 if (url == null || url.length() <= 0) {
431 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
433 mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
435 mIsStreaming
= false
;
437 mPlayer
.setDataSource(url
);
439 mState
= State
.PREPARING
;
440 setUpAsForeground(String
.format(getString(R
.string
.media_state_loading
), mFile
.getFileName()));
442 // starts preparing the media player in background
443 mPlayer
.prepareAsync();
445 // prevent the Wifi from going to sleep when streaming
448 } else if (mWifiLock
.isHeld()) {
452 } catch (SecurityException e
) {
453 Log
.e(TAG
, "SecurityException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
454 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_security_ex
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
455 processStopRequest(true
);
457 } catch (IOException e
) {
458 Log
.e(TAG
, "IOException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
459 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_io_ex
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
460 processStopRequest(true
);
462 } catch (IllegalStateException e
) {
463 Log
.e(TAG
, "IllegalStateException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
464 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_unexpected
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
465 processStopRequest(true
);
467 } catch (IllegalArgumentException e
) {
468 Log
.e(TAG
, "IllegalArgumentException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
469 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_unexpected
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
470 processStopRequest(true
);
475 /** Called when media player is done playing current song. */
476 public void onCompletion(MediaPlayer player
) {
477 if (mMediaController
!= null
) {
478 mMediaController
.hide();
480 Toast
.makeText(this, String
.format(getString(R
.string
.media_event_done
, mFile
.getFileName())), Toast
.LENGTH_LONG
).show();
481 processStopRequest(true
);
487 * Called when media player is done preparing.
491 public void onPrepared(MediaPlayer player
) {
492 mState
= State
.PLAYING
;
493 updateNotification(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
494 if (mMediaController
!= null
) {
495 mMediaController
.setEnabled(true
);
497 configAndStartMediaPlayer();
498 if (mMediaController
!= null
) {
499 mMediaController
.show(MEDIA_CONTROL_LIFE
);
505 * Updates the status notification
507 @SuppressWarnings("deprecation")
508 private void updateNotification(String content
) {
509 // TODO check if updating the Intent is really necessary
510 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
511 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
512 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
513 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
514 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
515 (int)System
.currentTimeMillis(),
517 PendingIntent
.FLAG_UPDATE_CURRENT
);
518 mNotification
.when
= System
.currentTimeMillis();
519 //mNotification.contentView.setTextViewText(R.id.status_text, content);
520 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
521 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
522 mNotificationManager
.notify(R
.string
.media_notif_ticker
, mNotification
);
527 * Configures the service as a foreground service.
529 * The system will avoid finishing the service as much as possible when resources as low.
531 * A notification must be created to keep the user aware of the existance of the service.
533 @SuppressWarnings("deprecation")
534 private void setUpAsForeground(String content
) {
535 /// creates status notification
536 // TODO put a progress bar to follow the playback progress
537 mNotification
= new Notification();
538 mNotification
.icon
= android
.R
.drawable
.ic_media_play
;
539 //mNotification.tickerText = text;
540 mNotification
.when
= System
.currentTimeMillis();
541 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
542 //mNotification.contentView.setTextViewText(R.id.status_text, "ownCloud Music Player"); // NULL POINTER
543 //mNotification.contentView.setTextViewText(R.id.status_text, getString(R.string.downloader_download_in_progress_content));
546 /// includes a pending intent in the notification showing the details view of the file
547 Intent showDetailsIntent
= new Intent(this, FileDetailActivity
.class);
548 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_FILE
, mFile
);
549 showDetailsIntent
.putExtra(FileDetailFragment
.EXTRA_ACCOUNT
, mAccount
);
550 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
551 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(),
552 (int)System
.currentTimeMillis(),
554 PendingIntent
.FLAG_UPDATE_CURRENT
);
557 //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
558 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
559 mNotification
.setLatestEventInfo(getApplicationContext(), ticker
, content
, mNotification
.contentIntent
);
560 startForeground(R
.string
.media_notif_ticker
, mNotification
);
565 * Called when there's an error playing media.
567 * Warns the user about the error and resets the media player.
569 public boolean onError(MediaPlayer mp
, int what
, int extra
) {
570 Log
.e(TAG
, "Error in audio playback, what = " + what
+ ", extra = " + extra
);
572 if (mMediaController
!= null
) {
573 mMediaController
.hide();
576 String message
= getMessageForMediaError(this, what
, extra
);
577 Toast
.makeText(getApplicationContext(), message
, Toast
.LENGTH_SHORT
).show();
579 processStopRequest(true
);
584 * Called by the system when another app tries to play some sound.
589 public void onAudioFocusChange(int focusChange
) {
590 if (focusChange
> 0) {
591 // focus gain; check AudioManager.AUDIOFOCUS_* values
592 mAudioFocus
= AudioFocus
.FOCUS
;
593 // restart media player with new focus settings
594 if (mState
== State
.PLAYING
)
595 configAndStartMediaPlayer();
597 } else if (focusChange
< 0) {
598 // focus loss; check AudioManager.AUDIOFOCUS_* values
599 boolean canDuck
= AudioManager
.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
== focusChange
;
600 mAudioFocus
= canDuck ? AudioFocus
.NO_FOCUS_CAN_DUCK
: AudioFocus
.NO_FOCUS
;
601 // start/restart/pause media player with new focus settings
602 if (mPlayer
!= null
&& mPlayer
.isPlaying())
603 configAndStartMediaPlayer();
609 * Called when the service is finished for final clean-up.
614 public void onDestroy() {
615 mState
= State
.STOPPED
;
616 releaseResources(true
);
622 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
625 public IBinder
onBind(Intent arg
) {
631 * Called when ALL the bound clients were onbound.
633 * The service is destroyed if playback stopped or paused
636 public boolean onUnbind(Intent intent
) {
637 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
638 processStopRequest(false
);
640 return false
; // not accepting rebinding (default behaviour)
645 * Accesses the current MediaPlayer instance in the service.
647 * To be handled carefully. Visibility is protected to be accessed only
649 * @return Current MediaPlayer instance handled by MediaService.
651 protected MediaPlayer
getPlayer() {
657 * Accesses the current OCFile loaded in the service.
659 * @return The current OCFile loaded in the service.
661 protected OCFile
getCurrentFile() {
667 * Accesses the current {@link State} of the MediaService.
669 * @return The current {@link State} of the MediaService.
671 protected State
getState() {
676 protected void setMediaContoller(MediaController mediaController
) {
677 if (mMediaController
!= null
) {
678 mMediaController
.hide();
680 mMediaController
= mediaController
;
684 protected MediaController
getMediaController() {
685 return mMediaController
;