Merge pull request #1048 from owncloud/shareWithYou_icon_in_fileList
[pub/Android/ownCloud.git] / src / com / owncloud / android / media / MediaService.java
index 8020f12..e53c635 100644 (file)
@@ -1,10 +1,12 @@
-/* ownCloud Android client application
- *   Copyright 2013 ownCloud Inc.
+/**
+ *   ownCloud Android client application
+ *
+ *   @author David A. Velasco
+ *   Copyright (C) 2015 ownCloud Inc.
  *
  *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License as published by
- *   the Free Software Foundation, either version 3 of the License, or
- *   (at your option) any later version.
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
  *
  *   This program is distributed in the hope that it will be useful,
  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -23,7 +25,6 @@ import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
-import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.media.AudioManager;
@@ -31,31 +32,26 @@ import android.media.MediaPlayer;
 import android.media.MediaPlayer.OnCompletionListener;
 import android.media.MediaPlayer.OnErrorListener;
 import android.media.MediaPlayer.OnPreparedListener;
-import android.net.Uri;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.WifiLock;
 import android.os.IBinder;
 import android.os.PowerManager;
-import android.util.Log;
-import android.widget.RemoteViews;
 import android.widget.Toast;
 
-import java.io.File;
 import java.io.IOException;
 
-import com.owncloud.android.AccountUtils;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.ui.activity.FileDetailActivity;
-import com.owncloud.android.ui.fragment.FileDetailFragment;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.activity.FileActivity;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
+
 
 /**
  * Service that handles media playback, both audio and video. 
  * 
  * Waits for Intents which signal the service to perform specific operations: Play, Pause,
  * Rewind, etc.
- * 
- * @author David A. Velasco
  */
 public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
                 OnErrorListener, AudioManager.OnAudioFocusChangeListener {
@@ -65,40 +61,36 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     private static final String MY_PACKAGE = MediaService.class.getPackage() != null ? MediaService.class.getPackage().getName() : "com.owncloud.android.media";
     
     /// Intent actions that we are prepared to handle
-    public static final String ACTION_TOGGLE_PLAYBACK = MY_PACKAGE + ".action.TOGGLE_PLAYBACK";
-    public static final String ACTION_PLAY = MY_PACKAGE + ".action.PLAY";
-    public static final String ACTION_PAUSE = MY_PACKAGE + ".android.media.action.PAUSE";
-    public static final String ACTION_STOP = MY_PACKAGE + ".android.media.action.STOP";
-    public static final String ACTION_REWIND = MY_PACKAGE + ".android.media.action.REWIND";
-    public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".android.media.action.URL";
+    public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".action.PLAY_FILE";
+    public static final String ACTION_STOP_ALL = MY_PACKAGE + ".action.STOP_ALL";
 
     /// Keys to add extras to the action
     public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
     public static final String EXTRA_ACCOUNT = MY_PACKAGE + ".extra.ACCOUNT";
+    public static String EXTRA_START_POSITION = MY_PACKAGE + ".extra.START_POSITION";
+    public static final String EXTRA_PLAY_ON_LOAD = MY_PACKAGE + ".extra.PLAY_ON_LOAD";
+
+
+    /** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
+    public static final int OC_MEDIA_ERROR = 0;
+
+    /** Time To keep the control panel visible when the user does not use it */
+    public static final int MEDIA_CONTROL_SHORT_LIFE = 4000;
     
-    /**
-     * Volume to set when audio focus is lost and ducking is allowed
-     */
+    /** Time To keep the control panel visible when the user does not use it */
+    public static final int MEDIA_CONTROL_PERMANENT = 0;
+    
+    /** Volume to set when audio focus is lost and ducking is allowed */
     private static final float DUCK_VOLUME = 0.1f;
 
-    /**
-     * Media player instance
-     */
+    /** Media player instance */
     private MediaPlayer mPlayer = null;
-
     
-    // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8)
-    // If not available, this will be null. Always check for null before using!
-    //AudioFocusHelper mAudioFocusHelper = null;
-    /**
-     * Reference to the system AudioManager
-     */
+    /** Reference to the system AudioManager */
     private AudioManager mAudioManager = null;
 
     
-    /**
-     * Values to indicate the state of the service
-     */
+    /** Values to indicate the state of the service */
     enum State {
         STOPPED,
         PREPARING,      
@@ -107,58 +99,119 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     };
     
 
-    /**
-     * Current state
-     */
+    /** Current state */
     private State mState = State.STOPPED;
     
-    
-    enum PauseReason {
-        UserRequest,  // paused by user request
-        FocusLoss,    // paused because of audio focus loss
-    };
-
-    
-    /**
-     * Possible focus values
-     */
+    /** Possible focus values */
     enum AudioFocus {
         NO_FOCUS,            
         NO_FOCUS_CAN_DUCK,   
         FOCUS           
     }
     
-    /**
-     * Current focus state
-     */
+    /** Current focus state */
     private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
     
 
-    /**
-     * 'True' when the current song is streaming from the network
-     */
+    /** 'True' when the current song is streaming from the network */
     private boolean mIsStreaming = false;
 
-    /**
-     * Wifi lock kept to prevents the device from shutting off the radio when streaming a file.
-     */
+    /** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
     private WifiLock mWifiLock;
+    
     private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
 
-
-
-    /**
-     * Id for the notification to keep in the notification bar while a song is playing
-     */
-    private final int NOTIFICATION_ID = 1;
+    /** Notification to keep in the notification bar while a song is playing */
     private NotificationManager mNotificationManager;
     private Notification mNotification = null;
-    
+
+    /** File being played */
     private OCFile mFile;
+    
+    /** Account holding the file being played */
     private Account mAccount;
+
+    /** Flag signaling if the audio should be played immediately when the file is prepared */ 
+    protected boolean mPlayOnPrepared;
+
+    /** Position, in miliseconds, where the audio should be started */
+    private int mStartPosition;
     
+    /** Interface to access the service through binding */
+    private IBinder mBinder;
+
+    /** Control panel shown to the user to control the playback, to register through binding */
+    private MediaControlView mMediaController;
     
 
+    
+    /**
+     * Helper method to get an error message suitable to show to users for errors occurred in media playback,
+     * 
+     * @param context   A context to access string resources.
+     * @param what      See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
+     * @param extra     See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
+     * @return          Message suitable to users.
+     */
+    public static String getMessageForMediaError(Context context, int what, int extra) {
+        int messageId;
+        
+        if (what == OC_MEDIA_ERROR) {
+            messageId = extra;
+                
+        } else if (extra == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
+            /*  Added in API level 17
+                Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature.
+                Constant Value: -1010 (0xfffffc0e)
+             */
+            messageId = R.string.media_err_unsupported;
+
+        } else if (extra == MediaPlayer.MEDIA_ERROR_IO) {
+            /*  Added in API level 17
+                File or network related operation errors.
+                Constant Value: -1004 (0xfffffc14) 
+             */
+            messageId = R.string.media_err_io;
+            
+        } else if (extra == MediaPlayer.MEDIA_ERROR_MALFORMED) {
+            /*  Added in API level 17
+                Bitstream is not conforming to the related coding standard or file spec.
+                Constant Value: -1007 (0xfffffc11) 
+             */
+            messageId = R.string.media_err_malformed;
+            
+        } else if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
+            /*  Added in API level 17
+                Some operation takes too long to complete, usually more than 3-5 seconds.
+                Constant Value: -110 (0xffffff92)
+            */
+            messageId = R.string.media_err_timeout;
+
+        } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+            /*  Added in API level 3
+                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.
+                Constant Value: 200 (0x000000c8)
+            */
+            messageId = R.string.media_err_invalid_progressive_playback;
+                
+        } else {
+            /*  MediaPlayer.MEDIA_ERROR_UNKNOWN
+                Added in API level 1
+                Unspecified media player error.
+                Constant Value: 1 (0x00000001)
+            */
+            /*  MediaPlayer.MEDIA_ERROR_SERVER_DIED)
+                Added in API level 1
+                Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.
+                Constant Value: 100 (0x00000064) 
+             */
+            messageId = R.string.media_err_unknown;
+        }
+        return context.getString(messageId);
+    }
+
+
+    
     /**
      * Initialize a service instance
      * 
@@ -166,37 +219,31 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
      */
     @Override
     public void onCreate() {
-        Log.d(TAG, "Creating ownCloud media service");
+        super.onCreate();
+        Log_OC.d(TAG, "Creating ownCloud media service");
 
         mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).
                 createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
 
         mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
         mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
