2 * ownCloud Android client application
4 * @author David A. Velasco
5 * Copyright (C) 2015 ownCloud Inc.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License version 2,
9 * as published by the Free Software Foundation.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 package com
.owncloud
.android
.media
;
23 import android
.accounts
.Account
;
24 import android
.app
.Notification
;
25 import android
.app
.NotificationManager
;
26 import android
.app
.PendingIntent
;
27 import android
.app
.Service
;
28 import android
.content
.Context
;
29 import android
.content
.Intent
;
30 import android
.media
.AudioManager
;
31 import android
.media
.MediaPlayer
;
32 import android
.media
.MediaPlayer
.OnCompletionListener
;
33 import android
.media
.MediaPlayer
.OnErrorListener
;
34 import android
.media
.MediaPlayer
.OnPreparedListener
;
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
.support
.v7
.app
.NotificationCompat
;
40 import android
.widget
.Toast
;
42 import java
.io
.IOException
;
44 import com
.owncloud
.android
.R
;
45 import com
.owncloud
.android
.datamodel
.OCFile
;
46 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
;
47 import com
.owncloud
.android
.ui
.activity
.FileActivity
;
48 import com
.owncloud
.android
.ui
.activity
.FileDisplayActivity
;
52 * Service that handles media playback, both audio and video.
54 * Waits for Intents which signal the service to perform specific operations: Play, Pause,
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
+ ".action.PLAY_FILE";
66 public static final String ACTION_STOP_ALL
= MY_PACKAGE
+ ".action.STOP_ALL";
68 /// Keys to add extras to the action
69 public static final String EXTRA_FILE
= MY_PACKAGE
+ ".extra.FILE";
70 public static final String EXTRA_ACCOUNT
= MY_PACKAGE
+ ".extra.ACCOUNT";
71 public static String EXTRA_START_POSITION
= MY_PACKAGE
+ ".extra.START_POSITION";
72 public static final String EXTRA_PLAY_ON_LOAD
= MY_PACKAGE
+ ".extra.PLAY_ON_LOAD";
75 /** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
76 public static final int OC_MEDIA_ERROR
= 0;
78 /** Time To keep the control panel visible when the user does not use it */
79 public static final int MEDIA_CONTROL_SHORT_LIFE
= 4000;
81 /** Time To keep the control panel visible when the user does not use it */
82 public static final int MEDIA_CONTROL_PERMANENT
= 0;
84 /** Volume to set when audio focus is lost and ducking is allowed */
85 private static final float DUCK_VOLUME
= 0.1f
;
87 /** Media player instance */
88 private MediaPlayer mPlayer
= null
;
90 /** Reference to the system AudioManager */
91 private AudioManager mAudioManager
= null
;
94 /** Values to indicate the state of the service */
104 private State mState
= State
.STOPPED
;
106 /** Possible focus values */
113 /** Current focus state */
114 private AudioFocus mAudioFocus
= AudioFocus
.NO_FOCUS
;
117 /** 'True' when the current song is streaming from the network */
118 private boolean mIsStreaming
= false
;
120 /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
121 private WifiLock mWifiLock
;
123 private static final String MEDIA_WIFI_LOCK_TAG
= MY_PACKAGE
+ ".WIFI_LOCK";
125 /** Notification to keep in the notification bar while a song is playing */
126 private NotificationManager mNotificationManager
;
128 /** File being played */
129 private OCFile mFile
;
131 /** Account holding the file being played */
132 private Account mAccount
;
134 /** Flag signaling if the audio should be played immediately when the file is prepared */
135 protected boolean mPlayOnPrepared
;
137 /** Position, in miliseconds, where the audio should be started */
138 private int mStartPosition
;
140 /** Interface to access the service through binding */
141 private IBinder mBinder
;
143 /** Control panel shown to the user to control the playback, to register through binding */
144 private MediaControlView mMediaController
;
146 /** Notification builder to create notifications, new reuse way since Android 6 */
147 private NotificationCompat
.Builder mNotificationBuilder
;
150 * Helper method to get an error message suitable to show to users for errors occurred in media playback,
152 * @param context A context to access string resources.
153 * @param what See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
154 * @param extra See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
155 * @return Message suitable to users.
157 public static String
getMessageForMediaError(Context context
, int what
, int extra
) {
160 if (what
== OC_MEDIA_ERROR
) {
163 } else if (extra
== MediaPlayer
.MEDIA_ERROR_UNSUPPORTED
) {
164 /* Added in API level 17
165 Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature.
166 Constant Value: -1010 (0xfffffc0e)
168 messageId
= R
.string
.media_err_unsupported
;
170 } else if (extra
== MediaPlayer
.MEDIA_ERROR_IO
) {
171 /* Added in API level 17
172 File or network related operation errors.
173 Constant Value: -1004 (0xfffffc14)
175 messageId
= R
.string
.media_err_io
;
177 } else if (extra
== MediaPlayer
.MEDIA_ERROR_MALFORMED
) {
178 /* Added in API level 17
179 Bitstream is not conforming to the related coding standard or file spec.
180 Constant Value: -1007 (0xfffffc11)
182 messageId
= R
.string
.media_err_malformed
;
184 } else if (extra
== MediaPlayer
.MEDIA_ERROR_TIMED_OUT
) {
185 /* Added in API level 17
186 Some operation takes too long to complete, usually more than 3-5 seconds.
187 Constant Value: -110 (0xffffff92)
189 messageId
= R
.string
.media_err_timeout
;
191 } else if (what
== MediaPlayer
.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK
) {
192 /* Added in API level 3
193 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.
194 Constant Value: 200 (0x000000c8)
196 messageId
= R
.string
.media_err_invalid_progressive_playback
;
199 /* MediaPlayer.MEDIA_ERROR_UNKNOWN
201 Unspecified media player error.
202 Constant Value: 1 (0x00000001)
204 /* MediaPlayer.MEDIA_ERROR_SERVER_DIED)
206 Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.
207 Constant Value: 100 (0x00000064)
209 messageId
= R
.string
.media_err_unknown
;
211 return context
.getString(messageId
);
217 * Initialize a service instance
222 public void onCreate() {
224 Log_OC
.d(TAG
, "Creating ownCloud media service");
226 mWifiLock
= ((WifiManager
) getSystemService(Context
.WIFI_SERVICE
)).
227 createWifiLock(WifiManager
.WIFI_MODE_FULL
, MEDIA_WIFI_LOCK_TAG
);
229 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
230 mNotificationBuilder
= new NotificationCompat
.Builder(this);
231 mNotificationBuilder
.setColor(this.getResources().getColor(R
.color
.primary
));
232 mAudioManager
= (AudioManager
) getSystemService(AUDIO_SERVICE
);
233 mBinder
= new MediaServiceBinder(this);
238 * Entry point for Intents requesting actions, sent here via startService.
243 public int onStartCommand(Intent intent
, int flags
, int startId
) {
244 String action
= intent
.getAction();
245 if (action
.equals(ACTION_PLAY_FILE
)) {
246 processPlayFileRequest(intent
);
248 } else if (action
.equals(ACTION_STOP_ALL
)) {
249 processStopRequest(true
);
252 return START_NOT_STICKY
; // don't want it to restart in case it's killed.
257 * Processes a request to play a media file received as a parameter
259 * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
261 * @param intent Intent received in the request with the data to identify the file to play.
263 private void processPlayFileRequest(Intent intent
) {
264 if (mState
!= State
.PREPARING
) {
265 mFile
= intent
.getExtras().getParcelable(EXTRA_FILE
);
266 mAccount
= intent
.getExtras().getParcelable(EXTRA_ACCOUNT
);
267 mPlayOnPrepared
= intent
.getExtras().getBoolean(EXTRA_PLAY_ON_LOAD
, false
);
268 mStartPosition
= intent
.getExtras().getInt(EXTRA_START_POSITION
, 0);
269 tryToGetAudioFocus();
276 * Processes a request to play a media file.
278 protected void processPlayRequest() {
279 // request audio focus
280 tryToGetAudioFocus();
282 // actually play the song
283 if (mState
== State
.STOPPED
) {
284 // (re)start playback
287 } else if (mState
== State
.PAUSED
) {
289 mState
= State
.PLAYING
;
290 setUpAsForeground(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
291 configAndStartMediaPlayer();
297 * Makes sure the media player exists and has been reset. This will create the media player
298 * if needed. reset the existing media player if one already exists.
300 protected void createMediaPlayerIfNeeded() {
301 if (mPlayer
== null
) {
302 mPlayer
= new MediaPlayer();
304 // make sure the CPU won't go to sleep while media is playing
305 mPlayer
.setWakeMode(getApplicationContext(), PowerManager
.PARTIAL_WAKE_LOCK
);
307 // the media player will notify the service when it's ready preparing, and when it's done playing
308 mPlayer
.setOnPreparedListener(this);
309 mPlayer
.setOnCompletionListener(this);
310 mPlayer
.setOnErrorListener(this);
318 * Processes a request to pause the current playback
320 protected void processPauseRequest() {
321 if (mState
== State
.PLAYING
) {
322 mState
= State
.PAUSED
;
324 releaseResources(false
); // retain media player in pause
325 // TODO polite audio focus, instead of keep it owned; or not?
331 * Processes a request to stop the playback.
333 * @param force When 'true', the playback is stopped no matter the value of mState
335 protected void processStopRequest(boolean force
) {
336 if (mState
!= State
.PREPARING
|| force
) {
337 mState
= State
.STOPPED
;
340 releaseResources(true
);
342 stopSelf(); // service is no longer necessary
348 * Releases resources used by the service for playback. This includes the "foreground service"
349 * status and notification, the wake locks and possibly the MediaPlayer.
351 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
353 protected void releaseResources(boolean releaseMediaPlayer
) {
354 // stop being a foreground service
355 stopForeground(true
);
357 // stop and release the Media Player, if it's available
358 if (releaseMediaPlayer
&& mPlayer
!= null
) {
364 // release the Wifi lock, if holding it
365 if (mWifiLock
.isHeld()) {
372 * Fully releases the audio focus.
374 private void giveUpAudioFocus() {
375 if (mAudioFocus
== AudioFocus
.FOCUS
376 && mAudioManager
!= null
377 && AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.abandonAudioFocus(this)) {
379 mAudioFocus
= AudioFocus
.NO_FOCUS
;
385 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
387 protected void configAndStartMediaPlayer() {
388 if (mPlayer
== null
) {
389 throw new IllegalStateException("mPlayer is NULL");
392 if (mAudioFocus
== AudioFocus
.NO_FOCUS
) {
393 if (mPlayer
.isPlaying()) {
394 mPlayer
.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
398 if (mAudioFocus
== AudioFocus
.NO_FOCUS_CAN_DUCK
) {
399 mPlayer
.setVolume(DUCK_VOLUME
, DUCK_VOLUME
);
402 mPlayer
.setVolume(1.0f
, 1.0f
); // full volume
405 if (!mPlayer
.isPlaying()) {
413 * Requests the audio focus to the Audio Manager
415 private void tryToGetAudioFocus() {
416 if (mAudioFocus
!= AudioFocus
.FOCUS
417 && mAudioManager
!= null
418 && (AudioManager
.AUDIOFOCUS_REQUEST_GRANTED
== mAudioManager
.requestAudioFocus( this,
419 AudioManager
.STREAM_MUSIC
,
420 AudioManager
.AUDIOFOCUS_GAIN
))
422 mAudioFocus
= AudioFocus
.FOCUS
;
428 * Starts playing the current media file.
430 protected void playMedia() {
431 mState
= State
.STOPPED
;
432 releaseResources(false
); // release everything except MediaPlayer
436 Toast
.makeText(this, R
.string
.media_err_nothing_to_play
, Toast
.LENGTH_LONG
).show();
437 processStopRequest(true
);
440 } else if (mAccount
== null
) {
441 Toast
.makeText(this, R
.string
.media_err_not_in_owncloud
, Toast
.LENGTH_LONG
).show();
442 processStopRequest(true
);
446 createMediaPlayerIfNeeded();
447 mPlayer
.setAudioStreamType(AudioManager
.STREAM_MUSIC
);
448 String url
= mFile
.getStoragePath();
449 /* Streaming is not possible right now
450 if (url == null || url.length() <= 0) {
451 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
453 mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
455 mIsStreaming
= false
;
457 mPlayer
.setDataSource(url
);
459 mState
= State
.PREPARING
;
460 setUpAsForeground(String
.format(getString(R
.string
.media_state_loading
), mFile
.getFileName()));
462 // starts preparing the media player in background
463 mPlayer
.prepareAsync();
465 // prevent the Wifi from going to sleep when streaming
468 } else if (mWifiLock
.isHeld()) {
472 } catch (SecurityException e
) {
473 Log_OC
.e(TAG
, "SecurityException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
474 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_security_ex
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
475 processStopRequest(true
);
477 } catch (IOException e
) {
478 Log_OC
.e(TAG
, "IOException playing " + mAccount
.name
+ mFile
.getRemotePath(), e
);
479 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_io_ex
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
480 processStopRequest(true
);
482 } catch (IllegalStateException e
) {
483 Log_OC
.e(TAG
, "IllegalStateException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
484 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_unexpected
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
485 processStopRequest(true
);
487 } catch (IllegalArgumentException e
) {
488 Log_OC
.e(TAG
, "IllegalArgumentException " + mAccount
.name
+ mFile
.getRemotePath(), e
);
489 Toast
.makeText(this, String
.format(getString(R
.string
.media_err_unexpected
), mFile
.getFileName()), Toast
.LENGTH_LONG
).show();
490 processStopRequest(true
);
495 /** Called when media player is done playing current song. */
496 public void onCompletion(MediaPlayer player
) {
497 Toast
.makeText(this, String
.format(getString(R
.string
.media_event_done
, mFile
.getFileName())), Toast
.LENGTH_LONG
).show();
498 if (mMediaController
!= null
) {
499 // somebody is still bound to the service
501 processPauseRequest();
502 mMediaController
.updatePausePlay();
505 processStopRequest(true
);
512 * Called when media player is done preparing.
516 public void onPrepared(MediaPlayer player
) {
517 mState
= State
.PLAYING
;
518 updateNotification(String
.format(getString(R
.string
.media_state_playing
), mFile
.getFileName()));
519 if (mMediaController
!= null
) {
520 mMediaController
.setEnabled(true
);
522 player
.seekTo(mStartPosition
);
523 configAndStartMediaPlayer();
524 if (!mPlayOnPrepared
) {
525 processPauseRequest();
528 if (mMediaController
!= null
) {
529 mMediaController
.updatePausePlay();
535 * Updates the status notification
537 private void updateNotification(String content
) {
538 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
540 // TODO check if updating the Intent is really necessary
541 Intent showDetailsIntent
= new Intent(this, FileDisplayActivity
.class);
542 showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, mFile
);
543 showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, mAccount
);
544 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
546 mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity(getApplicationContext(),
547 (int) System
.currentTimeMillis(),
549 PendingIntent
.FLAG_UPDATE_CURRENT
));
550 mNotificationBuilder
.setWhen(System
.currentTimeMillis());
551 mNotificationBuilder
.setTicker(ticker
);
552 mNotificationBuilder
.setContentTitle(ticker
);
553 mNotificationBuilder
.setContentText(content
);
555 mNotificationManager
.notify(R
.string
.media_notif_ticker
, mNotificationBuilder
.build());
560 * Configures the service as a foreground service.
562 * The system will avoid finishing the service as much as possible when resources as low.
564 * A notification must be created to keep the user aware of the existance of the service.
566 private void setUpAsForeground(String content
) {
567 String ticker
= String
.format(getString(R
.string
.media_notif_ticker
), getString(R
.string
.app_name
));
569 /// creates status notification
570 // TODO put a progress bar to follow the playback progress
571 mNotificationBuilder
.setSmallIcon(R
.drawable
.ic_play_arrow
);
572 //mNotification.tickerText = text;
573 mNotificationBuilder
.setWhen(System
.currentTimeMillis());
574 mNotificationBuilder
.setOngoing(true
);
576 /// includes a pending intent in the notification showing the details view of the file
577 Intent showDetailsIntent
= new Intent(this, FileDisplayActivity
.class);
578 showDetailsIntent
.putExtra(FileActivity
.EXTRA_FILE
, mFile
);
579 showDetailsIntent
.putExtra(FileActivity
.EXTRA_ACCOUNT
, mAccount
);
580 showDetailsIntent
.setFlags(Intent
.FLAG_ACTIVITY_CLEAR_TOP
);
581 mNotificationBuilder
.setContentIntent(PendingIntent
.getActivity(getApplicationContext(),
582 (int) System
.currentTimeMillis(),
584 PendingIntent
.FLAG_UPDATE_CURRENT
));
585 mNotificationBuilder
.setContentTitle(ticker
);
586 mNotificationBuilder
.setContentText(content
);
588 startForeground(R
.string
.media_notif_ticker
, mNotificationBuilder
.build());
592 * Called when there's an error playing media.
594 * Warns the user about the error and resets the media player.
596 public boolean onError(MediaPlayer mp
, int what
, int extra
) {
597 Log_OC
.e(TAG
, "Error in audio playback, what = " + what
+ ", extra = " + extra
);
599 String message
= getMessageForMediaError(this, what
, extra
);
600 Toast
.makeText(getApplicationContext(), message
, Toast
.LENGTH_SHORT
).show();
602 processStopRequest(true
);
607 * Called by the system when another app tries to play some sound.
612 public void onAudioFocusChange(int focusChange
) {
613 if (focusChange
> 0) {
614 // focus gain; check AudioManager.AUDIOFOCUS_* values
615 mAudioFocus
= AudioFocus
.FOCUS
;
616 // restart media player with new focus settings
617 if (mState
== State
.PLAYING
)
618 configAndStartMediaPlayer();
620 } else if (focusChange
< 0) {
621 // focus loss; check AudioManager.AUDIOFOCUS_* values
622 boolean canDuck
= AudioManager
.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
== focusChange
;
623 mAudioFocus
= canDuck ? AudioFocus
.NO_FOCUS_CAN_DUCK
: AudioFocus
.NO_FOCUS
;
624 // start/restart/pause media player with new focus settings
625 if (mPlayer
!= null
&& mPlayer
.isPlaying())
626 configAndStartMediaPlayer();
632 * Called when the service is finished for final clean-up.
637 public void onDestroy() {
638 mState
= State
.STOPPED
;
639 releaseResources(true
);
641 stopForeground(true
);
647 * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
650 public IBinder
onBind(Intent arg
) {
656 * Called when ALL the bound clients were onbound.
658 * The service is destroyed if playback stopped or paused
661 public boolean onUnbind(Intent intent
) {
662 if (mState
== State
.PAUSED
|| mState
== State
.STOPPED
) {
663 processStopRequest(false
);
665 return false
; // not accepting rebinding (default behaviour)
670 * Accesses the current MediaPlayer instance in the service.
672 * To be handled carefully. Visibility is protected to be accessed only
674 * @return Current MediaPlayer instance handled by MediaService.
676 protected MediaPlayer
getPlayer() {
682 * Accesses the current OCFile loaded in the service.
684 * @return The current OCFile loaded in the service.
686 protected OCFile
getCurrentFile() {
692 * Accesses the current {@link State} of the MediaService.
694 * @return The current {@link State} of the MediaService.
696 protected State
getState() {
701 protected void setMediaContoller(MediaControlView mediaController
) {
702 mMediaController
= mediaController
;
705 protected MediaControlView
getMediaController() {
706 return mMediaController
;