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;
+ }
+ }
+ }
+}