Making CallsManager interact with only Calls.

Eventually, I would like CallsManager to interact only with Call
objects.  This is one move in that direction.

OLD:
CallsManager->Switchboard-+->Call-+->CallService
                          +-------+

NEW:
CallsManager->Call-+->Switchboard-+->CallService
                   +--------------+

This change also includes new direct listening to Call events by
CallsManager. At the moment includes only success/failure of incoming
and outgoing calls but should eventually be more generic as we implement
more of the Telecomm design.

Change-Id: I7a764706acd0872960642a7b16c71a2bc514f3b3
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index e21f3de..fe984cd 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -22,6 +22,7 @@
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
 import android.telecomm.GatewayInfo;
+import android.telecomm.TelecommConstants;
 import android.telephony.DisconnectCause;
 import android.telephony.PhoneNumberUtils;
 
@@ -37,6 +38,17 @@
  *  connected etc).
  */
 final class Call {
+
+    /**
+     * Listener for events on the call.
+     */
+    interface Listener {
+        void onSuccessfulOutgoingCall(Call call);
+        void onFailedOutgoingCall(Call call, boolean isAborted);
+        void onSuccessfulIncomingCall(Call call, CallInfo callInfo);
+        void onFailedIncomingCall(Call call);
+    }
+
     /** Additional contact information beyond handle above, optional. */
     private final ContactInfo mContactInfo;
 
@@ -112,6 +124,9 @@
      */
     private CallServiceDescriptor mHandoffCallServiceDescriptor;
 
+    /** Set of listeners on this call. */
+    private Set<Listener> mListeners = Sets.newHashSet();
+
     /**
      * Creates an empty call object.
      *
@@ -137,6 +152,14 @@
         mIsIncoming = isIncoming;
     }
 
+    void addListener(Listener listener) {
+        mListeners.add(listener);
+    }
+
+    void removeListener(Listener listener) {
+        mListeners.remove(listener);
+    }
+
     /** {@inheritDoc} */
     @Override public String toString() {
         String component = null;
@@ -318,6 +341,66 @@
     }
 
     /**
+     * Starts the incoming call flow through the switchboard. When switchboard completes, it will
+     * invoke handle[Un]SuccessfulIncomingCall.
+     *
+     * @param descriptor The relevant call-service descriptor.
+     * @param extras The optional extras passed via
+     *         {@link TelecommConstants#EXTRA_INCOMING_CALL_EXTRAS}.
+     */
+    void startIncoming(CallServiceDescriptor descriptor, Bundle extras) {
+        Switchboard.getInstance().retrieveIncomingCall(this, descriptor, extras);
+    }
+
+    void handleSuccessfulIncoming(CallInfo callInfo) {
+        Preconditions.checkState(callInfo.getState() == CallState.RINGING);
+        setHandle(callInfo.getHandle());
+
+        // TODO(santoscordon): Make this class (not CallsManager) responsible for changing the call
+        // state to RINGING.
+
+        // TODO(santoscordon): Replace this with state transitions related to "connecting".
+        for (Listener l : mListeners) {
+            l.onSuccessfulIncomingCall(this, callInfo);
+        }
+    }
+
+    void handleFailedIncoming() {
+        clearCallService();
+
+        // TODO: Needs more specific disconnect error for this case.
+        setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED, null);
+        setState(CallState.DISCONNECTED);
+
+        // TODO(santoscordon): Replace this with state transitions related to "connecting".
+        for (Listener l : mListeners) {
+            l.onFailedIncomingCall(this);
+        }
+    }
+
+    /**
+     * Starts the outgoing call flow through the switchboard. When switchboard completes, it will
+     * invoke handleSuccessful/FailedOutgoingCall.
+     */
+    void startOutgoing() {
+        Switchboard.getInstance().placeOutgoingCall(this);
+    }
+
+    void handleSuccessfulOutgoing() {
+        // TODO(santoscordon): Replace this with state transitions related to "connecting".
+        for (Listener l : mListeners) {
+            l.onSuccessfulOutgoingCall(this);
+        }
+    }
+
+    void handleFailedOutgoing(boolean isAborted) {
+        // TODO(santoscordon): Replace this with state transitions related to "connecting".
+        for (Listener l : mListeners) {
+            l.onFailedOutgoingCall(this, isAborted);
+        }
+    }
+
+    /**
      * Adds the specified call service to the list of incompatible services.  The set is used when
      * attempting to switch a phone call between call services such that incompatible services can
      * be avoided.
@@ -344,17 +427,17 @@
     }
 
     /**
-     * Aborts ongoing attempts to connect this call. Only applicable to {@link CallState#NEW}
-     * outgoing calls.  See {@link #disconnect} for already-connected calls.
+     * Issues an abort signal to the associated call service and clears the current call service
+     * and call-service selector.
      */
