Merge "Add Call Id to state change broadcast" into master-nova
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 295e3dc..780906c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,6 +23,8 @@
          the user presses "home". -->
     <!-- TODO(gilad): Better understand/document this use case. -->
     <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
 
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"></uses-permission>
 
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..6905860
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Telecomm resources that may need to be customized for different hardware or product
+    builds. -->
+<resources>
+    <!-- Determines if the current device should allow emergency numbers to be logged in the
+         call log. Some carriers require that emergency calls *not* be logged, presumably to
+         avoid the risk of accidental redialing from the call log UI.
+         The default is false. -->
+    <bool name="allow_emergency_numbers_in_call_log">false</bool>
+</resources>
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 1463c51..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() {
@@ -132,6 +140,14 @@
         return new Date().getTime() - mCreationTime.getTime();
     }
 
+    /**
+     * @return The time when this call object was created and added to the set of pending outgoing
+     *     calls.
+     */
+    long getCreationTimeInMilliseconds() {
+        return mCreationTime.getTime();
+    }
+
     CallServiceWrapper getCallService() {
         return mCallService;
     }
@@ -169,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/CallLogManager.java b/src/com/android/telecomm/CallLogManager.java
index eb3b0f5..de9b301 100644
--- a/src/com/android/telecomm/CallLogManager.java
+++ b/src/com/android/telecomm/CallLogManager.java
@@ -1,5 +1,248 @@
+/*
+ * Copyright 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;
 
-/** Package private */
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.internal.telephony.PhoneConstants;
+
+/**
+ * Helper class that provides functionality to write information about calls and their associated
+ * caller details to the call log. All logging activity will be performed asynchronously in a
+ * background thread to avoid blocking on the main thread.
+ */
 class CallLogManager {
+    /**
+     * Parameter object to hold the arguments to add a call in the call log DB.
+     */
+    private static class AddCallArgs {
+        /**
+         * @param contactInfo Caller details.
+         * @param number The phone number to be logged.
+         * @param presentation Number presentation of the phone number to be logged.
+         * @param callType The type of call (e.g INCOMING_TYPE). @see
+         *     {@link android.provider.CallLog} for the list of values.
+         * @param creationDate Time when the call was created (milliseconds since epoch).
+         * @param durationInMillis Duration of the call (milliseconds).
+         */
+        public AddCallArgs(Context context, ContactInfo contactInfo, String number,
+                int presentation, int callType, long creationDate, long durationInMillis) {
+            this.context = context;
+            this.contactInfo = contactInfo;
+            this.number = number;
+            this.presentation = presentation;
+            this.callType = callType;
+            this.timestamp = creationDate;
+            this.durationInSec = (int)(durationInMillis / 1000);
+        }
+        // Since the members are accessed directly, we don't use the
+        // mXxxx notation.
+        public final Context context;
+        public final ContactInfo contactInfo;
+        public final String number;
+        public final int presentation;
+        public final int callType;
+        public final long timestamp;
+        public final int durationInSec;
+    }
+
+    private static final String TAG = CallLogManager.class.getSimpleName();
+
+    private final Context mContext;
+
+    public CallLogManager(Context context) {
+        mContext = context;
+    }
+
+    void logDisconnectedCall(Call call) {
+        // TODO: Until we add more state to the Call object to track whether this disconnected
+        // call orginated as an incoming or outgoing call, always log it as an incoming call.
+        // See b/13420887.
+        logCall(call, Calls.INCOMING_TYPE);
+    }
+
+    void logFailedOutgoingCall(Call call) {
+        logCall(call, Calls.OUTGOING_TYPE);
+    }
+
+    void logMissedCall(Call call) {
+        logCall(call, Calls.MISSED_TYPE);
+    }
+
+    /**
+     * Logs a call to the call log based on the {@link Call} object passed in.
+     *
+     * @param call The call object being logged
+     * @param callLogType The type of call log entry to log this call as. See:
+     *     {@link android.provider.CallLog.Calls#INCOMING_TYPE}
+     *     {@link android.provider.CallLog.Calls#OUTGOING_TYPE}
+     *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
+     */
+    private void logCall(Call call, int callLogType) {
+        String number = call.getHandle();
+        final long creationTime = call.getCreationTimeInMilliseconds();
+        final long age = call.getAgeInMilliseconds();
+
+        final ContactInfo contactInfo = call.getContactInfo();  // May be null.
+        final String logNumber = getLogNumber(call);
+
+        Log.d(TAG, "logNumber set to:" + Log.pii(logNumber) + ", number set to: "
+                + Log.pii(number));
+
+        final int presentation = getPresentation(call, contactInfo);
+
+        logCall(contactInfo, logNumber, presentation, callLogType, creationTime, age);
+    }
+
+    /**
+     * Inserts a call into the call log, based on the parameters passed in.
+     *
+     * @param contactInfo Caller details.
+     * @param number The number the call was made to or from.
+     * @param presentation
+     * @param callType The type of call.
+     * @param start The start time of the call, in milliseconds.
+     * @param duration The duration of the call, in milliseconds.
+     */
+    private void logCall(
+            ContactInfo contactInfo,
+            String number,
+            int presentation,
+            int callType,
+            long start,
+            long duration) {
+        boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(number, mContext);
+
+        // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
+        // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
+        // on carrier requirements.)
+        final boolean okToLogEmergencyNumber =
+                mContext.getResources().getBoolean(R.bool.allow_emergency_numbers_in_call_log);
+
+        // Don't log emergency numbers if the device doesn't allow it.
+        final boolean isOkToLogThisCall = !isEmergencyNumber || okToLogEmergencyNumber;
+
+        if (isOkToLogThisCall) {
+            Log.d(TAG, "Logging Calllog entry: " + contactInfo + ", "
+                    + Log.pii(number) + "," + presentation + ", " + callType
+                    + ", " + start + ", " + duration);
+            AddCallArgs args = new AddCallArgs(mContext, contactInfo, number, presentation,
+                    callType, start, duration);
+            logCallAsync(args);
+        } else {
+          Log.d(TAG, "Not adding emergency call to call log.");
+        }
+    }
+
+    /**
+     * Retrieve the phone number from the call, and then process it before returning the
+     * actual number that is to be logged.
+     *
+     * @param call The phone connection.
+     * @return the phone number to be logged.
+     */
+    private String getLogNumber(Call call) {
+        String handle = call.getHandle();
+
+        if (handle == null) {
+            return null;
+        }
+
+        if (!PhoneNumberUtils.isUriNumber(handle)) {
+            handle = PhoneNumberUtils.stripSeparators(handle);
+        }
+        return handle;
+    }
+
+    /**
+     * Gets the presentation from the {@link ContactInfo} if not null. Otherwise, gets it from the
+     * {@link Call}.
+     *
+     * TODO: There needs to be a way to pass information from
+     * Connection.getNumberPresentation() into a {@link Call} object. Until then, always return
+     * PhoneConstants.PRESENTATION_ALLOWED. On top of that, we might need to introduce
+     * getNumberPresentation to the ContactInfo object as well.
+     *
+     * @param call The call object to retrieve caller details from.
+     * @param contactInfo The CallerInfo. May be null.
+     * @return The number presentation constant to insert into the call logs.
+     */
+    private int getPresentation(Call call, ContactInfo contactInfo) {
+        return PhoneConstants.PRESENTATION_ALLOWED;
+    }
+
+    /**
+     * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider
+     * using an AsyncTask to avoid blocking the main thread.
+     *
+     * @param args Prepopulated call details.
+     * @return A handle to the AsyncTask that will add the call to the call log asynchronously.
+     */
+    public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
+        return new LogCallAsyncTask().execute(args);
+    }
+
+    /**
+     * Helper AsyncTask to access the call logs database asynchronously since database operations
+     * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
+     * its own thread pool.
+     */
+    private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
+        @Override
+        protected Uri[] doInBackground(AddCallArgs... callList) {
+            int count = callList.length;
+            Uri[] result = new Uri[count];
+            for (int i = 0; i < count; i++) {
+                AddCallArgs c = callList[i];
+
+                try {
+                    // May block.
+                    result[i] = Calls.addCall(null, c.context, c.number, c.presentation,
+                            c.callType, c.timestamp, c.durationInSec);
+                } catch (Exception e) {
+                    // This is very rare but may happen in legitimate cases.
+                    // E.g. If the phone is encrypted and thus write request fails, it may cause
+                    // some kind of Exception (right now it is IllegalArgumentException, but this
+                    // might change).
+                    //
+                    // We don't want to crash the whole process just because of that, so just log
+                    // it instead.
+                    Log.e(TAG, e, "Exception raised during adding CallLog entry.");
+                    result[i] = null;
+                }
+            }
+            return result;
+        }
+
+        /**
+         * Performs a simple sanity check to make sure the call was written in the database.
+         * Typically there is only one result per call so it is easy to identify which one failed.
+         */
+        @Override
+        protected void onPostExecute(Uri[] result) {
+            for (Uri uri : result) {
+                if (uri == null) {
+                    Log.w(TAG, "Failed to write call to the log.");
+                }
+            }
+        }
+    }
 }
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 871558f..b42e36c 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -94,6 +94,7 @@
         mSwitchboard = new Switchboard(this);
         mInCallController = new InCallController(this);
         mRinger = new Ringer();
