Adding ringback tone.

Adds InCallTonePlayer for playing generic tone. Copied mostly from
CallNotifier.InCallTonePlayer.

Adds RingbackPlayer for starting and stopping the ringback tone as
appropriate while switching between forground calls.

Change-Id: I02b82e3bb23ee64d80b9c0b3b7b5d00edd0361e8
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index b8d980b..bf41f70 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -34,6 +34,7 @@
     private CallAudioState mAudioState;
     private int mAudioFocusStreamType;
     private boolean mIsRinging;
+    private boolean mIsTonePlaying;
     private boolean mWasSpeakerOn;
 
     CallAudioManager() {
@@ -135,6 +136,23 @@
     }
 
     /**
+     * Sets the tone playing status. Some tones can play even when there are no live calls and this
+     * status indicates that we should keep audio focus even for tones that play beyond the life of
+     * calls.
+     *
+     * @param isPlayingNew The status to set.
+     */
+    void setIsTonePlaying(boolean isPlayingNew) {
+        ThreadUtil.checkOnMainThread();
+
+        if (mIsTonePlaying != isPlayingNew) {
+            Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
+            mIsTonePlaying = isPlayingNew;
+            updateAudioStreamAndMode();
+        }
+    }
+
+    /**
       * Updates the audio route when the headset plugged in state changes. For example, if audio is
       * being routed over speakerphone and a headset is plugged in then switch to wired headset.
       */
