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
;