-    void abort() {
-        if (mState == CallState.NEW) {
-            if (mCallService != null) {
-                mCallService.abort(this);
-            }
-            clearCallService();
-            clearCallServiceSelector();
+    void finalizeAbort() {
+        Preconditions.checkState(mState == CallState.NEW);
+
+        if (mCallService != null) {
+            mCallService.abort(this);
         }
+        clearCallService();
+        clearCallServiceSelector();
     }
 
     /**
@@ -385,11 +468,20 @@
      * Attempts to disconnect the call through the call service.
      */
     void disconnect() {
-        if (mCallService == null) {
-            Log.d(this, "disconnect() request on a call without a call service.");
-            // In case this call is ringing, ensure we abort and clean up right away.
-            CallsManager.getInstance().abortCall(this);
+        if (mState == CallState.NEW) {
+            // There is a very strange indirection here. When we are told to disconnect, we issue
+            // an 'abort' to the switchboard if we are still in the NEW (or "connecting") state.
+            // The switchboard will then cancel the outgoing call process and ask this class to
+            // finalize the abort procedure via {@link #finalizeAbort}. The issue is that
+            // Switchboard edits the state of the call as part of the process and then this class
+            // is responsible for undoing it, and that is all kinds of backwards.
+            // TODO(santoscordon): Remove Switchboard's requirement to edit the state of the Call
+            // objects and remove any multi-class shared state of incoming and outgoing call
+            // processing.
+            Switchboard.getInstance().abortCall(this);
         } else {
+            Preconditions.checkNotNull(mCallService);
+
             Log.i(this, "Send disconnect to call service for call: %s", this);
             // The call isn't officially disconnected until the call service confirms that the call
             // was actually disconnected. Only then is the association between call and call service
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index e019349..bef1dfa 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -23,19 +23,13 @@
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
 import android.telecomm.GatewayInfo;
-import android.telecomm.InCallCall;
 import android.telephony.DisconnectCause;
 
 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;
 import com.google.common.collect.Sets;
 
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 /**
@@ -45,9 +39,9 @@
  * access from other packages specifically refraining from passing the CallsManager instance
  * beyond the com.android.telecomm package boundary.
  */
-public final class CallsManager {
+public final class CallsManager implements Call.Listener {
 
-    // TODO(santoscordon): Consider renaming this CallsManagerPlugin.
+ // TODO(santoscordon): Consider renaming this CallsManagerPlugin.
     interface CallsManagerListener {
         void onCallAdded(Call call);
         void onCallRemoved(Call call);
@@ -69,8 +63,6 @@
 
     private static final CallsManager INSTANCE = new CallsManager();
 
-    private final Switchboard mSwitchboard;
-
     /**
      * The main call repository. Keeps an instance of all live calls. New incoming and outgoing
      * calls are added to the map and removed when the calls move to the disconnected state.
@@ -95,9 +87,10 @@
 
     private final Set<CallsManagerListener> mListeners = Sets.newHashSet();
 
-    private final List<OutgoingCallValidator> mOutgoingCallValidators = Lists.newArrayList();
-
-    private final List<IncomingCallValidator> mIncomingCallValidators = Lists.newArrayList();
+    /** Singleton accessor. */
+    static CallsManager getInstance() {
+        return INSTANCE;
+    }
 
     /**
      * Initializes the required Telecomm components.
@@ -105,7 +98,6 @@
     private CallsManager() {
         TelecommApp app = TelecommApp.getInstance();
 
-        mSwitchboard = new Switchboard(this);
         mCallAudioManager = new CallAudioManager();
 
         InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager);
@@ -120,8 +112,42 @@
         mListeners.add(mDtmfLocalTonePlayer);
     }
 
-    static CallsManager getInstance() {
-        return INSTANCE;
+    @Override
+    public void onSuccessfulOutgoingCall(Call call) {
+        Log.v(this, "onSuccessfulOutgoingCall, %s", call);
+        if (mCalls.contains(call)) {
+            // The call's CallService has been updated.
+            for (CallsManagerListener listener : mListeners) {
+                listener.onCallServiceChanged(call, null, call.getCallService());
+            }
+        } else if (mPendingHandoffCalls.contains(call)) {
+            updateHandoffCallServiceDescriptor(call.getOriginalCall(),
+                    call.getCallService().getDescriptor());
+        }
+    }
+
+    @Override
+    public void onFailedOutgoingCall(Call call, boolean isAborted) {
+        Log.v(this, "onFailedOutgoingCall, call: %s, isAborted: %b", call, isAborted);
+        if (isAborted) {
+            setCallState(call, CallState.ABORTED);
+            removeCall(call);
+        } else {
+            // TODO: Replace disconnect cause with more specific disconnect causes.
+            markCallAsDisconnected(call, DisconnectCause.ERROR_UNSPECIFIED, null);
+        }
+    }
+
+    @Override
+    public void onSuccessfulIncomingCall(Call call, CallInfo callInfo) {
+        Log.d(this, "onSuccessfulIncomingCall");
+        setCallState(call, callInfo.getState());
+        addCall(call);
+    }
+
+    @Override
+    public void onFailedIncomingCall(Call call) {
+        call.removeListener(this);
     }
 
     ImmutableCollection<Call> getCalls() {
@@ -148,7 +174,7 @@
     /**
      * 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
-     * to {@link #handleSuccessfulIncomingCall} to start the in-call UI.
+     * to {@link #onSuccessfulIncomingCall} to start the in-call UI.
      *
      * @param descriptor The descriptor of the call service to use for this incoming call.
      * @param extras The optional extras Bundle passed with the intent used for the incoming call.
@@ -159,50 +185,10 @@
         // additional information from the call service, but for now we just need one to pass
         // around.
         Call call = new Call(true /* isIncoming */);
+        // TODO(santoscordon): Move this to be a part of addCall()
+        call.addListener(this);
 
-        mSwitchboard.retrieveIncomingCall(call, descriptor, extras);
-    }
-
-    /**
-     * Validates the specified call and, upon no objection to connect it, adds the new call to the
-     * 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, CallInfo callInfo) {
-        Log.d(this, "handleSuccessfulIncomingCall");
-        Preconditions.checkState(callInfo.getState() == CallState.RINGING);
-
-        Uri handle = call.getHandle();
-        ContactInfo contactInfo = call.getContactInfo();
-        for (IncomingCallValidator validator : mIncomingCallValidators) {
-            if (!validator.isValid(handle, contactInfo)) {
-                // TODO(gilad): Consider displaying an error message.
-                Log.i(this, "Dropping restricted incoming call");
-                return;
-            }
-        }
-
-        // 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);
-    }
-
-    /**
-     * Called when an incoming call was not connected.
-     *
-     * @param call The incoming call.
-     */
-    void handleUnsuccessfulIncomingCall(Call call) {
-        // Incoming calls are not added unless they are successful. We set the state and disconnect
-        // cause just as a matter of good bookkeeping. We do not use the specific methods for
-        // setting those values so that we do not trigger CallsManagerListener events.
-        // TODO: Needs more specific disconnect error for this case.
-        call.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED, null);
-        call.setState(CallState.DISCONNECTED);
+        call.startIncoming(descriptor, extras);
     }
 
     /**
@@ -214,14 +200,6 @@
      *         actual dialed handle via a gateway provider. May be null.
      */
     void placeOutgoingCall(Uri handle, ContactInfo contactInfo, GatewayInfo gatewayInfo) {
-        for (OutgoingCallValidator validator : mOutgoingCallValidators) {
-            if (!validator.isValid(handle, contactInfo)) {
-                // TODO(gilad): Display an error message.
-                Log.i(this, "Dropping restricted outgoing call.");
-                return;
-            }
-        }
-
         final Uri uriHandle = (gatewayInfo == null) ? handle : gatewayInfo.getGatewayHandle();
 
         if (gatewayInfo == null) {
@@ -230,45 +208,14 @@
             Log.i(this, "Creating a new outgoing call with gateway handle: %s, original handle: %s",
                     Log.pii(uriHandle), Log.pii(handle));
         }
+
         Call call = new Call(uriHandle, contactInfo, gatewayInfo, false /* isIncoming */);
+
+        // TODO(santoscordon): Move this to be a part of addCall()
+        call.addListener(this);
         addCall(call);
-        mSwitchboard.placeOutgoingCall(call);
-    }
 
-    /**
-     * Called when a call service acknowledges that it can place a call.
-     *
-     * @param call The new outgoing call.
-     */
-    void handleSuccessfulOutgoingCall(Call call) {
-        Log.v(this, "handleSuccessfulOutgoingCall, %s", call);
-        if (mCalls.contains(call)) {
-            // The call's CallService has been updated.
-            for (CallsManagerListener listener : mListeners) {
-                listener.onCallServiceChanged(call, null, call.getCallService());
-            }
-        } else if (mPendingHandoffCalls.contains(call)) {
-            updateHandoffCallServiceDescriptor(call.getOriginalCall(),
-                    call.getCallService().getDescriptor());
-        }
-    }
-
-    /**
-     * Called when an outgoing call was not placed.
-     *
-     * @param call The outgoing call.
-     * @param isAborted True if the call was unsuccessful because it was aborted.
-     */
-    void handleUnsuccessfulOutgoingCall(Call call, boolean isAborted) {
-        Log.v(this, "handleAbortedOutgoingCall, call: %s, isAborted: %b", call, isAborted);
-        if (isAborted) {
-            call.abort();
-            setCallState(call, CallState.ABORTED);
-            removeCall(call);
-        } else {
-            // TODO: Replace disconnect cause with more specific disconnect causes.
-            markCallAsDisconnected(call, DisconnectCause.ERROR_UNSPECIFIED, null);
-        }
+        call.startOutgoing();
     }
 
     /**
@@ -399,18 +346,6 @@
         }
     }
 
-    /**
-     * Instructs Telecomm to abort any outgoing state of the specified call.
-     */
-    void abortCall(Call call) {
-        if (!mCalls.contains(call)) {
-            Log.w(this, "Unknown call (%s) asked to be aborted", call);
-        } else {
-            Log.d(this, "Aborting call: (%s)", call);
-            mSwitchboard.abortCall(call);
-        }
-    }
-
     /** Called by the in-call UI to change the mute state. */
     void mute(boolean shouldMute) {
         mCallAudioManager.mute(shouldMute);
@@ -448,7 +383,7 @@
         tempCall.setCallServiceSelector(originalCall.getCallServiceSelector());
         mPendingHandoffCalls.add(tempCall);
         Log.d(this, "Placing handoff call");
-        mSwitchboard.placeOutgoingCall(tempCall);
+        tempCall.startOutgoing();
     }
 
     /** Called when the audio state changes. */
@@ -557,6 +492,7 @@
         Preconditions.checkState(call.getHandoffCallServiceDescriptor() == null);
         Log.v(this, "removeCall(%s)", call);
 
+        call.removeListener(this);
         call.clearCallService();
         call.clearCallServiceSelector();
 
diff --git a/src/com/android/telecomm/IncomingCallValidator.java b/src/com/android/telecomm/IncomingCallValidator.java
deleted file mode 100644
index 769eae7..0000000
--- a/src/com/android/telecomm/IncomingCallValidator.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.net.Uri;
-
-/**
- * Implementations can be used to reject incoming calls based on arbitrary restrictions across call
- * services (e.g. suppressing calls from certain phone numbers regardless if they are received over
- * PSTN or WiFi). See OutgoingCallValidator regarding outgoing calls.
- */
-public interface IncomingCallValidator {
-
-    boolean isValid(Uri handle, ContactInfo contactInfo);
-}
diff --git a/src/com/android/telecomm/OutgoingCallProcessor.java b/src/com/android/telecomm/OutgoingCallProcessor.java
index 315a9bd..5c8d5f5 100644
--- a/src/com/android/telecomm/OutgoingCallProcessor.java
+++ b/src/com/android/telecomm/OutgoingCallProcessor.java
@@ -16,15 +16,11 @@
 
 package com.android.telecomm;
 
-import android.os.Handler;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.telecomm.CallState;
 import android.telecomm.CallServiceDescriptor;
 
 import com.google.android.collect.Sets;
-import com.google.common.collect.Maps;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 
 import java.util.Collection;
 import java.util.Iterator;
diff --git a/src/com/android/telecomm/OutgoingCallValidator.java b/src/com/android/telecomm/OutgoingCallValidator.java
deleted file mode 100644
index 288d37e..0000000
--- a/src/com/android/telecomm/OutgoingCallValidator.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.net.Uri;
-
-/**
- * Implementations can be used to suppress certain classes of outgoing calls based on arbitrary
- * restrictions across call services (e.g. black-listing some phone numbers regardless if these
- * are attempted over PSTN or WiFi). That being the case, FDN which is specific to GSM may need
- * to be implemented separately since classes implementing this interface are generally invoked
- * before any given call service is selected.
- * See http://en.wikipedia.org/wiki/Fixed_Dialing_Number and/or IncomingCallValidator regarding
- * incoming calls.
- */
-public interface OutgoingCallValidator {
-
-    boolean isValid(Uri handle, ContactInfo contactInfo);
-}
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index 82f94fa..81ab172 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -19,8 +19,6 @@
 import android.content.ComponentName;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.Looper;
-
 import android.os.Message;
 import android.telecomm.CallInfo;
 import android.telecomm.CallServiceDescriptor;
@@ -32,7 +30,6 @@
 import com.google.common.collect.Sets;
 
 import java.util.Collection;
-import java.util.Iterator;
 import java.util.Set;
 
 /**
@@ -45,7 +42,7 @@
 final class Switchboard {
     private final static int MSG_EXPIRE_STALE_CALL = 1;
 
-    private final CallsManager mCallsManager;
+    private final static Switchboard sInstance = new Switchboard();
 
     /** Used to place outgoing calls. */
     private final OutgoingCallsManager mOutgoingCallsManager;
@@ -95,13 +92,17 @@
      */
     private int mLookupId = 0;
 
+    /** Singleton accessor. */
+    static Switchboard getInstance() {
+        return sInstance;
+    }
+
     /**
      * Persists the specified parameters and initializes Switchboard.
      */
-    Switchboard(CallsManager callsManager) {
+    private Switchboard() {
         ThreadUtil.checkOnMainThread();
 
-        mCallsManager = callsManager;
         mOutgoingCallsManager = new OutgoingCallsManager(this);
         mIncomingCallsManager = new IncomingCallsManager(this);
         mSelectorRepository = new CallServiceSelectorRepository(this, mOutgoingCallsManager);
@@ -194,8 +195,8 @@
     void handleSuccessfulOutgoingCall(Call call) {
         Log.d(this, "handleSuccessfulOutgoingCall");
 
-        mCallsManager.handleSuccessfulOutgoingCall(call);
         finalizeOutgoingCall(call);
+        call.handleSuccessfulOutgoing();
     }
 
     /**
@@ -205,8 +206,8 @@
     void handleFailedOutgoingCall(Call call, boolean isAborted) {
         Log.d(this, "handleFailedOutgoingCall");
 
-        mCallsManager.handleUnsuccessfulOutgoingCall(call, isAborted);
         finalizeOutgoingCall(call);
+        call.handleFailedOutgoing(isAborted);
     }
 
     /**
@@ -216,8 +217,7 @@
      */
     void handleSuccessfulIncomingCall(Call call, CallInfo callInfo) {
         Log.d(this, "handleSuccessfulIncomingCall");
-
-        mCallsManager.handleSuccessfulIncomingCall(call, callInfo);
+        call.handleSuccessfulIncoming(callInfo);
     }
 
     /**
@@ -231,8 +231,7 @@
 
         // Since we set the call service before calling into incoming-calls manager, we clear it for
         // good measure if an error is reported.
-        call.clearCallService();
-        mCallsManager.handleUnsuccessfulIncomingCall(call);
+        call.handleFailedIncoming();
 
         // At the moment there is nothing more to do if an incoming call is not retrieved. We may at
         // a future date bind to the in-call app optimistically during the incoming-call sequence
@@ -301,7 +300,7 @@
                     new CallServiceSelectorWrapper(
                             componentName.flattenToShortString(),
                             componentName,
-                            mCallsManager,
+                            CallsManager.getInstance(),
                             mOutgoingCallsManager);
 
             selectorsBuilder.add(emergencySelector);