@@ -185,7 +203,8 @@
     }
 
     private void updateAudioStreamAndMode() {
-        Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b", mIsRinging);
+        Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
+                mIsTonePlaying);
         if (mIsRinging) {
             requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
         } else {
@@ -194,6 +213,10 @@
                 int mode = TelephonyUtil.isCurrentlyPSTNCall(call) ?
                         AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION;
                 requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
+            } else if (mIsTonePlaying) {
+                // There is no call, however, we are still playing a tone, so keep focus.
+                requestAudioFocusAndSetMode(
+                        AudioManager.STREAM_VOICE_CALL, AudioManager.MODE_IN_COMMUNICATION);
             } else {
                 abandonAudioFocus();
             }
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 114dd67..f1bb109 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -30,9 +30,11 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Singleton.
@@ -43,6 +45,7 @@
  */
 public final class CallsManager {
 
+    // TODO(santoscordon): Consider renaming this CallsManagerPlugin.
     interface CallsManagerListener {
         void onCallAdded(Call call);
         void onCallRemoved(Call call);
@@ -71,15 +74,9 @@
      */
     private Call mForegroundCall;
 
-    private final CallsManagerListener mCallLogManager;
-
-    private final CallsManagerListener mPhoneStateBroadcaster;
-
     private final CallAudioManager mCallAudioManager;
 
-    private final CallsManagerListener mInCallController;
-
-    private final Ringer mRinger;
+    private final Set<CallsManagerListener> mListeners = Sets.newHashSet();
 
     private final List<OutgoingCallValidator> mOutgoingCallValidators = Lists.newArrayList();
 
@@ -90,11 +87,15 @@
      */
     private CallsManager() {
         mSwitchboard = new Switchboard(this);
-        mCallLogManager = new CallLogManager(TelecommApp.getInstance());
-        mPhoneStateBroadcaster = new PhoneStateBroadcaster();
+
         mCallAudioManager = new CallAudioManager();
-        mInCallController = new InCallController();
-        mRinger = new Ringer(mCallAudioManager);
+
+        mListeners.add(new CallLogManager(TelecommApp.getInstance()));
+        mListeners.add(new PhoneStateBroadcaster());
+        mListeners.add(new InCallController());
+        mListeners.add(new Ringer(mCallAudioManager));
+        mListeners.add(new RingbackPlayer(this, new InCallTonePlayer.Factory(mCallAudioManager)));
+        mListeners.add(mCallAudioManager);
     }
 
     static CallsManager getInstance() {
@@ -246,11 +247,9 @@
         if (call == null) {
             Log.i(this, "Request to answer a non-existent call %s", callId);
         } else {
-            mCallLogManager.onIncomingCallAnswered(call);
-            mPhoneStateBroadcaster.onIncomingCallAnswered(call);
-            mCallAudioManager.onIncomingCallAnswered(call);
-            mInCallController.onIncomingCallAnswered(call);
-            mRinger.onIncomingCallAnswered(call);
+            for (CallsManagerListener listener : mListeners) {
+                listener.onIncomingCallAnswered(call);
+            }
 
             // We do not update the UI until we get confirmation of the answer() through
             // {@link #markCallAsActive}. However, if we ever change that to look more responsive,
@@ -272,11 +271,9 @@
         if (call == null) {
             Log.i(this, "Request to reject a non-existent call %s", callId);
         } else {
-            mCallLogManager.onIncomingCallRejected(call);
-            mPhoneStateBroadcaster.onIncomingCallRejected(call);
-            mCallAudioManager.onIncomingCallRejected(call);
-            mInCallController.onIncomingCallRejected(call);
-            mRinger.onIncomingCallRejected(call);
+            for (CallsManagerListener listener : mListeners) {
+                listener.onIncomingCallRejected(call);
+            }
 
             call.reject();
         }
@@ -348,11 +345,9 @@
     /** Called when the audio state changes. */
     void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
         Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
-        mCallLogManager.onAudioStateChanged(oldAudioState, newAudioState);
-        mPhoneStateBroadcaster.onAudioStateChanged(oldAudioState, newAudioState);
-        mCallAudioManager.onAudioStateChanged(oldAudioState, newAudioState);
-        mInCallController.onAudioStateChanged(oldAudioState, newAudioState);
-        mRinger.onAudioStateChanged(oldAudioState, newAudioState);
+        for (CallsManagerListener listener : mListeners) {
+            listener.onAudioStateChanged(oldAudioState, newAudioState);
+        }
     }
 
     void markCallAsRinging(String callId) {
@@ -404,23 +399,17 @@
      */
     private void addCall(Call call) {
         mCalls.put(call.getId(), call);
-
-        mCallLogManager.onCallAdded(call);
-        mPhoneStateBroadcaster.onCallAdded(call);
-        mCallAudioManager.onCallAdded(call);
-        mInCallController.onCallAdded(call);
-        mRinger.onCallAdded(call);
+        for (CallsManagerListener listener : mListeners) {
+            listener.onCallAdded(call);
+        }
         updateForegroundCall();
     }
 
     private void removeCall(Call call) {
         call.clearCallService();
-
-        mCallLogManager.onCallRemoved(call);
-        mPhoneStateBroadcaster.onCallRemoved(call);
-        mCallAudioManager.onCallRemoved(call);
-        mInCallController.onCallRemoved(call);
-        mRinger.onCallRemoved(call);
+        for (CallsManagerListener listener : mListeners) {
+            listener.onCallRemoved(call);
+        }
         updateForegroundCall();
     }
 
@@ -463,11 +452,9 @@
 
             // Only broadcast state change for calls that are being tracked.
             if (mCalls.containsKey(call.getId())) {
-                mCallLogManager.onCallStateChanged(call, oldState, newState);
-                mPhoneStateBroadcaster.onCallStateChanged(call, oldState, newState);
-                mCallAudioManager.onCallStateChanged(call, oldState, newState);
-                mInCallController.onCallStateChanged(call, oldState, newState);
-                mRinger.onCallStateChanged(call, oldState, newState);
+                for (CallsManagerListener listener : mListeners) {
+                    listener.onCallStateChanged(call, oldState, newState);
+                }
                 updateForegroundCall();
             }
         }
@@ -494,11 +481,9 @@
             Call oldForegroundCall = mForegroundCall;
             mForegroundCall = newForegroundCall;
 
-            mCallLogManager.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
-            mPhoneStateBroadcaster.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
-            mCallAudioManager.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
-            mInCallController.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
-            mRinger.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            for (CallsManagerListener listener : mListeners) {
+                listener.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            }
         }
     }
 }
