Work in preparation for wiring up the remaining de-allocation and
abort bits.
1) limit to one attempt per call service upon multiple selectors
returning the same call service
2) record the failing/incompatible ones such that these are avoided
when switching
3) addressing some related todos etc.

Change-Id: I62204e9947bb8557888df33ca70f4352d3e6decf
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index d54072d..6660dc3 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -20,10 +20,12 @@
 import android.telecomm.CallState;
 
 import com.android.internal.telecomm.ICallServiceSelector;
+import com.google.android.collect.Sets;
 import com.google.common.base.Preconditions;
 
 import java.util.Date;
 import java.util.Locale;
+import java.util.Set;
 import java.util.UUID;
 
 /**
@@ -67,6 +69,12 @@
     private CallInfo mCallInfo;
 
     /**
+     * The set of call services that were attempted in the process of placing/switching this call
+     * but turned out unsuitable.  Only used in the context of call switching.
+     */
+    private Set<CallServiceWrapper> mIncompatibleCallServices;
+
+    /**
      * Creates an empty call object with a unique call ID.
      */
     Call() {
@@ -177,6 +185,32 @@
     }
 
     /**
+     * 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.
+     *
+     * @param callService The incompatible call service.
+     */
+    void addIncompatibleCallService(CallServiceWrapper callService) {
+        if (mIncompatibleCallServices == null) {
+            mIncompatibleCallServices = Sets.newHashSet();
+        }
+        mIncompatibleCallServices.add(callService);
+    }
+
+    /**
+     * Checks whether or not the specified callService was identified as incompatible in the
+     * context of this call.
+     *
+     * @param callService The call service to evaluate.
+     * @return True upon incompatible call services and false otherwise.
+     */
+    boolean isIncompatibleCallService(CallServiceWrapper callService) {
+        return mIncompatibleCallServices != null &&
+                mIncompatibleCallServices.contains(callService);
+    }
+
+    /**
      * Aborts ongoing attempts to connect this call. Only applicable to {@link CallState#NEW}
      * outgoing calls.  See {@link #disconnect} for already-connected calls.
      */
diff --git a/src/com/android/telecomm/CallServiceAdapter.java b/src/com/android/telecomm/CallServiceAdapter.java
index 3e63f39..1e830d5 100644
--- a/src/com/android/telecomm/CallServiceAdapter.java
+++ b/src/com/android/telecomm/CallServiceAdapter.java
@@ -74,8 +74,17 @@
     }
 
     /** {@inheritDoc} */
-    @Override public void setIsCompatibleWith(String callId, boolean isCompatible) {
-        // TODO(santoscordon): fill in.
+    @Override public void setIsCompatibleWith(final String callId, final boolean isCompatible) {
+        checkValidCallId(callId);
+        mHandler.post(new Runnable() {
+            @Override public void run() {
+                if (mPendingOutgoingCallIds.contains(callId)) {
+                    mOutgoingCallsManager.setIsCompatibleWith(callId, isCompatible);
+                } else {
+                    Log.wtf(CallServiceAdapter.this, "Unknown outgoing call: %s", callId);
+                }
+            }
+        });
     }
 
     /** {@inheritDoc} */
@@ -86,8 +95,7 @@
                 if (mPendingIncomingCallIds.remove(callInfo.getId())) {
                     mIncomingCallsManager.handleSuccessfulIncomingCall(callInfo);
                 } else {
-                    Log.wtf(CallServiceAdapter.this,
-                            "Unknown incoming call: %s", callInfo);
+                    Log.wtf(CallServiceAdapter.this, "Unknown incoming call: %s", callInfo);
                 }
             }
         });
@@ -102,8 +110,7 @@
                     mOutgoingCallsManager.handleSuccessfulCallAttempt(callId);
                 } else {
                     // TODO(gilad): Figure out how to wire up the callService.abort() call.
-                    Log.wtf(CallServiceAdapter.this,
-                            "Unknown outgoing call: %s", callId);
+                    Log.wtf(CallServiceAdapter.this, "Unknown outgoing call: %s", callId);
                 }
             }
         });
