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