-
+        mBinder = new MediaServiceBinder(this);
     }
 
     
     /**
      * Entry point for Intents requesting actions, sent here via startService.
      * 
-     * TODO maybe, replace by an API based in binding
+     * {@inheritDoc}
      */
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         String action = intent.getAction();
         if (action.equals(ACTION_PLAY_FILE)) { 
             processPlayFileRequest(intent);
-        } else if (action.equals(ACTION_PLAY)) { 
-            processPlayRequest();
-        } else if (action.equals(ACTION_TOGGLE_PLAYBACK))  { 
-            processTogglePlaybackRequest();
-        } else if (action.equals(ACTION_PAUSE)) { 
-            processPauseRequest();
-        } else if (action.equals(ACTION_STOP)) { 
-            processStopRequest();
-        } else if (action.equals(ACTION_REWIND)) { 
-            processRewindRequest();
+            
+        } else if (action.equals(ACTION_STOP_ALL)) {
+            processStopRequest(true);
         }
 
         return START_NOT_STICKY; // don't want it to restart in case it's killed.
@@ -206,23 +253,26 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     /**
      * Processes a request to play a media file received as a parameter
      * 
+     * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want? 
+     * 
      * @param intent    Intent received in the request with the data to identify the file to play. 
      */
     private void processPlayFileRequest(Intent intent) {
-        if (mState == State.PLAYING || mState == State.PAUSED || mState == State.STOPPED) {
+        if (mState != State.PREPARING) {
             mFile = intent.getExtras().getParcelable(EXTRA_FILE);
             mAccount = intent.getExtras().getParcelable(EXTRA_ACCOUNT);
+            mPlayOnPrepared = intent.getExtras().getBoolean(EXTRA_PLAY_ON_LOAD, false);
+            mStartPosition = intent.getExtras().getInt(EXTRA_START_POSITION, 0);
             tryToGetAudioFocus();
             playMedia();
         }
-        // TODO think what happens if mState == State.PREPARING
     }
 
     
     /**
      * Processes a request to play a media file.
      */
-    void processPlayRequest() {
+    protected void processPlayRequest() {
         // request audio focus
         tryToGetAudioFocus();
 
@@ -243,9 +293,9 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     
     /**
      * Makes sure the media player exists and has been reset. This will create the media player
-     * if needed, or reset the existing media player if one already exists.
+     * if needed. reset the existing media player if one already exists.
      */
-    void createMediaPlayerIfNeeded() {
+    protected void createMediaPlayerIfNeeded() {
         if (mPlayer == null) {
             mPlayer = new MediaPlayer();
 
@@ -263,21 +313,9 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     }
 
     /**
-     * Processes a request to toggle from PLAY to PAUSE, or from PAUSE to PLAY
-     */
-    private void processTogglePlaybackRequest() {
-        if (mState == State.PAUSED || mState == State.STOPPED) {
-            processPlayRequest();
-            
-        } else {
-            processPauseRequest();
-        }
-    }
-
-    /**
      * Processes a request to pause the current playback 
      */
-    private void processPauseRequest() {
+    protected void processPauseRequest() {
         if (mState == State.PLAYING) {
             mState = State.PAUSED;
             mPlayer.pause();
@@ -288,31 +326,15 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     
     
     /**
-     * Process a request to rewind the current media playback to the start point.
-     */
-    private void processRewindRequest() {
-        if (mState == State.PLAYING || mState == State.PAUSED) {
-            mPlayer.seekTo(0);
-        }
-    }
-
-    /**
-     * Processes a request to stop the playback
-     */
-    private void processStopRequest() {
-        processStopRequest(false);
-    }
-
-    
-    /**
      * Processes a request to stop the playback.
      * 
      * @param   force       When 'true', the playback is stopped no matter the value of mState
      */
-    void processStopRequest(boolean force) {
-        if (mState == State.PLAYING || mState == State.PAUSED || force) {
+    protected void processStopRequest(boolean force) {
+        if (mState != State.PREPARING || force) {
             mState = State.STOPPED;
-
+            mFile = null;
+            mAccount = null;
             releaseResources(true);
             giveUpAudioFocus();
             stopSelf();     // service is no longer necessary
@@ -326,7 +348,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
      *
      * @param releaseMediaPlayer    Indicates whether the Media Player should also be released or not
      */
-    void releaseResources(boolean releaseMediaPlayer) {
+    protected void releaseResources(boolean releaseMediaPlayer) {
         // stop being a foreground service
         stopForeground(true);
 
@@ -360,7 +382,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     /**
      * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it. 
      */
-    void configAndStartMediaPlayer() {
+    protected void configAndStartMediaPlayer() {
         if (mPlayer == null) {
             throw new IllegalStateException("mPlayer is NULL");
         }
@@ -400,53 +422,10 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     }
 
     
-    public static class Item {
-        long id;
-        String artist;
-        String title;
-        String album;
-        long duration;
-
-        public Item(long id, String artist, String title, String album, long duration) {
-            this.id = id;
-            this.artist = artist;
-            this.title = title;
-            this.album = album;
-            this.duration = duration;
-        }
-
-        public long getId() {
-            return id;
-        }
-
-        public String getArtist() {
-            return artist;
-        }
-
-        public String getTitle() {
-            return title;
-        }
-
-        public String getAlbum() {
-            return album;
-        }
-
-        public long getDuration() {
-            return duration;
-        }
-
-        public Uri getURI() {
-            return ContentUris.withAppendedId(
-                    android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
-        }
-    }
-    
-    
-    
     /**
      * Starts playing the current media file. 
      */
-    void playMedia() {
+    protected void playMedia() {
         mState = State.STOPPED;
         releaseResources(false); // release everything except MediaPlayer
 
@@ -465,10 +444,13 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
             createMediaPlayerIfNeeded();
             mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
             String url = mFile.getStoragePath();
+            /* Streaming is not possible right now
             if (url == null || url.length() <= 0) {
                 url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
             }
             mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
+            */
+            mIsStreaming = false;
             
             mPlayer.setDataSource(url);
 
@@ -486,19 +468,24 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
             }
             
         } catch (SecurityException e) {
-            Log.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
-            // TODO message to the user
+            Log_OC.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
+            Toast.makeText(this, String.format(getString(R.string.media_err_security_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
+            processStopRequest(true);
             
         } catch (IOException e) {
-            Log.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
-            // TODO message to the user
+            Log_OC.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
+            Toast.makeText(this, String.format(getString(R.string.media_err_io_ex), mFile.getFileName()), Toast.LENGTH_LONG).show();
+            processStopRequest(true);
             
         } catch (IllegalStateException e) {
-            Log.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
+            Log_OC.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
+            Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
+            processStopRequest(true);
             
         } catch (IllegalArgumentException e) {
-            Log.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
-            e.printStackTrace();
+            Log_OC.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
+            Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()), Toast.LENGTH_LONG).show();
+            processStopRequest(true);
         }
     }
 
@@ -506,7 +493,15 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     /** Called when media player is done playing current song. */
     public void onCompletion(MediaPlayer player) {
         Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
-        processStopRequest(true);
+        if (mMediaController != null) {
+            // somebody is still bound to the service
+            player.seekTo(0);
+            processPauseRequest();
+            mMediaController.updatePausePlay();
+        } else {
+            // nobody is bound
+            processStopRequest(true);
+        }
         return;
     }
     
@@ -519,7 +514,18 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     public void onPrepared(MediaPlayer player) {
         mState = State.PLAYING;
         updateNotification(String.format(getString(R.string.media_state_playing), mFile.getFileName()));
+        if (mMediaController != null) {
+            mMediaController.setEnabled(true);
+        }
+        player.seekTo(mStartPosition);
         configAndStartMediaPlayer();
+        if (!mPlayOnPrepared) {
+            processPauseRequest();
+        }
+        
+        if (mMediaController != null) {
+            mMediaController.updatePausePlay();
+        }
     }
     
 
@@ -529,9 +535,9 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     @SuppressWarnings("deprecation")
     private void updateNotification(String content) {
         // TODO check if updating the Intent is really necessary
-        Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
-        showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
-        showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
+        Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
+        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, mFile);
+        showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, mAccount);
         showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 
                                                                 (int)System.currentTimeMillis(), 
@@ -539,9 +545,9 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
                                                                 PendingIntent.FLAG_UPDATE_CURRENT);
         mNotification.when = System.currentTimeMillis();
         //mNotification.contentView.setTextViewText(R.id.status_text, content);
-        String ticker = "ownCloud MusicPlayer";
+        String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
         mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
-        mNotificationManager.notify(NOTIFICATION_ID, mNotification);
+        mNotificationManager.notify(R.string.media_notif_ticker, mNotification);
     }
 
     
@@ -566,9 +572,9 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
 
         
         /// includes a pending intent in the notification showing the details view of the file
-        Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
-        showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, mFile);
-        showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount);
+        Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
+        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, mFile);
+        showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, mAccount);
         showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 
                                                                 (int)System.currentTimeMillis(), 
@@ -577,9 +583,9 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         
         
         //mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
-        String ticker = "ownCloud MusicPlayer";
+        String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
         mNotification.setLatestEventInfo(getApplicationContext(), ticker, content, mNotification.contentIntent);
-        startForeground(NOTIFICATION_ID, mNotification);
+        startForeground(R.string.media_notif_ticker, mNotification);
         
     }
 
@@ -589,19 +595,15 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
      * Warns the user about the error and resets the media player.
      */
     public boolean onError(MediaPlayer mp, int what, int extra) {
-        // TODO FOLLOW HERE!!!!!!
+        Log_OC.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
         
-        Toast.makeText(getApplicationContext(), "Media player error! Resetting.",
-            Toast.LENGTH_SHORT).show();
-        Log.e(TAG, "Error: what=" + String.valueOf(what) + ", extra=" + String.valueOf(extra));
-
-        mState = State.STOPPED;
-        releaseResources(true);
-        giveUpAudioFocus();
+        String message = getMessageForMediaError(this, what, extra);
+        Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
+        
+        processStopRequest(true);
         return true; 
     }
 
-    
     /**
      * Called by the system when another app tries to play some sound.
      * 
@@ -611,9 +613,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
     public void onAudioFocusChange(int focusChange) {
         if (focusChange > 0) {
             // focus gain; check AudioManager.AUDIOFOCUS_* values
-            Toast.makeText(getApplicationContext(), "gained audio focus.", Toast.LENGTH_SHORT).show();
             mAudioFocus = AudioFocus.FOCUS;
-
             // restart media player with new focus settings
             if (mState == State.PLAYING)
                 configAndStartMediaPlayer();
@@ -621,10 +621,7 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         } else if (focusChange < 0) {
             // focus loss; check AudioManager.AUDIOFOCUS_* values
             boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
-            Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" :
-                    "no duck"), Toast.LENGTH_SHORT).show();
                 mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
-
                 // start/restart/pause media player with new focus settings
                 if (mPlayer != null && mPlayer.isPlaying())
                     configAndStartMediaPlayer();
@@ -642,13 +639,71 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         mState = State.STOPPED;
         releaseResources(true);
         giveUpAudioFocus();
+        super.onDestroy();
     }
     
 
+    /**
+     * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService. 
+     */
     @Override
-    public IBinder onBind(Intent arg0) {
-        // TODO provide a binding API? may we use a service to play VIDEO?
-        return null;
+    public IBinder onBind(Intent arg) {
+        return mBinder;
+    }
+    
+    
+    /**
+     * Called when ALL the bound clients were onbound.
+     * 
+     * The service is destroyed if playback stopped or paused
+     */
+    @Override
+    public boolean onUnbind(Intent intent) {
+        if (mState == State.PAUSED || mState == State.STOPPED)  {
+            processStopRequest(false);
+        }
+        return false;   // not accepting rebinding (default behaviour)
+    }
+
+
+    /**
+     * Accesses the current MediaPlayer instance in the service.
+     * 
+     * To be handled carefully. Visibility is protected to be accessed only 
+     * 
+     * @return Current MediaPlayer instance handled by MediaService.
+     */
+    protected MediaPlayer getPlayer() {
+        return mPlayer;
+    }
+
+
+    /**
+     * Accesses the current OCFile loaded in the service.
+     * 
+     * @return  The current OCFile loaded in the service.
+     */
+    protected OCFile getCurrentFile() {
+        return mFile;
+    }
+
+    
+    /**
+     * Accesses the current {@link State} of the MediaService.
+     * 
+     * @return  The current {@link State} of the MediaService.
+     */
+    protected State getState() {
+        return mState;
+    }
+
+
+    protected void setMediaContoller(MediaControlView mediaController) {
+        mMediaController = mediaController;
+    }
+
+    protected MediaControlView getMediaController() {
+        return mMediaController;
     }
 
 }