@@ -117,8 +124,7 @@
                 if (mPendingOutgoingCallIds.remove(callId)) {
                     mOutgoingCallsManager.handleFailedCallAttempt(callId, reason);
                 } else {
-                    Log.wtf(CallServiceAdapter.this,
-                            "Unknown outgoing call: %s", callId);
+                    Log.wtf(CallServiceAdapter.this, "Unknown outgoing call: %s", callId);
                 }
             }
         });
diff --git a/src/com/android/telecomm/CallServiceProviderWrapper.java b/src/com/android/telecomm/CallServiceProviderWrapper.java
index 97303cd..1a3b3e1 100644
--- a/src/com/android/telecomm/CallServiceProviderWrapper.java
+++ b/src/com/android/telecomm/CallServiceProviderWrapper.java
@@ -31,35 +31,56 @@
  * {@link ICallServiceProvider}.
  * TODO(santoscordon): Keep track of when the service can be safely unbound.
  */
-public class CallServiceProviderWrapper extends ServiceBinder<ICallServiceProvider> {
+final class CallServiceProviderWrapper extends ServiceBinder<ICallServiceProvider> {
+    /**
+     * The service action used to bind to ICallServiceProvider implementations.
+     * TODO(santoscordon): Move this to TelecommConstants.
+     */
+    static final String CALL_SERVICE_PROVIDER_ACTION = ICallServiceProvider.class.getName();
+
     /** The actual service implementation. */
     private ICallServiceProvider mServiceInterface;
 
+    private Binder mBinder = new Binder();
+
     /**
      * Creates a call-service provider for the specified component.
      *
      * @param componentName The component name of the service to bind to.
-     * @param repository The call-service repository.
      */
-    public CallServiceProviderWrapper(
-            ComponentName componentName, CallServiceRepository repository) {
-
+    CallServiceProviderWrapper(ComponentName componentName) {
         super(TelecommConstants.ACTION_CALL_SERVICE_PROVIDER, componentName);
     }
 
     /**
-     * See {@link ICallServiceProvider#lookupCallServices}.
+     * initiates a call-service lookup cycle, see {@link ICallServiceProvider#lookupCallServices}.
+     * Upon failure, the specified error callback is invoked.  Can be invoked even when the call
+     * service is unbound.
+     *
+     * @param response The response object via which to return the relevant call-service
+     *     implementations, if any.
+     * @param errorCallback The callback to invoke upon failure.
      */
-    public void lookupCallServices(ICallServiceLookupResponse response) {
-        try {
-            if (mServiceInterface == null) {
-                Log.wtf(this, "lookupCallServices() invoked while the service is unbound.");
-            } else {
-                mServiceInterface.lookupCallServices(response);
+    void lookupCallServices(
+            final ICallServiceLookupResponse response,
+            final Runnable errorCallback) {
+
+        BindCallback callback = new BindCallback() {
+            @Override public void onSuccess() {
+                if (isServiceValid("lookupCallServices")) {
+                    try {
+                        mServiceInterface.lookupCallServices(response);
+                    } catch (RemoteException e) {
+                        Log.e(CallServiceProviderWrapper.this, e, "Failed to lookupCallServices.");
+                    }
+                }
             }
-        } catch (RemoteException e) {
-            Log.e(this, e, "Failed to lookupCallServices.");
-        }
+            @Override public void onFailure() {
+                errorCallback.run();
+            }
+        };
+
+        mBinder.bind(callback);
     }
 
     /** {@inheritDoc} */
diff --git a/src/com/android/telecomm/CallServiceRepository.java b/src/com/android/telecomm/CallServiceRepository.java
index 6a341cd..2abc8fe 100644
--- a/src/com/android/telecomm/CallServiceRepository.java
+++ b/src/com/android/telecomm/CallServiceRepository.java
@@ -28,7 +28,6 @@
 
 import com.android.internal.telecomm.ICallServiceLookupResponse;
 import com.android.internal.telecomm.ICallServiceProvider;
-import com.android.telecomm.ServiceBinder.BindCallback;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -143,7 +142,7 @@
         mOutstandingProviders.clear();
         for (ComponentName name : providerNames) {
             mOutstandingProviders.add(name);
-            bindProvider(name);
+            lookupCallServices(name);
         }
 
         Log.i(this, "Found %d implementations of ICallServiceProvider.",
@@ -213,41 +212,16 @@
     }
 
     /**
-     * Attempts to bind to the specified provider.
+     * Attempts to obtain call-service descriptors from the specified provider (asynchronously) and
+     * passes the list through to {@link #processCallServices}, which then relinquishes control back
+     * to the switchboard.
      *
      * @param providerName The component name of the relevant provider.
      */
-    private void bindProvider(final ComponentName providerName) {
-        final CallServiceProviderWrapper provider =
-                new CallServiceProviderWrapper(providerName, this);
+    private void lookupCallServices(final ComponentName providerName) {
+        final CallServiceProviderWrapper provider = new CallServiceProviderWrapper(providerName);
 
-        BindCallback callback = new BindCallback() {
-            @Override public void onSuccess() {
-                processProvider(provider);
-            }
-            @Override public void onFailure() {
-                removeOutstandingProvider(providerName);
-            }
-        };
-
-        provider.bind(callback);
-    }
-
-    /**
-     * Queries the supplied provider asynchronously for its CallServices and passes the list through
-     * to {@link #processCallServices} which will relinquish control back to switchboard.
-     *
-     * @param provider The provider object to process.
-     */
-    private void processProvider(final CallServiceProviderWrapper provider) {
-        if (!mIsLookupInProgress) {
-            return;
-        }
-
-        Preconditions.checkNotNull(provider);
-
-        // Query the provider for {@link ICallService} implementations.
-        provider.lookupCallServices(new ICallServiceLookupResponse.Stub() {
+        ICallServiceLookupResponse response = new ICallServiceLookupResponse.Stub() {
             @Override
             public void setCallServiceDescriptors(
                     final List<CallServiceDescriptor> callServiceDescriptors) {
@@ -255,11 +229,21 @@
                 // TODO(santoscordon): Do we need Binder.clear/restoreCallingIdentity()?
                 mHandler.post(new Runnable() {
                     @Override public void run() {
-                        processCallServices(provider, Sets.newHashSet(callServiceDescriptors));
+                        if (mIsLookupInProgress) {
+                            processCallServices(provider, Sets.newHashSet(callServiceDescriptors));
+                        }
                     }
                 });
             }
-        });
+        };
+
+        Runnable errorCallback = new Runnable() {
+            @Override public void run() {
+                removeOutstandingProvider(providerName);
+            }
+        };
+
+        provider.lookupCallServices(response, errorCallback);
     }
 
     /**
@@ -291,8 +275,8 @@
 
             removeOutstandingProvider(providerName);
         } else {
-            Log.i(this, "Unexpected list of call services in lookup %s from %s ", mLookupId,
-                    providerName);
+            Log.i(this,
+                    "Unexpected call services from %s in lookup %s", providerName, mLookupId);
         }
     }
 
diff --git a/src/com/android/telecomm/CallServiceWrapper.java b/src/com/android/telecomm/CallServiceWrapper.java
index 5c9905e..c4d033c 100644
--- a/src/com/android/telecomm/CallServiceWrapper.java
+++ b/src/com/android/telecomm/CallServiceWrapper.java
@@ -35,7 +35,7 @@
  * TODO(santoscordon): Keep track of when the service can be safely unbound.
  * TODO(santoscordon): Look into combining with android.telecomm.CallService.
  */
-public class CallServiceWrapper extends ServiceBinder<ICallService> {
+final class CallServiceWrapper extends ServiceBinder<ICallService> {
 
     /** The descriptor of this call service as supplied by the call-service provider. */
     private final CallServiceDescriptor mDescriptor;
@@ -48,6 +48,8 @@
     /** The actual service implementation. */
     private ICallService mServiceInterface;
 
+    private Binder mBinder = new Binder();
+
     /**
      * Creates a call-service provider for the specified component.
      *
@@ -55,18 +57,18 @@
      *         {@link ICallServiceProvider#lookupCallServices}.
      * @param adapter The call-service adapter.
      */
-    public CallServiceWrapper(CallServiceDescriptor descriptor, CallServiceAdapter adapter) {
+    CallServiceWrapper(CallServiceDescriptor descriptor, CallServiceAdapter adapter) {
         super(TelecommConstants.ACTION_CALL_SERVICE, descriptor.getServiceComponent());
         mDescriptor = descriptor;
         mAdapter = adapter;
     }
 
-    public CallServiceDescriptor getDescriptor() {
+    CallServiceDescriptor getDescriptor() {
         return mDescriptor;
     }
 
     /** See {@link ICallService#setCallServiceAdapter}. */
-    public void setCallServiceAdapter(ICallServiceAdapter callServiceAdapter) {
+    void setCallServiceAdapter(ICallServiceAdapter callServiceAdapter) {
         if (isServiceValid("setCallServiceAdapter")) {
             try {
                 mServiceInterface.setCallServiceAdapter(callServiceAdapter);
@@ -76,32 +78,63 @@
         }
     }
 
-    /** See {@link ICallService#isCompatibleWith}. */
-    public void isCompatibleWith(CallInfo callInfo) {
-        if (isServiceValid("isCompatibleWith")) {
-            try {
-                mServiceInterface.isCompatibleWith(callInfo);
-            } catch (RemoteException e) {
-                Log.e(this, e, "Failed checking isCompatibleWith.");
+    /**
+     * Checks whether or not the specified call is compatible with this call-service implementation,
+     * see {@link ICallService#isCompatibleWith}.  Upon failure, the specified error callback is
+     * invoked. Can be invoked even when the call service is unbound.
+     *
+     * @param callInfo The details of the call.
+     * @param errorCallback The callback to invoke upon failure.
+     */
+    void isCompatibleWith(final CallInfo callInfo, final Runnable errorCallback) {
+        BindCallback callback = new BindCallback() {
+            @Override public void onSuccess() {
+                if (isServiceValid("isCompatibleWith")) {
+                    try {
+                        mServiceInterface.isCompatibleWith(callInfo);
+                    } catch (RemoteException e) {
+                        Log.e(CallServiceWrapper.this, e, "Failed checking isCompatibleWith.");
+                    }
+                }
             }
-        }
+            @Override public void onFailure() {
+                errorCallback.run();
+            }
+        };
+
+        mBinder.bind(callback);
     }
 
-    /** See {@link ICallService#call}. */
-    public void call(CallInfo callInfo) {
-        String callId = callInfo.getId();
-        if (isServiceValid("call")) {
-            try {
-                mServiceInterface.call(callInfo);
-                mAdapter.addPendingOutgoingCallId(callId);
-            } catch (RemoteException e) {
-                Log.e(this, e, "Failed to place call " + callId + ".");
+    /**
+     * Attempts to place the specified call, see {@link ICallService#call}.  Upon failure, the
+     * specified error callback is invoked. Can be invoked even when the call service is unbound.
+     *
+     * @param callInfo The details of the call.
+     * @param errorCallback The callback to invoke upon failure.
+     */
+    void call(final CallInfo callInfo, final Runnable errorCallback) {
+        BindCallback callback = new BindCallback() {
+            @Override public void onSuccess() {
+                String callId = callInfo.getId();
+                if (isServiceValid("call")) {
+                    try {
+                        mServiceInterface.call(callInfo);
+                        mAdapter.addPendingOutgoingCallId(callId);
+                    } catch (RemoteException e) {
+                        Log.e(CallServiceWrapper.this, e, "Failed to place call %s", callId);
+                    }
+                }
             }
-        }
+            @Override public void onFailure() {
+                errorCallback.run();
+            }
+        };
+
+         mBinder.bind(callback);
     }
 
     /** See {@link ICallService#abort}. */
-    public void abort(String callId) {
+    void abort(String callId) {
         mAdapter.removePendingOutgoingCallId(callId);
         if (isServiceValid("abort")) {
             try {
@@ -112,21 +145,44 @@
         }
     }
 
-    /** See {@link ICallService#setIncomingCallId}. */
-    public void setIncomingCallId(String callId, Bundle extras) {
-        if (isServiceValid("setIncomingCallId")) {
-            mAdapter.addPendingIncomingCallId(callId);
-            try {
-                mServiceInterface.setIncomingCallId(callId, extras);
-            } catch (RemoteException e) {
-                Log.e(this, e, "Failed to setIncomingCallId for call %s", callId);
-                mAdapter.removePendingIncomingCallId(callId);
+    /**
+     * 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
+     * is invoked. Can be invoked even when the call service is unbound.
+     * See {@link ICallService#setIncomingCallId}.
+     *
+     * @param callId The call ID used for the incoming call.
+     * @param extras The {@link CallService}-provided extras which need to be sent back.
+     * @param errorCallback The callback to invoke upon failure.
+     */
+    void setIncomingCallId(
+            final String callId,
+            final Bundle extras,
+            final Runnable errorCallback) {
+
+        BindCallback callback = new BindCallback() {
+            @Override public void onSuccess() {
+                if (isServiceValid("setIncomingCallId")) {
+                    mAdapter.addPendingIncomingCallId(callId);
+                    try {
+                        mServiceInterface.setIncomingCallId(callId, extras);
+                    } catch (RemoteException e) {
+                        Log.e(CallServiceWrapper.this, e,
+                                "Failed to setIncomingCallId for call %s", callId);
+                        mAdapter.removePendingIncomingCallId(callId);
+                    }
+                }
             }
-        }
+            @Override public void onFailure() {
+                errorCallback.run();
+            }
+        };
+
+        mBinder.bind(callback);
     }
 
     /** See {@link ICallService#disconnect}. */
-    public void disconnect(String callId) {
+    void disconnect(String callId) {
         if (isServiceValid("disconnect")) {
             try {
                 mServiceInterface.disconnect(callId);
@@ -137,7 +193,7 @@
     }
 
     /** See {@link ICallService#answer}. */
-    public void answer(String callId) {
+    void answer(String callId) {
         if (isServiceValid("answer")) {
             try {
                 mServiceInterface.answer(callId);
@@ -148,7 +204,7 @@
     }
 
     /** See {@link ICallService#reject}. */
-    public void reject(String callId) {
+    void reject(String callId) {
         if (isServiceValid("reject")) {
             try {
                 mServiceInterface.reject(callId);
@@ -159,30 +215,6 @@
     }
 
     /**
-     * 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
-     * is invoked. Can be invoked even when the call service is unbound.
-     *
-     * @param callId The call ID used for the incoming call.
-     * @param extras The {@link CallService}-provided extras which need to be sent back.
-     * @param errorCallback The callback invoked upon failure.
-     */
-    void retrieveIncomingCall(final String callId, final Bundle extras,
-            final Runnable errorCallback) {
-
-        BindCallback callback = new BindCallback() {
-            @Override public void onSuccess() {
-                setIncomingCallId(callId, extras);
-            }
-            @Override public void onFailure() {
-                errorCallback.run();
-            }
-        };
-
-        bind(callback);
-    }
-
-    /**
      * Cancels the incoming call for the specified call ID.
      * TODO(santoscordon): This method should be called by IncomingCallsManager when the incoming
      * call has failed.
@@ -198,13 +230,4 @@
         mServiceInterface = ICallService.Stub.asInterface(binder);
         setCallServiceAdapter(mAdapter);
     }
-
-    private boolean isServiceValid(String actionName) {
-        if (mServiceInterface != null) {
-            return true;
-        }
-
-        Log.wtf(this, "%s invoked while service is unbound", actionName);
-        return false;
-    }
 }
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 61f331b..cb9cab4 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -436,9 +436,9 @@
 
     /**
      * Stops playing the ringer if the specified call is the top-most incoming call. This exists
-     * separately from {@link #removeIncomingCall} for cases where we would like to stop playing the
-     * ringer for a call, but that call may still exist in {@link #mUnansweredIncomingCalls} - See
-     * {@link #rejectCall}, {@link #answerCall}.
+     * 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.
      */
diff --git a/src/com/android/telecomm/IncomingCallsManager.java b/src/com/android/telecomm/IncomingCallsManager.java
index 7d33d6b..3b6365f 100644
--- a/src/com/android/telecomm/IncomingCallsManager.java
+++ b/src/com/android/telecomm/IncomingCallsManager.java
@@ -71,8 +71,7 @@
             }
         };
 
-        // TODO(gilad): call.retrieve*Call() seems a bit unusual, consider revisiting.
-        call.getCallService().retrieveIncomingCall(callId, extras, errorCallback);
+        call.getCallService().setIncomingCallId(callId, extras, errorCallback);
     }
 
     /**
diff --git a/src/com/android/telecomm/OutgoingCallProcessor.java b/src/com/android/telecomm/OutgoingCallProcessor.java
index bcfda61..a242866 100644
--- a/src/com/android/telecomm/OutgoingCallProcessor.java
+++ b/src/com/android/telecomm/OutgoingCallProcessor.java
@@ -16,10 +16,6 @@
 
 package com.android.telecomm;
 
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Lists;
-
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -28,7 +24,9 @@
 
 import com.android.internal.telecomm.ICallServiceSelectionResponse;
 import com.android.internal.telecomm.ICallServiceSelector;
-import com.android.telecomm.ServiceBinder.BindCallback;
+import com.google.android.collect.Sets;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Lists;
 
 import java.util.Iterator;
 import java.util.List;
@@ -68,6 +66,17 @@
     private final Map<String, CallServiceWrapper> mCallServicesById = Maps.newHashMap();
 
     /**
+     * The set of attempted call services, used to ensure services are attempted at most once per
+     * outgoing-call attempt.
+     */
+    private final Set<CallServiceWrapper> mAttemptedCallServices = Sets.newHashSet();
+
+    /**
+     * The set of incompatible call services, used to suppress unnecessary call switching attempts.
+     */
+    private final Set<CallServiceWrapper> mIncompatibleCallServices = Sets.newHashSet();
+
+    /**
      * The list of currently-available call-service selector implementations.
      */
     private final List<ICallServiceSelector> mSelectors;
@@ -80,6 +89,12 @@
 
     private final Switchboard mSwitchboard;
 
+    private final Runnable mNextCallServiceCallback = new Runnable() {
+        @Override public void run() {
+            attemptNextCallService();
+        }
+    };
+
     /**
      * The iterator over the currently-selected ordered list of call-service descriptors.
      */
@@ -141,6 +156,34 @@
     }
 
     /**
+     * Handles the specified compatibility status from the call-service implementation.
+     * TODO(gilad): Consider making this class stateful, potentially rejecting out-of-order/
+     * unexpected invocations (i.e. beyond checking for unexpected call IDs).
+     *
+     * @param callId The ID of the call.
+     * @param isCompatible True if the call-service is compatible with the corresponding call and
+     *     false otherwise.
+     */
+    void setIsCompatibleWith(String callId, boolean isCompatible) {
+      if (callId != mCall.getId()) {
+          Log.wtf(this, "setIsCompatibleWith invoked with unexpected call ID: %s", callId);
+          return;
+      }
+
+      if (!mIsAborted) {
+          CallServiceWrapper callService = mCall.getCallService();
+          if (callService != null) {
+              if (isCompatible) {
+                  callService.call(mCall.toCallInfo(), mNextCallServiceCallback);
+                  return;
+              }
+              mIncompatibleCallServices.add(callService);
+          }
+          attemptNextCallService();
+      }
+    }
+
+    /**
      * Aborts the attempt to place the relevant call.  Intended to be invoked by
      * switchboard through the outgoing-calls manager.
      */
@@ -166,6 +209,9 @@
         }
 
         mCall.setState(CallState.DIALING);
+        for (CallServiceWrapper callService : mIncompatibleCallServices) {
+            mCall.addIncompatibleCallService(callService);
+        }
         mSwitchboard.handleSuccessfulOutgoingCall(mCall);
     }
 
@@ -263,23 +309,14 @@
             final CallServiceWrapper callService =
                     mCallServicesById.get(descriptor.getCallServiceId());
 
-            if (callService == null) {
+            if (callService == null || mAttemptedCallServices.contains(callService)) {
+                // The next call service is either null or has already been attempted, fast forward
+                // to the next.
                 attemptNextCallService();
             } else {
-                BindCallback callback = new BindCallback() {
-                    @Override public void onSuccess() {
-                        callService.call(mCall.toCallInfo());
-                    }
-                    @Override public void onFailure() {
-                        attemptNextCallService();
-                    }
-                };
-
+                mAttemptedCallServices.add(callService);
                 mCall.setCallService(callService);
-
-                // TODO(santoscordon): Consider making bind private to CallServiceWrapper and having
-                // CSWrapper.call() do the bind automatically.
-                callService.bind(callback);
+                callService.isCompatibleWith(mCall.toCallInfo(), mNextCallServiceCallback);
             }
         } else {
             mCallServiceDescriptorIterator = null;
diff --git a/src/com/android/telecomm/OutgoingCallsManager.java b/src/com/android/telecomm/OutgoingCallsManager.java
index 0ed30e6..c37665f 100644
--- a/src/com/android/telecomm/OutgoingCallsManager.java
+++ b/src/com/android/telecomm/OutgoingCallsManager.java
@@ -69,6 +69,24 @@
     }
 
     /**
+     * Forwards the compatibility status from the call-service implementation to the corresponding
+     * outgoing-call processor.
+     *
+     * @param callId The ID of the call.
+     * @param isCompatible True if the call-service is compatible with the corresponding call and
+     *     false otherwise.
+     */
+    void setCompatibleWith(String callId, boolean isCompatible) {
+        OutgoingCallProcessor processor = mOutgoingCallProcessors.get(callId);
+        if (processor == null) {
+            // Shouldn't happen, so log a wtf if it does.
+            Log.wtf(this, "Received unexpected setCompatibleWith notification.");
+        } else {
+            processor.setCompatibleWith(callId, isCompatible);
+        }
+    }
+
+    /**
      * Removes the outgoing call processor mapping for the successful call and returns execution to
      * the switchboard. This method is invoked from {@link CallServiceAdapter} after a call service
      * has notified Telecomm that it successfully placed the call.
diff --git a/src/com/android/telecomm/ServiceBinder.java b/src/com/android/telecomm/ServiceBinder.java
index 98ce4ca..b5c0050 100644
--- a/src/com/android/telecomm/ServiceBinder.java
+++ b/src/com/android/telecomm/ServiceBinder.java
@@ -44,6 +44,47 @@
         public void onFailure();
     }
 
+    /**
+     * Helper class to perform on-demand binding.
+     */
+    final class Binder {
+        /**
+         * Performs an asynchronous bind to the service (only if not already bound) and executes the
+         * specified callback.
+         *
+         * @param callback The callback to notify of the binding's success or failure.
+         */
+        void bind(BindCallback callback) {
+            ThreadUtil.checkOnMainThread();
+            Log.d(ServiceBinder.this, "bind()");
+
+            // Reset any abort request if we're asked to bind again.
+            clearAbort();
+
+            if (!mCallbacks.isEmpty()) {
+                // Binding already in progress, append to the list of callbacks and bail out.
+                mCallbacks.add(callback);
+                return;
+            }
+
+            mCallbacks.add(callback);
+            if (mServiceConnection == null) {
+                Intent serviceIntent = new Intent(mServiceAction).setComponent(mComponentName);
+                ServiceConnection connection = new ServiceBinderConnection();
+
+                Log.d(ServiceBinder.this, "Binding to call service with intent: %s", serviceIntent);
+                if (!mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
+                    handleFailedConnection();
+                    return;
+                }
+            } else {
+                Log.d(ServiceBinder.this, "Service is already bound.");
+                Preconditions.checkNotNull(mBinder);
+                handleSuccessfulConnection();
+            }
+        }
+    }
+
     private final class ServiceBinderConnection implements ServiceConnection {
         @Override
         public void onServiceConnected(ComponentName componentName, IBinder binder) {
@@ -58,8 +99,7 @@
             }
 
             mServiceConnection = this;
-            mBinder = binder;
-            setServiceInterface(binder);
+            setBinder(binder);
             handleSuccessfulConnection();
         }
 
@@ -114,45 +154,6 @@
         mComponentName = componentName;
     }
 
-    /**
-     * Performs an asynchronous bind to the service if not already bound.
-     *
-     * @param callback The callback to notify of the binding's success or failure.
-     * @return The result of {#link Context#bindService} or true if already bound.
-     */
-    final boolean bind(BindCallback callback) {
-        ThreadUtil.checkOnMainThread();
-        Log.d(this, "bind()");
-
-        // Reset any abort request if we're asked to bind again.
-        clearAbort();
-
-        // If we are already waiting on a binding request, simply append to the list of waiting
-        // callbacks.
-        if (!mCallbacks.isEmpty()) {
-            mCallbacks.add(callback);
-            return true;
-        }
-
-        mCallbacks.add(callback);
-        if (mServiceConnection == null) {
-            Intent serviceIntent = new Intent(mServiceAction).setComponent(mComponentName);
-            ServiceConnection connection = new ServiceBinderConnection();
-
-            Log.d(this, "Binding to call service with intent: %s", serviceIntent);
-            if (!mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
-                handleFailedConnection();
-                return false;
-            }
-        } else {
-            Log.d(this, "Service is already bound.");
-            Preconditions.checkNotNull(mBinder);
-            handleSuccessfulConnection();
-        }
-
-        return true;
-    }
-
     final void incrementAssociatedCallCount() {
         mAssociatedCallCount++;
     }
@@ -182,7 +183,7 @@
         } else {
             mContext.unbindService(mServiceConnection);
             mServiceConnection = null;
-            mBinder = null;
+            setBinder(null);
         }
     }
 
@@ -190,6 +191,15 @@
         return mComponentName;
     }
 
+    final boolean isServiceValid(String actionName) {
+        if (mBinder == null) {
+            Log.wtf(this, "%s invoked while service is unbound", actionName);
+            return false;
+        }
+
+        return true;
+    }
+
     /**
      * Notifies all the outstanding callbacks that the service is successfully bound. The list of
      * outstanding callbacks is cleared afterwards.
@@ -216,7 +226,7 @@
      * Handles a service disconnection.
      */
     private void handleServiceDisconnected() {
-        setServiceInterface(null);
+        setBinder(null);
     }
 
     private void clearAbort() {
@@ -224,6 +234,16 @@
     }
 
     /**
+     * Sets the (private) binder and updates the child class.
+     *
+     * @param binder The new binder value.
+     */
+    private void setBinder(IBinder binder) {
+        mBinder = binder;
+        setServiceInterface(binder);
+    }
+
+    /**
      * Sets the service interface after the service is bound or unbound.
      *
      * @param binder The actual bound service implementation.