Add CallsManagerListener
Also contains some minor bug fixes:
- add an incoming field to Call
- correcty log failed outgoing calls (previously these were mostly dropped)
- log missed incoming calls
Change-Id: I72dc39efd519302c1f765f4f9c9d04c5095e45a6
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 26f7ac1..452f8e1 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -16,21 +16,18 @@
package com.android.telecomm;
-import android.Manifest;
import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
-import android.telecomm.CallService;
+import android.telecomm.CallInfo;
import android.telecomm.CallServiceDescriptor;
import android.telecomm.CallState;
-import android.telecomm.TelecommConstants;
-import android.telephony.TelephonyManager;
import com.android.internal.telecomm.ICallService;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -46,15 +43,19 @@
*/
public final class CallsManager {
+ interface CallsManagerListener {
+ void onCallAdded(Call call);
+ void onCallRemoved(Call call);
+ void onCallStateChanged(Call call, CallState oldState, CallState newState);
+ void onIncomingCallAnswered(Call call);
+ void onIncomingCallRejected(Call call);
+ void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
+ }
+
private static final CallsManager INSTANCE = new CallsManager();
private final Switchboard mSwitchboard;
- /** Used to control the in-call app. */
- private final InCallController mInCallController;
-
- private final Ringer mRinger;
-
/**
* The main call repository. Keeps an instance of all live calls keyed by call ID. New incoming
* and outgoing calls are added to the map and removed when the calls move to the disconnected
@@ -64,24 +65,20 @@
private final Map<String, Call> mCalls = Maps.newHashMap();
/**
- * 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.
- * TODO(santoscordon): May want to add comments about ITelephony.answerCall() method since
- * ordering may also apply to that case.
+ * The call the user is currently interacting with. This is the call that should have audio
+ * focus and be visible in the in-call UI.
*/
- private final List<Call> mUnansweredIncomingCalls = Lists.newLinkedList();
+ private Call mForegroundCall;
- /**
- * May be unnecessary per off-line discussions (between santoscordon and gilad) since the set
- * of CallsManager APIs that need to be exposed to the dialer (or any application firing call
- * intents) may be empty.
- */
- private DialerAdapter mDialerAdapter;
+ private final CallsManagerListener mCallLogManager;
- private InCallAdapter mInCallAdapter;
+ private final CallsManagerListener mPhoneStateBroadcaster;
- private CallLogManager mCallLogManager;
+ private final CallsManagerListener mCallAudioManager;
+
+ private final CallsManagerListener mInCallController;
+
+ private final CallsManagerListener mRinger;
private VoicemailManager mVoicemailManager;
@@ -94,15 +91,25 @@
*/
private CallsManager() {
mSwitchboard = new Switchboard(this);
- mInCallController = new InCallController(this);
- mRinger = new Ringer();
mCallLogManager = new CallLogManager(TelecommApp.getInstance());
+ mPhoneStateBroadcaster = new PhoneStateBroadcaster();
+ mCallAudioManager = new CallAudioManager();
+ mInCallController = new InCallController();
+ mRinger = new Ringer();
}
static CallsManager getInstance() {
return INSTANCE;
}
+ ImmutableCollection<Call> getCalls() {
+ return ImmutableList.copyOf(mCalls.values());
+ }
+
+ Call getForegroundCall() {
+ return mForegroundCall;
+ }
+
/**
* 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
@@ -116,7 +123,7 @@
// Create a call with no handle. Eventually, switchboard will update the call with
// additional information from the call service, but for now we just need one to pass around
// with a unique call ID.
- Call call = new Call();
+ Call call = new Call(true /* isIncoming */);
mSwitchboard.retrieveIncomingCall(call, descriptor, extras);
}
@@ -126,10 +133,11 @@
* list of live calls. Also notifies the in-call app so the user can answer or reject the call.
*
* @param call The new incoming call.
+ * @param callInfo The details of the call.
*/
- void handleSuccessfulIncomingCall(Call call) {
+ void handleSuccessfulIncomingCall(Call call, CallInfo callInfo) {
Log.d(this, "handleSuccessfulIncomingCall");
- Preconditions.checkState(call.getState() == CallState.RINGING);
+ Preconditions.checkState(callInfo.getState() == CallState.RINGING);
Uri handle = call.getHandle();
ContactInfo contactInfo = call.getContactInfo();
@@ -143,13 +151,18 @@
// No objection to accept the incoming call, proceed with potentially connecting it (based
// on the user's action, or lack thereof).
+ call.setHandle(callInfo.getHandle());
+ setCallState(call, callInfo.getState());
addCall(call);
+ }
- mUnansweredIncomingCalls.add(call);
- if (mUnansweredIncomingCalls.size() == 1) {
- // Start the ringer if we are the top-most incoming call (the only one in this case).
- mRinger.startRinging();
- }
+ /**
+ * Called when an incoming call was not connected.
+ *
+ * @param call The incoming call.
+ */
+ void handleUnsuccessfulIncomingCall(Call call) {
+ setCallState(call, CallState.DISCONNECTED);
}
/**
@@ -171,30 +184,36 @@
}
// No objection to issue the call, proceed with trying to put it through.
- Call call = new Call(handle, contactInfo);
+ Call call = new Call(handle, contactInfo, false /* isIncoming */);
+ setCallState(call, CallState.DIALING);
+ addCall(call);
mSwitchboard.placeOutgoingCall(call);
}
/**
- * Adds a new outgoing call to the list of live calls and notifies the in-call app.
+ * Called when a call service acknowledges that it can place a call.
*
* @param call The new outgoing call.
*/
void handleSuccessfulOutgoingCall(Call call) {
- // OutgoingCallProcessor sets the call state to DIALING when it receives confirmation of the
- // placed call from the call service so there is no need to set it here. Instead, check that
- // the state is appropriate.
- Preconditions.checkState(call.getState() == CallState.DIALING);
- addCall(call);
+ Log.v(this, "handleSuccessfulOutgoingCall, %s", call);
}
/**
- * Informs mCallLogManager about the outgoing call that failed, so that it can be logged.
+ * Called when an outgoing call was not placed.
*
- * @param call The failed outgoing call.
+ * @param call The outgoing call.
+ * @param isAborted True if the call was unsuccessful because it was aborted.
*/
- void handleFailedOutgoingCall(Call call) {
- mCallLogManager.logFailedOutgoingCall(call);
+ void handleUnsuccessfulOutgoingCall(Call call, boolean isAborted) {
+ Log.v(this, "handleAbortedOutgoingCall, call: %s, isAborted: %b", call, isAborted);
+ if (isAborted) {
+ call.abort();
+ setCallState(call, CallState.ABORTED);
+ } else {
+ setCallState(call, CallState.DISCONNECTED);
+ }
+ removeCall(call);
}
/**
@@ -209,7 +228,11 @@
if (call == null) {
Log.i(this, "Request to answer a non-existent call %s", callId);
} else {
- stopRinging(call);
+ mCallLogManager.onIncomingCallAnswered(call);
+ 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,
@@ -231,7 +254,12 @@
if (call == null) {
Log.i(this, "Request to reject a non-existent call %s", callId);
} else {
- stopRinging(call);
+ mCallLogManager.onIncomingCallRejected(call);
+ mPhoneStateBroadcaster.onIncomingCallRejected(call);
+ mCallAudioManager.onIncomingCallRejected(call);
+ mInCallController.onIncomingCallRejected(call);
+ mRinger.onIncomingCallRejected(call);
+
call.reject();
}
}
@@ -250,13 +278,6 @@
} else {
call.disconnect();
}
-
- // TODO(sail): Replace with CallAudioManager.
- Log.v(this, "disconnectCall, abandoning audio focus");
- Context context = TelecommApp.getInstance().getApplicationContext();
- AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- audioManager.setMode(AudioManager.MODE_NORMAL);
- audioManager.abandonAudioFocusForCall();
}
/**
@@ -303,25 +324,10 @@
void markCallAsActive(String callId) {
setCallState(callId, CallState.ACTIVE);
- removeFromUnansweredCalls(callId);
- mInCallController.markCallAsActive(callId);
-
- // TODO(sail): Replace with CallAudioManager.
- Log.v(this, "markCallAsActive, requesting audio focus");
- Context context = TelecommApp.getInstance().getApplicationContext();
- AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- audioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
- audioManager.setMode(AudioManager.MODE_IN_CALL);
- audioManager.setMicrophoneMute(false);
- audioManager.setSpeakerphoneOn(false);
}
void markCallAsOnHold(String callId) {
setCallState(callId, CallState.ON_HOLD);
-
- // Notify the in-call UI
- mInCallController.markCallAsOnHold(callId);
}
/**
@@ -332,35 +338,7 @@
*/
void markCallAsDisconnected(String callId) {
setCallState(callId, CallState.DISCONNECTED);
- removeFromUnansweredCalls(callId);
-
- Call call = mCalls.remove(callId);
- // At this point the call service has confirmed that the call is disconnected to it is
- // safe to disassociate the call from its call service.
- call.clearCallService();
-
- // Notify the in-call UI
- mInCallController.markCallAsDisconnected(callId);
- if (mCalls.isEmpty()) {
- mInCallController.unbind();
- }
-
- // Log the call in the call log.
- mCallLogManager.logDisconnectedCall(call);
- }
-
- /**
- * Sends all the live calls to the in-call app if any exist. If there are no live calls, then
- * tells the in-call controller to unbind since it is not needed.
- */
- void updateInCall() {
- if (mCalls.isEmpty()) {
- mInCallController.unbind();
- } else {
- for (Call call : mCalls.values()) {
- addInCallEntry(call);
- }
- }
+ removeCall(mCalls.remove(callId));
}
/**
@@ -370,14 +348,24 @@
*/
private void addCall(Call call) {
mCalls.put(call.getId(), call);
- addInCallEntry(call);
+
+ mCallLogManager.onCallAdded(call);
+ mPhoneStateBroadcaster.onCallAdded(call);
+ mCallAudioManager.onCallAdded(call);
+ mInCallController.onCallAdded(call);
+ mRinger.onCallAdded(call);
+ updateForegroundCall();
}
- /**
- * Notifies the in-call app of the specified (new) call.
- */
- private void addInCallEntry(Call call) {
- mInCallController.addCall(call.toCallInfo());
+ private void removeCall(Call call) {
+ call.clearCallService();
+
+ mCallLogManager.onCallRemoved(call);
+ mPhoneStateBroadcaster.onCallRemoved(call);
+ mCallAudioManager.onCallRemoved(call);
+ mInCallController.onCallRemoved(call);
+ mRinger.onCallRemoved(call);
+ updateForegroundCall();
}
/**
@@ -396,102 +384,67 @@
Log.w(this, "Call %s was not found while attempting to update the state to %s.",
callId, newState );
} else {
- if (newState != call.getState()) {
- // Unfortunately, in the telephony world the radio is king. So if the call notifies
- // us that the call is in a particular state, we allow it even if it doesn't make
- // sense (e.g., ACTIVE -> RINGING).
- // TODO(santoscordon): Consider putting a stop to the above and turning CallState
- // into a well-defined state machine.
- // TODO(santoscordon): Define expected state transitions here, and log when an
- // unexpected transition occurs.
- call.setState(newState);
- // TODO(santoscordon): Notify the in-call app whenever a call changes state.
+ setCallState(call, newState);
+ }
+ }
- broadcastState(call);
+ /**
+ * Sets the specified state on the specified call. Updates the ringer if the call is exiting
+ * the RINGING state.
+ *
+ * @param call The call.
+ * @param newState The new state of the call.
+ */
+ private void setCallState(Call call, CallState newState) {
+ CallState oldState = call.getState();
+ if (newState != oldState) {
+ // Unfortunately, in the telephony world the radio is king. So if the call notifies
+ // us that the call is in a particular state, we allow it even if it doesn't make
+ // sense (e.g., ACTIVE -> RINGING).
+ // TODO(santoscordon): Consider putting a stop to the above and turning CallState
+ // into a well-defined state machine.
+ // TODO(santoscordon): Define expected state transitions here, and log when an
+ // unexpected transition occurs.
+ call.setState(newState);
+
+ // 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);
+ updateForegroundCall();
}
}
}
/**
- * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
- * changes. TODO: Split this out into a separate class and do it properly; this is just a first
- * pass over the functionality.
- *
- * @param call The {@link Call} being updated.
+ * Checks which call should be visible to the user and have audio focus.
*/
- private void broadcastState(Call call) {
- final String callState;
- switch (call.getState()) {
- case DIALING:
- case ACTIVE:
- case ON_HOLD:
- callState = TelephonyManager.EXTRA_STATE_OFFHOOK;
+ private void updateForegroundCall() {
+ Call newForegroundCall = null;
+ for (Call call : mCalls.values()) {
+ // Incoming ringing calls have priority.
+ if (call.getState() == CallState.RINGING) {
+ newForegroundCall = call;
break;
-
- case RINGING:
- callState = TelephonyManager.EXTRA_STATE_RINGING;
- break;
-
- case DISCONNECTED:
- case ABORTED:
- callState = TelephonyManager.EXTRA_STATE_IDLE;
- break;
-
- default:
- Log.w(this, "Call is in an unknown state (%s), not broadcasting: %s", call.getState(),
- call.getId());
- return;
- }
-
- Intent updateIntent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
- updateIntent.putExtra(TelephonyManager.EXTRA_STATE, callState);
- // TODO: See if we can add this (the current API doesn't have a callId).
- updateIntent.putExtra(TelecommConstants.EXTRA_CALL_ID, call.getId());
-
- // Populate both, since the original API was needlessly complicated.
- updateIntent.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, call.getHandle());
- // TODO: See if we can add this (the current API only sets this on NEW_OUTGOING_CALL).
- updateIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, call.getHandle());
-
- // TODO: Replace these with real constants once this API has been vetted.
- CallServiceWrapper callService = call.getCallService();
- if (callService != null) {
- updateIntent.putExtra(CallService.class.getName(), callService.getComponentName());
- }
- TelecommApp.getInstance().sendBroadcast(updateIntent, Manifest.permission.READ_PHONE_STATE);
- Log.i(this, "Broadcasted state change: %s", callState);
- }
-
- /**
- * Removes the specified call from the list of unanswered incoming calls and updates the ringer
- * based on the new state of {@link #mUnansweredIncomingCalls}. 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 removeFromUnansweredCalls(String callId) {
- Call call = mCalls.get(callId);
- if (call != null && mUnansweredIncomingCalls.remove(call)) {
- if (mUnansweredIncomingCalls.isEmpty()) {
- mRinger.stopRinging();
- } else {
- mRinger.startRinging();
+ }
+ if (call.getState() == CallState.ACTIVE) {
+ newForegroundCall = call;
+ // Don't break in case there's a ringing call that has priority.
}
}
- }
- /**
- * Stops playing the ringer if the specified call is the top-most incoming call. This exists
- * separately from {@link #removeFromUnansweredCalls} to allow stopping the ringer for calls
- * that should remain in {@link #mUnansweredIncomingCalls}, invoked from {@link #answerCall}
- * and {@link #rejectCall}.
- *
- * @param call The call for which we should stop ringing.
- */
- private void stopRinging(Call call) {
- // Only stop the ringer if this call is the top-most incoming call.
- if (!mUnansweredIncomingCalls.isEmpty() && mUnansweredIncomingCalls.get(0) == call) {
- mRinger.stopRinging();
+ if (newForegroundCall != mForegroundCall) {
+ 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);
}
}
}