Add audio mode and audio focus support to Telecomm
This CL adds support for:
- audio focus
- switching to speaker phone
- mute
- wired headset
Bluetooth support is coming in a different CL.
Change-Id: Iaf4013ea747548451cff45b48bcbb5b1dd1b8498
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 568d7c6..0789135 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -20,6 +20,7 @@
import android.telecomm.CallInfo;
import android.telecomm.CallState;
import android.telecomm.GatewayInfo;
+import android.telephony.PhoneNumberUtils;
import com.google.android.collect.Sets;
import com.google.common.base.Preconditions;
@@ -80,6 +81,8 @@
*/
private Set<CallServiceWrapper> mIncompatibleCallServices;
+ private boolean mIsEmergencyCall;
+
/**
* Creates an empty call object with a unique call ID.
*
@@ -100,7 +103,7 @@
Call(Uri handle, ContactInfo contactInfo, GatewayInfo gatewayInfo, boolean isIncoming) {
mId = UUID.randomUUID().toString(); // UUIDs should provide sufficient uniqueness.
mState = CallState.NEW;
- mHandle = handle;
+ setHandle(handle);
mContactInfo = contactInfo;
mGatewayInfo = gatewayInfo;
mIsIncoming = isIncoming;
@@ -141,6 +144,12 @@
void setHandle(Uri handle) {
mHandle = handle;
+ mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(
+ mHandle.getSchemeSpecificPart(), TelecommApp.getInstance());
+ }
+
+ boolean isEmergencyCall() {
+ return mIsEmergencyCall;
}
/**
@@ -332,6 +341,19 @@
return new CallInfo(mId, mState, mHandle, mGatewayInfo);
}
+ /** Checks if this is a live call or not. */
+ boolean isAlive() {
+ switch (mState) {
+ case NEW:
+ case RINGING:
+ case DISCONNECTED:
+ case ABORTED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
/**
* @return True if the call is ringing, else logs the action name.
*/
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index 26987ba..b8d980b 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -18,176 +18,270 @@
import android.content.Context;
import android.media.AudioManager;
+import android.telecomm.CallAudioState;
import android.telecomm.CallState;
-import com.google.common.collect.Lists;
-
-import java.util.List;
+import com.google.common.base.Preconditions;
/**
* This class manages audio modes, streams and other properties.
*/
final class CallAudioManager extends CallsManagerListenerBase {
- private AsyncRingtonePlayer mRinger = new AsyncRingtonePlayer();
+ private static final int STREAM_NONE = -1;
- private boolean mHasAudioFocus = false;
+ private final AudioManager mAudioManager;
+ private final WiredHeadsetManager mWiredHeadsetManager;
+ private CallAudioState mAudioState;
+ private int mAudioFocusStreamType;
+ private boolean mIsRinging;
+ private boolean mWasSpeakerOn;
- /**
- * Used to keep ordering of unanswered incoming calls. The existence of multiple call services
- * means that there can easily exist multiple incoming calls and explicit ordering is useful for
- * maintaining the proper state of the ringer.
- */
- private final List<String> mUnansweredCallIds = Lists.newLinkedList();
+ CallAudioManager() {
+ Context context = TelecommApp.getInstance();
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mWiredHeadsetManager = new WiredHeadsetManager(this);
+ mAudioState = getInitialAudioState();
+ mAudioFocusStreamType = STREAM_NONE;
+ }
- /**
- * Denotes when the ringer is disabled. This is useful in temporarily disabling the ringer when
- * the a call is answered/rejected by the user, but the call hasn't actually moved out of the
- * ringing state.
- */
- private boolean mIsRingingDisabled = false;
+ CallAudioState getAudioState() {
+ return mAudioState;
+ }
@Override
public void onCallAdded(Call call) {
- if (call.getState() == CallState.RINGING) {
- mUnansweredCallIds.add(call.getId());
+ updateAudioStreamAndMode();
+ if (CallsManager.getInstance().getCalls().size() == 1) {
+ Log.v(this, "first call added, reseting system audio to default state");
+ setInitialAudioState();
+ } else if (!call.isIncoming()) {
+ // Unmute new outgoing call.
+ setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
}
- updateAudio();
}
@Override
public void onCallRemoved(Call call) {
- removeFromUnansweredCallIds(call.getId());
- updateAudio();
+ if (CallsManager.getInstance().getCalls().isEmpty()) {
+ Log.v(this, "all calls removed, reseting system audio to default state");
+ setInitialAudioState();
+ }
+ updateAudioStreamAndMode();
}
@Override
public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
- if (oldState == CallState.RINGING) {
- removeFromUnansweredCallIds(call.getId());
- }
-
- updateAudio();
+ updateAudioStreamAndMode();
}
@Override
public void onIncomingCallAnswered(Call call) {
- mIsRingingDisabled = true;
- updateAudio();
+ // Unmute new incoming call.
+ setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
}
@Override
- public void onIncomingCallRejected(Call call) {
- mIsRingingDisabled = true;
- updateAudio();
+ public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+ updateAudioStreamAndMode();
+ // Ensure that the foreground call knows about the latest audio state.
+ updateAudioForForegroundCall();
}
- /**
- * Reads the current state of all calls from CallsManager and sets the appropriate audio modes
- * as well as triggers the start/stop of the ringer.
- */
- private void updateAudio() {
- CallsManager callsManager = CallsManager.getInstance();
+ void mute(boolean shouldMute) {
+ Log.v(this, "mute, shouldMute: %b", shouldMute);
- boolean hasRingingCall = !mIsRingingDisabled && !mUnansweredCallIds.isEmpty();
- boolean hasLiveCall = callsManager.hasCallWithState(CallState.ACTIVE, CallState.DIALING);
-
- int mode = hasRingingCall ? AudioManager.MODE_RINGTONE :
- hasLiveCall ? AudioManager.MODE_IN_CALL :
- AudioManager.MODE_NORMAL;
-
- boolean needsFocus = (mode != AudioManager.MODE_NORMAL);
-
- // Acquiring focus needs to be first, unlike releasing focus, which happens at the end.
- if (needsFocus) {
- acquireFocus(hasRingingCall);
- setMode(mode);
+ // Don't mute if there are any emergency calls.
+ if (CallsManager.getInstance().hasEmergencyCall()) {
+ shouldMute = false;
+ Log.v(this, "ignoring mute for emergency call");
}
- if (hasRingingCall) {
- mRinger.play();
- } else {
- mRinger.stop();
- }
-
- if (!needsFocus && mHasAudioFocus) {
- setMode(AudioManager.MODE_NORMAL);
- releaseFocus();
+ if (mAudioState.isMuted != shouldMute) {
+ setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
}
}
/**
- * Acquires audio focus.
+ * Changed the audio route, for example from earpiece to speaker phone.
*
- * @param isForRinging True if this focus is for playing the ringer.
+ * @param route The new audio route to use. See {@link CallAudioState}.
*/
- private void acquireFocus(boolean isForRinging) {
- if (!mHasAudioFocus) {
- int stream = isForRinging ? AudioManager.STREAM_RING : AudioManager.STREAM_VOICE_CALL;
+ void setAudioRoute(int route) {
+ Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
- AudioManager audioManager = getAudioManager();
- audioManager.requestAudioFocusForCall(stream, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
- audioManager.setMicrophoneMute(false);
- audioManager.setSpeakerphoneOn(false);
- mHasAudioFocus = true;
+ // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+ int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
+
+ // If route is unsupported, do nothing.
+ if ((mAudioState.supportedRouteMask | newRoute) == 0) {
+ Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
+ return;
+ }
+
+ if (mAudioState.route != newRoute) {
+ // Remember the new speaker state so it can be restored when the user plugs and unplugs
+ // a headset.
+ mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
+ setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
+ }
+ }
+
+ void setIsRinging(boolean isRinging) {
+ if (mIsRinging != isRinging) {
+ Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
+ mIsRinging = isRinging;
+ updateAudioStreamAndMode();
}
}
/**
- * Releases focus.
- */
- void releaseFocus() {
- if (mHasAudioFocus) {
- AudioManager audioManager = getAudioManager();
+ * 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.
+ */
+ void onHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ int newRoute = CallAudioState.ROUTE_EARPIECE;
+ if (newIsPluggedIn) {
+ newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+ } else if (mWasSpeakerOn) {
+ Call call = CallsManager.getInstance().getForegroundCall();
+ if (call != null && call.isAlive()) {
+ // Restore the speaker state.
+ newRoute = CallAudioState.ROUTE_SPEAKER;
+ }
+ }
+ setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
+ }
- // Reset speakerphone and mute in case they were changed by telecomm.
- audioManager.setMicrophoneMute(false);
- audioManager.setSpeakerphoneOn(false);
- audioManager.abandonAudioFocusForCall();
+ private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
+ CallAudioState oldAudioState = mAudioState;
+ mAudioState = new CallAudioState(isMuted, route, supportedRouteMask);
+ Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
- mHasAudioFocus = false;
- Log.v(this, "Focus released");
+ // Mute.
+ if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
+ Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
+ mAudioManager.setMicrophoneMute(mAudioState.isMuted);
}
+ // Audio route.
+ if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
+ if (!mAudioManager.isSpeakerphoneOn()) {
+ Log.i(this, "turning speaker phone on");
+ mAudioManager.setSpeakerphoneOn(true);
+ }
+ } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
+ mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
+ // Wired headset and earpiece work the same way
+ if (mAudioManager.isSpeakerphoneOn()) {
+ Log.i(this, "turning speaker phone off");
+ mAudioManager.setSpeakerphoneOn(false);
+ }
+ }
+
+ if (!oldAudioState.equals(mAudioState)) {
+ CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
+ updateAudioForForegroundCall();
+ }
+ }
+
+ private void updateAudioStreamAndMode() {
+ Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b", mIsRinging);
+ if (mIsRinging) {
+ requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
+ } else {
+ Call call = CallsManager.getInstance().getForegroundCall();
+ if (call != null) {
+ int mode = TelephonyUtil.isCurrentlyPSTNCall(call) ?
+ AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION;
+ requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
+ } else {
+ abandonAudioFocus();
+ }
+ }
+ }
+
+ private void requestAudioFocusAndSetMode(int stream, int mode) {
+ Log.v(this, "setSystemAudioStreamAndMode, stream: %d -> %d", mAudioFocusStreamType, stream);
+ Preconditions.checkState(stream != STREAM_NONE);
+
+ // Only request audio focus once. If the stream type changes there's no need to abandon
+ // and re-request audio focus. The system doesn't really care about the stream we requested
+ // focus for so just silently switch.
+ if (mAudioFocusStreamType == STREAM_NONE) {
+ Log.v(this, "requesting audio focus for stream: %d", stream);
+ mAudioManager.requestAudioFocusForCall(stream,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ }
+ mAudioFocusStreamType = stream;
+ setMode(mode);
+ }
+
+ private void abandonAudioFocus() {
+ if (mAudioFocusStreamType != STREAM_NONE) {
+ setMode(AudioManager.MODE_NORMAL);
+ Log.v(this, "abandoning audio focus");
+ mAudioManager.abandonAudioFocusForCall();
+ mAudioFocusStreamType = STREAM_NONE;
+ }
}
/**
* Sets the audio mode.
*
- * @param mode Mode constant from AudioManager.MODE_*.
+ * @param newMode Mode constant from AudioManager.MODE_*.
*/
- void setMode(int mode) {
- if (mHasAudioFocus) {
- AudioManager audioManager = getAudioManager();
- if (mode != audioManager.getMode()) {
- Log.v(this, "Audio mode set to %d.", mode);
- audioManager.setMode(mode);
- Log.v(this, "Audio mode actually set to %d.", audioManager.getMode());
+ private void setMode(int newMode) {
+ Preconditions.checkState(mAudioFocusStreamType != STREAM_NONE);
+ int oldMode = mAudioManager.getMode();
+ Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
+ if (oldMode != newMode) {
+ mAudioManager.setMode(newMode);
+ }
+ }
+
+ private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+ // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+ // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
+ // supported before calling setAudioRoute.
+ if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
+ route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+ if (route == 0) {
+ Log.wtf(this, "One of wired headset or earpiece should always be valid.");
+ // assume earpiece in this case.
+ route = CallAudioState.ROUTE_EARPIECE;
}
+ }
+ return route;
+ }
+
+ private int calculateSupportedRoutes() {
+ int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
} else {
- Log.wtf(this, "Trying to set audio mode to %d without focus.", mode);
+ routeMask |= CallAudioState.ROUTE_EARPIECE;
}
+
+ return routeMask;
}
- /**
- * Removes the specified call from the list of unanswered incoming calls.
- *
- * @param callId The ID of the call.
- */
- private void removeFromUnansweredCallIds(String callId) {
- if (!mUnansweredCallIds.isEmpty()) {
- // If the call is the top-most call, then no longer disable the ringer.
- if (callId.equals(mUnansweredCallIds.get(0))) {
- mIsRingingDisabled = false;
- }
-
- mUnansweredCallIds.remove(callId);
- }
+ private CallAudioState getInitialAudioState() {
+ int supportedRouteMask = calculateSupportedRoutes();
+ return new CallAudioState(false,
+ selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask),
+ supportedRouteMask);
}
- /**
- * Returns the system audio manager.
- */
- private AudioManager getAudioManager() {
- return (AudioManager) TelecommApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
+ private void setInitialAudioState() {
+ CallAudioState audioState = getInitialAudioState();
+ setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
+ }
+
+ private void updateAudioForForegroundCall() {
+ Call call = CallsManager.getInstance().getForegroundCall();
+ if (call != null && call.getCallService() != null) {
+ call.getCallService().onAudioStateChanged(call.getId(), mAudioState);
+ }
}
}
diff --git a/src/com/android/telecomm/CallServiceWrapper.java b/src/com/android/telecomm/CallServiceWrapper.java
index 46e99cb..0722ec6 100644
--- a/src/com/android/telecomm/CallServiceWrapper.java
+++ b/src/com/android/telecomm/CallServiceWrapper.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
+import android.telecomm.CallAudioState;
import android.telecomm.CallInfo;
import android.telecomm.CallService;
import android.telecomm.CallServiceDescriptor;
@@ -147,7 +148,7 @@
}
/** See {@link ICallService#hold}. */
- public void hold(String callId) {
+ void hold(String callId) {
if (isServiceValid("hold")) {
try {
mServiceInterface.hold(callId);
@@ -158,7 +159,7 @@
}
/** See {@link ICallService#unhold}. */
- public void unhold(String callId) {
+ void unhold(String callId) {
if (isServiceValid("unhold")) {
try {
mServiceInterface.unhold(callId);
@@ -168,6 +169,17 @@
}
}
+ /** See {@link ICallService#onAudioStateChanged}. */
+ void onAudioStateChanged(String activeCallId, CallAudioState audioState) {
+ if (isServiceValid("onAudioStateChanged")) {
+ try {
+ mServiceInterface.onAudioStateChanged(activeCallId, audioState);
+ } catch (RemoteException e) {
+ Log.e(this, e, "Failed to update audio state for call %s", activeCallId);
+ }
+ }
+ }
+
/**
* Starts retrieval of details for an incoming call. Details are returned through the
* call-service adapter using the specified call ID. Upon failure, the specified error callback
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 22d9ce6..114dd67 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -18,6 +18,7 @@
import android.net.Uri;
import android.os.Bundle;
+import android.telecomm.CallAudioState;
import android.telecomm.CallInfo;
import android.telecomm.CallServiceDescriptor;
import android.telecomm.CallState;
@@ -49,6 +50,7 @@
void onIncomingCallAnswered(Call call);
void onIncomingCallRejected(Call call);
void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
+ void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
}
private static final CallsManager INSTANCE = new CallsManager();
@@ -73,10 +75,12 @@
private final CallsManagerListener mPhoneStateBroadcaster;
- private final CallsManagerListener mCallAudioManager;
+ private final CallAudioManager mCallAudioManager;
private final CallsManagerListener mInCallController;
+ private final Ringer mRinger;
+
private final List<OutgoingCallValidator> mOutgoingCallValidators = Lists.newArrayList();
private final List<IncomingCallValidator> mIncomingCallValidators = Lists.newArrayList();
@@ -90,6 +94,7 @@
mPhoneStateBroadcaster = new PhoneStateBroadcaster();
mCallAudioManager = new CallAudioManager();
mInCallController = new InCallController();
+ mRinger = new Ringer(mCallAudioManager);
}
static CallsManager getInstance() {
@@ -104,6 +109,19 @@
return mForegroundCall;
}
+ boolean hasEmergencyCall() {
+ for (Call call : mCalls.values()) {
+ if (call.isEmergencyCall()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ CallAudioState getAudioState() {
+ return mCallAudioManager.getAudioState();
+ }
+
/**
* Starts the incoming call sequence by having switchboard gather more information about the
* specified call; using the specified call service descriptor. Upon success, execution returns
@@ -232,6 +250,7 @@
mPhoneStateBroadcaster.onIncomingCallAnswered(call);
mCallAudioManager.onIncomingCallAnswered(call);
mInCallController.onIncomingCallAnswered(call);
+ mRinger.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,
@@ -257,6 +276,7 @@
mPhoneStateBroadcaster.onIncomingCallRejected(call);
mCallAudioManager.onIncomingCallRejected(call);
mInCallController.onIncomingCallRejected(call);
+ mRinger.onIncomingCallRejected(call);
call.reject();
}
@@ -312,6 +332,29 @@
}
}
+ /** Called by the in-call UI to change the mute state. */
+ void mute(boolean shouldMute) {
+ mCallAudioManager.mute(shouldMute);
+ }
+
+ /**
+ * Called by the in-call UI to change the audio route, for example to change from earpiece to
+ * speaker phone.
+ */
+ void setAudioRoute(int route) {
+ mCallAudioManager.setAudioRoute(route);
+ }
+
+ /** 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);
+ }
+
void markCallAsRinging(String callId) {
setCallState(callId, CallState.RINGING);
}
@@ -340,21 +383,6 @@
}
/**
- * @return True if there exists a call with the specific state.
- */
- boolean hasCallWithState(CallState... states) {
- for (Call call : mCalls.values()) {
- for (CallState state : states) {
- if (call.getState() == state) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
* Cleans up any calls currently associated with the specified call service when the
* call-service binder disconnects unexpectedly.
*
@@ -381,6 +409,7 @@
mPhoneStateBroadcaster.onCallAdded(call);
mCallAudioManager.onCallAdded(call);
mInCallController.onCallAdded(call);
+ mRinger.onCallAdded(call);
updateForegroundCall();
}
@@ -391,6 +420,7 @@
mPhoneStateBroadcaster.onCallRemoved(call);
mCallAudioManager.onCallRemoved(call);
mInCallController.onCallRemoved(call);
+ mRinger.onCallRemoved(call);
updateForegroundCall();
}
@@ -437,6 +467,7 @@
mPhoneStateBroadcaster.onCallStateChanged(call, oldState, newState);
mCallAudioManager.onCallStateChanged(call, oldState, newState);
mInCallController.onCallStateChanged(call, oldState, newState);
+ mRinger.onCallStateChanged(call, oldState, newState);
updateForegroundCall();
}
}
@@ -453,7 +484,7 @@
newForegroundCall = call;
break;
}
- if (call.getState() == CallState.ACTIVE) {
+ if (call.isAlive()) {
newForegroundCall = call;
// Don't break in case there's a ringing call that has priority.
}
@@ -467,6 +498,7 @@
mPhoneStateBroadcaster.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
mCallAudioManager.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
mInCallController.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+ mRinger.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
}
}
}
diff --git a/src/com/android/telecomm/CallsManagerListenerBase.java b/src/com/android/telecomm/CallsManagerListenerBase.java
index c81067f..04b78fe 100644
--- a/src/com/android/telecomm/CallsManagerListenerBase.java
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -16,10 +16,11 @@
package com.android.telecomm;
+import android.telecomm.CallAudioState;
import android.telecomm.CallState;
/**
- * Provides a default implementation for listeners for CallsManager.
+ * Provides a default implementation for listeners of CallsManager.
*/
class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
@Override
@@ -45,4 +46,8 @@
@Override
public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
}
+
+ @Override
+ public void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+ }
}
diff --git a/src/com/android/telecomm/InCallAdapter.java b/src/com/android/telecomm/InCallAdapter.java
index 37dc3b9..e61c919 100644
--- a/src/com/android/telecomm/InCallAdapter.java
+++ b/src/com/android/telecomm/InCallAdapter.java
@@ -88,4 +88,24 @@
}
});
}
+
+ /** {@inheritDoc} */
+ @Override
+ public void mute(final boolean shouldMute) {
+ mHandler.post(new Runnable() {
+ @Override public void run() {
+ mCallsManager.mute(shouldMute);
+ }
+ });
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setAudioRoute(final int route) {
+ mHandler.post(new Runnable() {
+ @Override public void run() {
+ mCallsManager.setAudioRoute(route);
+ }
+ });
+ }
}
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 0bfe30d..5c3e3e6 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -22,6 +22,7 @@
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
+import android.telecomm.CallAudioState;
import android.telecomm.CallInfo;
import android.telecomm.CallState;
@@ -138,6 +139,19 @@
}
}
+ @Override
+ public void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+ if (mInCallService != null) {
+ Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
+ newAudioState);
+ try {
+ mInCallService.onAudioStateChanged(newAudioState);
+ } catch (RemoteException e) {
+ Log.e(this, e, "Exception attempting to update audio state.");
+ }
+ }
+ }
+
/**
* Unbinds an existing bound connection to the in-call app.
*/
@@ -198,6 +212,7 @@
for (Call call : calls) {
onCallAdded(call);
}
+ onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
} else {
unbind();
}
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
new file mode 100644
index 0000000..dbf76d5
--- /dev/null
+++ b/src/com/android/telecomm/Ringer.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 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.content.Context;
+import android.media.AudioManager;
+import android.telecomm.CallState;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Controls the ringtone player.
+ */
+final class Ringer extends CallsManagerListenerBase {
+ private final AsyncRingtonePlayer mRingtonePlayer = new AsyncRingtonePlayer();
+
+ /**
+ * Used to keep ordering of unanswered incoming calls. There can easily exist multiple incoming
+ * calls and explicit ordering is useful for maintaining the proper state of the ringer.
+ */
+ private final List<String> mUnansweredCallIds = Lists.newLinkedList();
+
+ private final CallAudioManager mCallAudioManager;
+
+ Ringer(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ if (call.isIncoming() && call.getState() == CallState.RINGING) {
+ if (mUnansweredCallIds.contains(call.getId())) {
+ Log.wtf(this, "New ringing call is already in list of unanswered calls");
+ }
+ mUnansweredCallIds.add(call.getId());
+ if (mUnansweredCallIds.size() == 1) {
+ // Start the ringer if we are the top-most incoming call (the only one in this
+ // case).
+ startRinging();
+ }
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ removeFromUnansweredCallIds(call.getId());
+ }
+
+ @Override
+ public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+ if (newState != CallState.RINGING) {
+ removeFromUnansweredCallIds(call.getId());
+ }
+ }
+
+ @Override
+ public void onIncomingCallAnswered(Call call) {
+ onRespondedToIncomingCall(call);
+ }
+
+ @Override
+ public void onIncomingCallRejected(Call call) {
+ onRespondedToIncomingCall(call);
+ }
+
+ private void onRespondedToIncomingCall(Call call) {
+ // Only stop the ringer if this call is the top-most incoming call.
+ if (!mUnansweredCallIds.isEmpty() && mUnansweredCallIds.get(0).equals(call.getId())) {
+ stopRinging();
+ }
+ }
+
+ /**
+ * Removes the specified call from the list of unanswered incoming calls and updates the ringer
+ * based on the new state of {@link #mUnansweredCallIds}. Safe to call with a call ID that
+ * is not present in the list of incoming calls.
+ *
+ * @param callId The ID of the call.
+ */
+ private void removeFromUnansweredCallIds(String callId) {
+ if (mUnansweredCallIds.remove(callId)) {
+ if (mUnansweredCallIds.isEmpty()) {
+ stopRinging();
+ } else {
+ startRinging();
+ }
+ }
+ }
+
+ private void startRinging() {
+ AudioManager audioManager = (AudioManager) TelecommApp.getInstance().getSystemService(
+ Context.AUDIO_SERVICE);
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+ Log.v(this, "startRinging");
+ mCallAudioManager.setIsRinging(true);
+ mRingtonePlayer.play();
+ } else {
+ Log.v(this, "startRinging, skipping because volume is 0");
+ }
+ }
+
+ private void stopRinging() {
+ Log.v(this, "stopRinging");
+ mRingtonePlayer.stop();
+ // Even though stop is asynchronous it's ok to update the audio manager. Things like audio
+ // focus are voluntary so releasing focus too early is not detrimental.
+ mCallAudioManager.setIsRinging(false);
+ }
+}
diff --git a/src/com/android/telecomm/WiredHeadsetManager.java b/src/com/android/telecomm/WiredHeadsetManager.java
new file mode 100644
index 0000000..329df71
--- /dev/null
+++ b/src/com/android/telecomm/WiredHeadsetManager.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+
+/**
+ * Listens for and caches headset state. Used By the CallAudioManger for maintaining
+ * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
+ * headset to the phone call.
+ */
+class WiredHeadsetManager {
+ /** Receiver for wired headset plugged and unplugged events. */
+ private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+ Log.v(WiredHeadsetManager.this, "ACTION_HEADSET_PLUG event, plugged in: %b",
+ isPluggedIn);
+ onHeadsetPluggedInChanged(isPluggedIn);
+ }
+ }
+ }
+
+ private final CallAudioManager mCallAudioManager;
+ private final WiredHeadsetBroadcastReceiver mReceiver;
+ private boolean mIsPluggedIn;
+
+ WiredHeadsetManager(CallAudioManager callAudioManager) {
+ mCallAudioManager = callAudioManager;
+ mReceiver = new WiredHeadsetBroadcastReceiver();
+
+ Context context = TelecommApp.getInstance();
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mIsPluggedIn = audioManager.isWiredHeadsetOn();
+
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+ context.registerReceiver(mReceiver, intentFilter);
+ }
+
+ boolean isPluggedIn() {
+ return mIsPluggedIn;
+ }
+
+ private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+ if (mIsPluggedIn != isPluggedIn) {
+ Log.v(this, "onHeadsetPluggedInChanged, mIsPluggedIn: %b -> %b", mIsPluggedIn,
+ isPluggedIn);
+ boolean oldIsPluggedIn = mIsPluggedIn;
+ mIsPluggedIn = isPluggedIn;
+ mCallAudioManager.onHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+ }
+ }
+}