Clean up ringer code.

- Ringer now uses a reentrant lock to protect mHandler.
- mRingtone is only set from the handler thread.
- The handler thread is kept alive forever until the ringer is stopped and
  there exist no pending messages on the ringer handler thread.
- Added ringer audio mode (until it is moved to AudioManager when that is
  added).

Change-Id: Iaa93f92a9d9556815a4a46580d40cb45f473dbb5
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
index fce29da..bca0d15 100644
--- a/src/com/android/telecomm/Ringer.java
+++ b/src/com/android/telecomm/Ringer.java
@@ -16,13 +16,17 @@
 
 package com.android.telecomm;
 
-import android.media.Ringtone;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
 import android.media.RingtoneManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-import android.provider.Settings;
-import android.util.Log;
+import android.net.Uri;
+
+import com.google.common.base.Preconditions;
+
+import java.io.IOException;
 
 /**
  * Controls ringing and vibration for incoming calls.
@@ -30,120 +34,239 @@
  * TODO(santoscordon): Consider moving all ringing responsibility to InCall app as an implementation
  * within InCallServiceBase.
  */
-final class Ringer {
+final class Ringer implements OnErrorListener, OnPreparedListener {
+    // States for the Ringer.
+    /** Actively playing the ringer. */
+    private static final int RINGING = 1;
 
-    private static final String TAG = Ringer.class.getSimpleName();
+    /** Ringer currently stopped. */
+    private static final int STOPPED = 2;
 
-    // Message codes used with {@link #mRingtoneHandler}.
-    private static final int EVENT_PLAY_RING = 1;
-    private static final int EVENT_STOP_RING = 2;
+    /** {@link #mMediaPlayer} is preparing, expected to ring once prepared. */
+    private static final int PREPARING_WITH_RING = 3;
+
+    /** {@link #mMediaPlayer} is preparing, expected to stop once prepared. */
+    private static final int PREPARING_WITH_STOP = 4;
 
     /**
-     * Handler used to send messages to the ringtone-playing thread.
+     * The current state of the ringer.
      */
-    private Handler mRingtoneHandler;
+    private int mState = STOPPED;
 
-    /**
-     * The active ringtone. Accessed only from the thread looping {@link #mRingtoneHandler}.
-     */
-    private Ringtone mRingtone;
+    /** The active media player for the ringer. */
+    private MediaPlayer mMediaPlayer;
 
     /**
      * Starts the vibration, ringer, and/or call-waiting tone.
+     * TODO(santoscordon): vibration and call-waiting tone.
      */
     void startRinging() {
-        // TODO(santoscordon): Double-check that we want to play the ringtone. e.g., don't play if
-        // the volume is currently set to 0.
-
-        ThreadUtil.checkOnMainThread();
-        Handler handler = getRingtoneHandler();
-
-        Log.d(TAG, "Posting play");
-        handler.obtainMessage(EVENT_PLAY_RING, getCurrentRingtone()).sendToTarget();
+        // Check if we are muted before playing the ringer.
+        if (getAudioManager().getStreamVolume(AudioManager.STREAM_RING) > 0) {
+            moveToState(RINGING);
+        } else {
+            Log.d(this, "Ringer play skipped due to muted volume.");
+        }
     }
 
     /**
      * Stops the vibration, ringer, and/or call-waiting tone.
      */
     void stopRinging() {
-        ThreadUtil.checkOnMainThread();
-        if (mRingtoneHandler != null) {
-            Log.d(TAG, "Posting stop");
-            mRingtoneHandler.sendEmptyMessage(EVENT_STOP_RING);
-            mRingtoneHandler = null;
-        }
+        moveToState(STOPPED);
     }
 
     /**
-     * Returns the handler to use for playing ringtones.
+     * Handles asynchronous media player "prepared" response by playing the ringer if we are
+     * still expected to or uninitializing it if we've been asked to stop.
+     *
+     * {@inheritDoc}
      */
-    private Handler getRingtoneHandler() {
-        if (mRingtoneHandler == null) {
-            // TODO(santoscordon): Clean this up. Needs more investigation for multi-incoming calls
-            // and this multiple thread approach.
-            HandlerThread thread = new HandlerThread("ringer");
-            thread.start();
+    @Override
+    public void onPrepared(MediaPlayer mediaPlayer) {
+        Preconditions.checkState(mMediaPlayer == null);
 
-            mRingtoneHandler = new Handler(thread.getLooper()) {
-                @Override
-                public void handleMessage(Message msg) {
-                    switch(msg.what) {
-                        case EVENT_PLAY_RING:
-                            handlePlayRingtone(this, (Ringtone) msg.obj);
-                            break;
-                        case EVENT_STOP_RING:
-                            handleStopRingtone(this);
-                            break;
-                    }
+        // See {@link #moveToState} for state transitions.
+        if (PREPARING_WITH_RING == mState) {
+            Log.i(this, "Playing the ringer.");
+            setRingerAudioMode();
+            mMediaPlayer = mediaPlayer;
+            mMediaPlayer.start();
+            setState(RINGING);
+        } else if (PREPARING_WITH_STOP == mState) {
+            mediaPlayer.release();
+            setState(STOPPED);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
+        Log.i(this, "Mediaplayer failed to initialize. What: %d, extra: %d.", what, extra);
+        resetMediaPlayer();
+        setState(STOPPED);
+        return true;
+    }
+
+    /**
+     * Transitions the state of the ringer. State machine below. Any missing arrows imply that the
+     * state remains the same (e.g., (r) on RING state keeps it at RING state).
+     *
+     * +----------------(s)----------------------------+
+     * |                                               |
+     * +----------------(e)-------+                    |
+     * |                          |                    |
+     * +-> STOPPED -(r)-> PREPARING_WITH_RING +-(p)-> RING
+     *       ^                    ^           |
+     *       |                    |           |
+     *     (p,e)                 (r)          |
+     *       |                    |           +-(s)-> PREPARING_WITH_STOP
+     *       |                    |                      | |
+     *       |                    +----------------------+ |
+     *       +---------------------------------------------+
+     *
+     * STOPPED - Ringer completely stopped, like its initial state.
+     * PREPARING_TO_RING - Media player preparing asynchronously to start ringing.
+     * RINGING - The ringtone is currently playing.
+     * PREPARING_TO_STOP - Media player is still preparing, but we've already been asked to stop.
+     *
+     * (r) - {@link #startRinging}
+     * (s) - {@link #stopRinging}
+     * (p) - {@link #onPrepared}
+     * (e) - {@link #onError}
+     */
+    private void moveToState(int newState) {
+        // Only this method sets PREPARING_* states.
+        Preconditions.checkState(newState == RINGING || newState == STOPPED);
+
+        if (newState == mState) {
+            return;
+        }
+
+        if (RINGING == newState) {
+            if (STOPPED == mState) {
+                // If we are stopped, we need to preparing the media player and wait for it to
+                // start the ring. New state set by prepareForRinging.
+                if (prepareForRinging()) {
+                    setState(PREPARING_WITH_RING);
                 }
-            };
-        }
-        return mRingtoneHandler;
-    }
-
-    /**
-     * @return The user's currently-selected ringtone.
-     */
-    private Ringtone getCurrentRingtone() {
-        // TODO(santoscordon): Needs support for custom ringtones.
-        return RingtoneManager.getRingtone(
-                TelecommApp.getInstance(), Settings.System.DEFAULT_RINGTONE_URI);
-    }
-
-    /**
-     * Plays the ringtone. Processed by {@link #mRingtoneHandler}.
-     *
-     * @param handler The handler that invoked this method.
-     */
-    private void handlePlayRingtone(Handler handler, Ringtone ringtone) {
-        ThreadUtil.checkNotOnMainThread();
-        // Verify that we haven't been asked to stop the ringtone before we start playing it.
-        if (!handler.hasMessages(EVENT_STOP_RING)) {
-
-            // Check to see if a ringtone already exists and is playing.
-            if (mRingtone != null && mRingtone.isPlaying()) {
-                mRingtone.stop();
+            } else if (PREPARING_WITH_STOP == mState) {
+                // We are currently preparing the media player, but expect it to put the ringer into
+                // stop once prepared...change that to ring.
+                setState(PREPARING_WITH_RING);
             }
-            mRingtone = ringtone;
-            mRingtone.play();
+        } else if (STOPPED == newState) {
+            if (RINGING == mState) {
+                // We are currently ringing, so just stop it.
+                stopPlayingRinger();
+                setState(STOPPED);
+            } else if (PREPARING_WITH_RING == mState) {
+                // We are preparing the media player, make sure that when it is finished, it moves
+                // to STOPPED instead of ringing.
+                setState(PREPARING_WITH_STOP);
+            }
         }
-
-        // TODO(santoscordon): Requires reposting EVENT_PLAY_RINGTONE in the case where the ringtone
-        // ends. This method only plays one loop of the ringtone.
     }
 
     /**
-     * Stops the ringtone and cleans up references.
+     * Sets the ringer state and checks the current thread.
      *
-     * @param handler The handler that invoked this method.
+     * @param newState The new state to set.
      */
-    private void handleStopRingtone(Handler handler) {
-        ThreadUtil.checkNotOnMainThread();
-        if (mRingtone != null) {
-            mRingtone.stop();
-            mRingtone = null;
+    private void setState(int newState) {
+        ThreadUtil.checkOnMainThread();
+        Log.v(this, "setState, %d -> %d", mState, newState);
+        mState = newState;
+    }
+
+    /**
+     * Starts media player's asynchronous prepare. Response returned in either {@link #onError} or
+     * {@link #onPrepared}.
+     *
+     * @return True if the prepare was successfully started.
+     */
+    private boolean prepareForRinging() {
+        Log.i(this, "Preparing the ringer.");
+
+        Uri ringtoneUri = getCurrentRingtoneUri();
+        if (ringtoneUri == null) {
+            Log.e(this, null, "Ringtone not set.");
+            return false;
         }
 
-        handler.getLooper().quitSafely();
+        MediaPlayer mediaPlayer = new MediaPlayer();
+        mediaPlayer.setOnErrorListener(this);
+        mediaPlayer.setOnPreparedListener(this);
+        mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
+
+        try {
+            mediaPlayer.setDataSource(TelecommApp.getInstance(), ringtoneUri);
+            mediaPlayer.prepareAsync();
+            return true;
+        } catch (IOException e) {
+            mediaPlayer.reset();
+            mediaPlayer.release();
+
+            Log.e(this, e, "Failed to initialize media player for ringer: %s.", ringtoneUri);
+            return false;
+        }
+    }
+
+    /**
+     * Stops and uninitializes the media player.
+     */
+    private void stopPlayingRinger() {
+        Preconditions.checkNotNull(mMediaPlayer);
+        Log.i(this, "Stopping the ringer.");
+
+        resetMediaPlayer();
+        unsetRingerAudioMode();
+    }
+
+    /**
+     * Stops and uninitializes the media player.
+     */
+    private void resetMediaPlayer() {
+        if (mMediaPlayer != null) {
+            // Ringtone.java does not do stop() before release, but it's safer to do so and none of
+            // the documentation suggests that stop() should be skipped.
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+        }
+    }
+
+    /**
+     * @return The default ringtone Uri.
+     */
+    private Uri getCurrentRingtoneUri() {
+        return RingtoneManager.getActualDefaultRingtoneUri(
+                TelecommApp.getInstance(), RingtoneManager.TYPE_RINGTONE);
+    }
+
+    /**
+     * Sets the audio mode for playing the ringtone.
+     */
+    private void setRingerAudioMode() {
+        AudioManager audioManager = getAudioManager();
+        audioManager.requestAudioFocusForCall(
+                AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        audioManager.setMode(AudioManager.MODE_RINGTONE);
+    }
+
+    /**
+     * Returns the audio mode to the normal state after ringing.
+     */
+    private void unsetRingerAudioMode() {
+        AudioManager audioManager = getAudioManager();
+        audioManager.setMode(AudioManager.MODE_NORMAL);
+        audioManager.abandonAudioFocusForCall();
+    }
+
+    /**
+     * Returns the system audio manager.
+     */
+    private AudioManager getAudioManager() {
+        return (AudioManager) TelecommApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
     }
 }