diff --git a/src/com/android/telecomm/InCallTonePlayer.java b/src/com/android/telecomm/InCallTonePlayer.java
new file mode 100644
index 0000000..c00e1cf
--- /dev/null
+++ b/src/com/android/telecomm/InCallTonePlayer.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Play a call-related tone (ringback, busy signal, etc.) through ToneGenerator. To use, create an
+ * instance using InCallTonePlayer.Factory (passing in the TONE_* constant for the tone you want)
+ * and start() it. Implemented on top of {@link Thread} so that the tone plays in its own thread.
+ */
+public final class InCallTonePlayer extends Thread {
+
+    /**
+     * Factory used to create InCallTonePlayers. Exists to aid with testing mocks.
+     */
+    public static class Factory {
+        private final CallAudioManager mCallAudioManager;
+
+        Factory(CallAudioManager callAudioManager) {
+            mCallAudioManager = callAudioManager;
+        }
+
+        InCallTonePlayer createPlayer(int tone) {
+            return new InCallTonePlayer(tone, mCallAudioManager);
+        }
+    }
+
+    // The possible tones that we can play.
+    public static final int TONE_NONE = 0;
+    public static final int TONE_RING_BACK = 1;
+
+    // The tone volume relative to other sounds in the stream.
+    private static final int RELATIVE_VOLUME_EMERGENCY = 100;
+    private static final int RELATIVE_VOLUME_HIPRI = 80;
+    private static final int RELATIVE_VOLUME_LOPRI = 50;
+
+    // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
+    // value for a tone is exact duration of the tone itself.
+    private static final int TIMEOUT_BUFFER_MS = 20;
+
+    // The tone state.
+    private static final int STATE_OFF = 0;
+    private static final int STATE_ON = 1;
+    private static final int STATE_STOPPED = 2;
+
+    /**
+     * Keeps count of the number of actively playing tones so that we can notify CallAudioManager
+     * when we need focus and when it can be release. This should only be manipulated from the main
+     * thread.
+     */
+    private static int sTonesPlaying = 0;
+
+    private final CallAudioManager mCallAudioManager;
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+    /** The ID of the tone to play. */
+    private final int mToneId;
+
+    /** Current state of the tone player. */
+    private int mState;
+
+    /**
+     * Initializes the tone player. Private; use the {@link Factory} to create tone players.
+     *
+     * @param toneId ID of the tone to play, see TONE_* constants.
+     */
+    private InCallTonePlayer(int toneId, CallAudioManager callAudioManager) {
+        mState = STATE_OFF;
+        mToneId = toneId;
+        mCallAudioManager = callAudioManager;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void run() {
+        ToneGenerator toneGenerator = null;
+        try {
+            Log.d(this, "run(toneId = %s)", mToneId);
+
+            final int toneType;  // Passed to ToneGenerator.startTone.
+            final int toneVolume;  // Passed to the ToneGenerator constructor.
+            final int toneLengthMs;
+
+            switch (mToneId) {
+                case TONE_RING_BACK:
+                    toneType = ToneGenerator.TONE_SUP_RINGTONE;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMs = Integer.MAX_VALUE - TIMEOUT_BUFFER_MS;
+                    break;
+                default:
+                    throw new IllegalStateException("Bad toneId: " + mToneId);
+            }
+
+            // TODO(santoscordon): Bluetooth should be set manually (STREAM_BLUETOOTH_SCO) for tone
+            // generator.
+            int stream = AudioManager.STREAM_VOICE_CALL;
+
+            // If the ToneGenerator creation fails, just continue without it. It is a local audio
+            // signal, and is not as important.
+            try {
+                Log.v(this, "Creating generator");
+                toneGenerator = new ToneGenerator(stream, toneVolume);
+            } catch (RuntimeException e) {
+                Log.w(this, "Failed to create ToneGenerator.", e);
+                return;
+            }
+
+            // TODO(santoscordon): Certain CDMA tones need to check the ringer-volume state before
+            // playing. See CallNotifier.InCallTonePlayer.
+
+            // TODO(santoscordon): Some tones play through the end of a call so we need to inform
+            // CallAudioManager that we want focus the same way that Ringer does.
+
+            synchronized (this) {
+                if (mState != STATE_STOPPED) {
+                    mState = STATE_ON;
+                    toneGenerator.startTone(toneType);
+                    try {
+                        Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
+                                toneLengthMs + TIMEOUT_BUFFER_MS);
+                        wait(toneLengthMs + TIMEOUT_BUFFER_MS);
+                    } catch (InterruptedException e) {
+                        Log.w(this, "wait interrupted", e);
+                    }
+                }
+            }
+            mState = STATE_OFF;
+        } finally {
+            if (toneGenerator != null) {
+                toneGenerator.release();
+            }
+            cleanUpTonePlayer();
+        }
+    }
+
+    void startTone() {
+        ThreadUtil.checkOnMainThread();
+
+        sTonesPlaying++;
+        if (sTonesPlaying == 1) {
+            mCallAudioManager.setIsTonePlaying(true);
+        }
+
+        start();
+    }
+
+    /**
+     * Stops the tone.
+     */
+    void stopTone() {
+        synchronized (this) {
+            if (mState == STATE_ON) {
+                Log.d(this, "Stopping the tone %d.", mToneId);
+                notify();
+            }
+            mState = STATE_STOPPED;
+        }
+    }
+
+    private void cleanUpTonePlayer() {
+        // Release focus on the main thread.
+        mMainThreadHandler.post(new Runnable() {
+            @Override public void run() {
+                if (sTonesPlaying == 0) {
+                    Log.wtf(this, "Over-releasing focus for tone player.");
+                } else if (--sTonesPlaying == 0) {
+                    mCallAudioManager.setIsTonePlaying(false);
+                }
+            }
+        });
+    }
+}
diff --git a/src/com/android/telecomm/RingbackPlayer.java b/src/com/android/telecomm/RingbackPlayer.java
new file mode 100644
index 0000000..5e0834f
--- /dev/null
+++ b/src/com/android/telecomm/RingbackPlayer.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm;
+
+import android.telecomm.CallState;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Plays ringback tones. Ringback is different from other tones because it operates as the current
+ * audio for a call, whereas most tones play as simple timed events. This means ringback must be
+ * able to turn off and on as the user switches between calls. This is why it is implemented as its
+ * own class.
+ */
+class RingbackPlayer extends CallsManagerListenerBase {
+
+    private final CallsManager mCallsManager;
+
+    private final InCallTonePlayer.Factory mPlayerFactory;
+
+    /**
+     * The ID of the current call for which the ringback tone is being played.
+     */
+    private String mCallId;
+
+    /**
+     * The currently active player.
+     */
+    private InCallTonePlayer mTonePlayer;
+
+    RingbackPlayer(CallsManager callsManager, InCallTonePlayer.Factory playerFactory) {
+        mCallsManager = callsManager;
+        mPlayerFactory = playerFactory;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        // Only operate on the foreground call.
+        if (mCallsManager.getForegroundCall() == call) {
+
+            // Treat as ending or begining dialing based on the state transition.
+            if (newState == CallState.DIALING) {
+                startRingbackForCall(call);
+            } else if (oldState == CallState.DIALING) {
+                stopRingbackForCall(call);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        if (oldForegroundCall != null) {
+            stopRingbackForCall(oldForegroundCall);
+        }
+
+        if (newForegroundCall != null && newForegroundCall.getState() == CallState.DIALING) {
+            startRingbackForCall(newForegroundCall);
+        }
+    }
+
+    /**
+     * Starts ringback for the specified dialing call as needed.
+     *
+     * @param call The call for which to ringback.
+     */
+    private void startRingbackForCall(Call call) {
+        Preconditions.checkState(call.getState() == CallState.DIALING);
+        ThreadUtil.checkOnMainThread();
+
+        if (mCallId != null) {
+            // We only get here for the foreground call so, there's no reason why there should
+            // exist a current dialing call ID.
+            Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
+        }
+
+        mCallId = call.getId();
+        if (mTonePlayer == null) {
+            Log.d(this, "Playing the ringback tone.");
+            mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+            mTonePlayer.startTone();
+        }
+    }
+
+    /**
+     * Stops the ringback for the specified dialing call as needed.
+     *
+     * @param call The call for which to stop ringback.
+     */
+    private void stopRingbackForCall(Call call) {
+        ThreadUtil.checkOnMainThread();
+
+        if (mCallId != null && mCallId.equals(call.getId())) {
+            // The foreground call is no longer dialing or is no longer the foreground call. In
+            // either case, stop the ringback tone.
+            mCallId = null;
+
+            if (mTonePlayer == null) {
+                Log.w(this, "No player found to stop.");
+            } else {
+                Log.i(this, "Stopping the ringback tone.");
+                mTonePlayer.stopTone();
+                mTonePlayer = null;
+            }
+        }
+    }
+}