+        mCallLogManager = new CallLogManager(TelecommApp.getInstance());
     }
 
     static CallsManager getInstance() {
@@ -186,6 +187,15 @@
     }
 
     /**
+     * Informs mCallLogManager about the outgoing call that failed, so that it can be logged.
+     *
+     * @param call The failed outgoing call.
+     */
+    void handleFailedOutgoingCall(Call call) {
+        mCallLogManager.logFailedOutgoingCall(call);
+    }
+
+    /**
      * Instructs Telecomm to answer the specified call. Intended to be invoked by the in-call
      * app through {@link InCallAdapter} after Telecomm notifies it of an incoming call followed by
      * the user opting to answer said call.
@@ -291,6 +301,9 @@
         if (mCalls.isEmpty()) {
             mInCallController.unbind();
         }
+
+        // Log the call in the call log.
+        mCallLogManager.logDisconnectedCall(call);
     }
 
     /**
@@ -425,9 +438,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..7c2a501 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 setIsCompatibleWith(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.setIsCompatibleWith(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.
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index 040eb5f..af8d380 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -211,8 +211,6 @@
     void handleFailedOutgoingCall(Call call) {
         Log.d(this, "handleFailedOutgoingCall");
 
-        // TODO(gilad): Notify mCallsManager.
-
         finalizeOutgoingCall(call);
     }
 
diff --git a/tests/src/com/android/telecomm/testcallservice/TestCallService.java b/tests/src/com/android/telecomm/testcallservice/TestCallService.java
index db41055..4cbaf71 100644
--- a/tests/src/com/android/telecomm/testcallservice/TestCallService.java
+++ b/tests/src/com/android/telecomm/testcallservice/TestCallService.java
@@ -48,7 +48,7 @@
     /**
      * Adapter to call back into CallsManager.
      */
-    private CallServiceAdapter mCallsManagerAdapter;
+    private CallServiceAdapter mTelecommAdapter;
 
     /**
      * Used to play an audio tone during a call.
@@ -60,7 +60,7 @@
     public void setCallServiceAdapter(CallServiceAdapter callServiceAdapter) {
         Log.i(TAG, "setCallServiceAdapter()");
 
-        mCallsManagerAdapter = callServiceAdapter;
+        mTelecommAdapter = callServiceAdapter;
         mLiveCallIds = Sets.newHashSet();
 
         // Prepare the media player to play a tone when there is a call.
@@ -89,7 +89,7 @@
         // Returning positively on setCompatibleWith() doesn't guarantee that we will be chosen
         // to place the call. If we *are* chosen then CallsManager will execute the call()
         // method below.
-        mCallsManagerAdapter.setIsCompatibleWith(callInfo.getId(), isCompatible);
+        mTelecommAdapter.setIsCompatibleWith(callInfo.getId(), isCompatible);
     }
 
     /**
@@ -103,8 +103,7 @@
         Log.i(TAG, "call(" + callInfo + ")");
 
         createCall(callInfo.getId());
-
-        mCallsManagerAdapter.handleSuccessfulOutgoingCall(callInfo.getId());
+        mTelecommAdapter.handleSuccessfulOutgoingCall(callInfo.getId());
     }
 
     /** {@inheritDoc} */
@@ -123,19 +122,19 @@
         String handle = "5551234";
 
         CallInfo callInfo = new CallInfo(callId, CallState.RINGING, handle);
-        mCallsManagerAdapter.notifyIncomingCall(callInfo);
+        mTelecommAdapter.notifyIncomingCall(callInfo);
     }
 
     /** {@inheritDoc} */
     @Override
     public void answer(String callId) {
-        mCallsManagerAdapter.setActive(callId);
+        mTelecommAdapter.setActive(callId);
     }
 
     /** {@inheritDoc} */
     @Override
     public void reject(String callId) {
-        mCallsManagerAdapter.setDisconnected(callId);
+        mTelecommAdapter.setDisconnected(callId);
     }
 
     /** {@inheritDoc} */
@@ -144,7 +143,7 @@
         Log.i(TAG, "disconnect(" + callId + ")");
 
         destroyCall(callId);
-        mCallsManagerAdapter.setDisconnected(callId);
+        mTelecommAdapter.setDisconnected(callId);
     }
 
     /** {@inheritDoc} */
diff --git a/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java b/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java
index 80c82db..781f629 100644
--- a/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java
+++ b/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java
@@ -16,8 +16,6 @@
 
 package com.android.telecomm.testcallservice;
 
-import android.content.ComponentName;
-import android.os.IBinder;
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallServiceLookupResponse;
 import android.telecomm.CallServiceProvider;
@@ -25,8 +23,6 @@
 
 import com.google.common.collect.Lists;
 
-import java.util.List;
-
 /**
  * Service which provides fake calls to test the ICallService interface.
  * TODO(santoscordon): Build more dummy providers for more CallServiceDescriptor.FLAG_* types.