Merge "Add CALL_PRIVILEGED" into master-nova
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 0eef378..6aae5ca 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -28,7 +28,6 @@
 import com.google.android.collect.Sets;
 import com.google.common.base.Preconditions;
 
-import java.util.Date;
 import java.util.Locale;
 import java.util.Set;
 
@@ -50,7 +49,9 @@
      * Beyond logging and such, may also be used for bookkeeping and specifically for marking
      * certain call attempts as failed attempts.
      */
-    private final Date mCreationTime;
+    private final long mCreationTimeMillis = System.currentTimeMillis();
+
+    private long mConnectTimeMillis;
 
     /** The state of the call. */
     private CallState mState;
@@ -86,7 +87,7 @@
      * Disconnect cause for the call. Only valid if the state of the call is DISCONNECTED.
      * See {@link android.telephony.DisconnectCause}.
      */
-    private int mDisconnectCause;
+    private int mDisconnectCause = DisconnectCause.NOT_VALID;
 
     /**
      * Additional disconnect information provided by the call service.
@@ -94,7 +95,7 @@
     private String mDisconnectMessage;
 
     /** Info used by the call services. */
-    private Bundle mExtras;
+    private Bundle mExtras = Bundle.EMPTY;
 
     /** The Uri to dial to perform the handoff. If this is null then handoff is not supported. */
     private Uri mHandoffHandle;
@@ -106,6 +107,12 @@
     private Call mOriginalCall;
 
     /**
+     * The descriptor for the call service that this call is being switched to, null if handoff is
+     * not in progress.
+     */
+    private CallServiceDescriptor mHandoffCallServiceDescriptor;
+
+    /**
      * Creates an empty call object.
      *
      * @param isIncoming True if this is an incoming call.
@@ -128,9 +135,6 @@
         mContactInfo = contactInfo;
         mGatewayInfo = gatewayInfo;
         mIsIncoming = isIncoming;
-        mCreationTime = new Date();
-        mDisconnectCause = DisconnectCause.NOT_VALID;
-        mExtras = Bundle.EMPTY;
     }
 
     /** {@inheritDoc} */
@@ -218,18 +222,27 @@
 
     /**
      * @return The "age" of this call object in milliseconds, which typically also represents the
-     *     period since this call was added to the set pending outgoing calls, see mCreationTime.
+     *     period since this call was added to the set pending outgoing calls, see
+     *     mCreationTimeMillis.
      */
-    long getAgeMs() {
-        return new Date().getTime() - mCreationTime.getTime();
+    long getAgeMillis() {
+        return System.currentTimeMillis() - mCreationTimeMillis;
     }
 
     /**
      * @return The time when this call object was created and added to the set of pending outgoing
      *     calls.
      */
-    long getCreationTimeMs() {
-        return mCreationTime.getTime();
+    long getCreationTimeMillis() {
+        return mCreationTimeMillis;
+    }
+
+    long getConnectTimeMillis() {
+        return mConnectTimeMillis;
+    }
+
+    void setConnectTimeMillis(long connectTimeMillis) {
+        mConnectTimeMillis = connectTimeMillis;
     }
 
     CallServiceWrapper getCallService() {
@@ -237,13 +250,25 @@
     }
 
     void setCallService(CallServiceWrapper callService) {
+        setCallService(callService, null);
+    }
+
+    /**
+     * Changes the call service this call is associated with. If callToReplace is non-null then this
+     * call takes its place within the call service.
+     */
+    void setCallService(CallServiceWrapper callService, Call callToReplace) {
         Preconditions.checkNotNull(callService);
 
         clearCallService();
 
         callService.incrementAssociatedCallCount();
         mCallService = callService;
-        mCallService.addCall(this);
+        if (callToReplace == null) {
+            mCallService.addCall(this);
+        } else {
+            mCallService.replaceCall(this, callToReplace);
+        }
     }
 
     /**
@@ -460,6 +485,14 @@
         mOriginalCall = originalCall;
     }
 
+    CallServiceDescriptor getHandoffCallServiceDescriptor() {
+        return mHandoffCallServiceDescriptor;
+    }
+
+    void setHandoffCallServiceDescriptor(CallServiceDescriptor descriptor) {
+        mHandoffCallServiceDescriptor = descriptor;
+    }
+
     /**
      * @return True if the call is ringing, else logs the action name.
      */
diff --git a/src/com/android/telecomm/CallIdMapper.java b/src/com/android/telecomm/CallIdMapper.java
index aec3b5e..a5d70bf 100644
--- a/src/com/android/telecomm/CallIdMapper.java
+++ b/src/com/android/telecomm/CallIdMapper.java
@@ -30,6 +30,14 @@
         mCallIdPrefix = callIdPrefix + "@";
     }
 
+    void replaceCall(Call newCall, Call callToReplace) {
+        ThreadUtil.checkOnMainThread();
+
+        // Use the old call's ID for the new call.
+        String callId = getCallId(callToReplace);
+        mCalls.put(callId, newCall);
+    }
+
     void addCall(Call call) {
         ThreadUtil.checkOnMainThread();
         Preconditions.checkNotNull(call);
diff --git a/src/com/android/telecomm/CallLogManager.java b/src/com/android/telecomm/CallLogManager.java
index 8bdc150..fcaf5f2 100644
--- a/src/com/android/telecomm/CallLogManager.java
+++ b/src/com/android/telecomm/CallLogManager.java
@@ -101,8 +101,8 @@
      *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
      */
     private void logCall(Call call, int callLogType) {
-        final long creationTime = call.getCreationTimeMs();
-        final long age = call.getAgeMs();
+        final long creationTime = call.getCreationTimeMillis();
+        final long age = call.getAgeMillis();
 
         final ContactInfo contactInfo = call.getContactInfo();  // May be null.
         final String logNumber = getLogNumber(call);
diff --git a/src/com/android/telecomm/CallServiceRepository.java b/src/com/android/telecomm/CallServiceRepository.java
index 7247259..50ef977 100644
--- a/src/com/android/telecomm/CallServiceRepository.java
+++ b/src/com/android/telecomm/CallServiceRepository.java
@@ -139,7 +139,7 @@
                 mOutstandingProviders.size());
 
         // Schedule a timeout.
-        mHandler.postDelayed(mTimeoutLookupTerminator, Timeouts.getProviderLookupMs());
+        mHandler.postDelayed(mTimeoutLookupTerminator, Timeouts.getProviderLookupMillis());
     }
 
     /**
@@ -168,8 +168,8 @@
 
             // TODO(gilad): Either add ICallService.getActiveCallCount() or have this tracked by the
             // Switchboard if we rather not rely on 3rd-party code to do the bookkeeping for us. If
-            // we prefer the latter, we can also have purgeInactiveCallService(descriptor). Otherwise
-            // this might look something like:
+            // we prefer the latter, we can also have purgeInactiveCallService(descriptor).
+            // Otherwise this might look something like:
             //
             // if (callService.getActiveCallCount() < 1) {
             //     mCallServices.remove(callServiceName);
diff --git a/src/com/android/telecomm/CallServiceWrapper.java b/src/com/android/telecomm/CallServiceWrapper.java
index 007d35a..70f0ec2 100644
--- a/src/com/android/telecomm/CallServiceWrapper.java
+++ b/src/com/android/telecomm/CallServiceWrapper.java
@@ -28,6 +28,7 @@
 import com.android.internal.telecomm.ICallService;
 import com.android.internal.telecomm.ICallServiceAdapter;
 import com.android.internal.telecomm.ICallServiceProvider;
+import com.google.common.base.Preconditions;
 
 /**
  * Wrapper for {@link ICallService}s, handles binding to {@link ICallService} and keeps track of
@@ -253,6 +254,14 @@
         mCallIdMapper.addCall(call);
     }
 
+    /**
+     * Associates newCall with this call service by replacing callToReplace.
+     */
+    void replaceCall(Call newCall, Call callToReplace) {
+        Preconditions.checkState(callToReplace.getCallService() == this);
+        mCallIdMapper.replaceCall(newCall, callToReplace);
+    }
+
     void removeCall(Call call) {
         mAdapter.removePendingCall(call);
         mCallIdMapper.removeCall(call);
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index b8724e3..d3b22be 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -23,6 +23,7 @@
 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;
@@ -51,9 +52,17 @@
         void onCallAdded(Call call);
         void onCallRemoved(Call call);
         void onCallStateChanged(Call call, CallState oldState, CallState newState);
+        void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle);
+        void onCallServiceChanged(
+                Call call,
+                CallServiceWrapper oldCallService,
+                CallServiceWrapper newCallService);
+        void onCallHandoffCallServiceDescriptorChanged(
+                Call call,
+                CallServiceDescriptor oldDescriptor,
+                CallServiceDescriptor newDescriptor);
         void onIncomingCallAnswered(Call call);
         void onIncomingCallRejected(Call call);
-        void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle);
         void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
         void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
     }
@@ -231,6 +240,15 @@
      */
     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());
+        }
     }
 
     /**
@@ -421,10 +439,13 @@
     }
 
     void markCallAsActive(Call call) {
+        if (call.getConnectTimeMillis() == 0) {
+            call.setConnectTimeMillis(System.currentTimeMillis());
+        }
         setCallState(call, CallState.ACTIVE);
 
         if (mPendingHandoffCalls.contains(call)) {
-            completeHandoff(call);
+            completeHandoff(call, true);
         }
     }
 
@@ -442,7 +463,11 @@
     void markCallAsDisconnected(Call call, int disconnectCause, String disconnectMessage) {
         call.setDisconnectCause(disconnectCause, disconnectMessage);
         setCallState(call, CallState.DISCONNECTED);
-        removeCall(call);
+
+        // Only remove the call if handoff is not pending.
+        if (call.getHandoffCallServiceDescriptor() == null) {
+            removeCall(call);
+        }
     }
 
     void setHandoffInfo(Call call, Uri handle, Bundle extras) {
@@ -496,6 +521,9 @@
     }
 
     private void removeCall(Call call) {
+        // If a handoff is pending then the original call shouldn't be removed.
+        Preconditions.checkState(call.getHandoffCallServiceDescriptor() == null);
+
         call.clearCallService();
         call.clearCallServiceSelector();
 
@@ -503,10 +531,9 @@
         if (mCalls.contains(call)) {
             mCalls.remove(call);
             shouldNotify = true;
-            cleanUpHandoffCallsForOriginalCall(call);
         } else if (mPendingHandoffCalls.contains(call)) {
-            Log.v(this, "silently removing handoff call %s", call);
-            mPendingHandoffCalls.remove(call);
+            Log.v(this, "removeCall, marking handoff call as failed");
+            completeHandoff(call, false);
         }
 
         // Only broadcast changes for calls that are being tracked.
@@ -527,6 +554,7 @@
     private void setCallState(Call call, CallState newState) {
         Preconditions.checkNotNull(newState);
         CallState oldState = call.getState();
+        Log.i(this, "setCallState %s -> %s, call: %s", oldState, newState, call);
         if (newState != oldState) {
             // Unfortunately, in the telephony world the radio is king. So if the call notifies
             // us that the call is in a particular state, we allow it even if it doesn't make
@@ -574,45 +602,63 @@
         }
     }
 
-    private void completeHandoff(Call handoffCall) {
+    private void completeHandoff(Call handoffCall, boolean wasSuccessful) {
         Call originalCall = handoffCall.getOriginalCall();
-        Log.v(this, "complete handoff, %s -> %s", handoffCall, originalCall);
+        Log.v(this, "complete handoff, %s -> %s, wasSuccessful: %b", handoffCall, originalCall,
+                wasSuccessful);
 
-        // Disconnect.
-        originalCall.disconnect();
+        // Remove the transient handoff call object (don't disconnect because the call could still
+        // be live).
+        mPendingHandoffCalls.remove(handoffCall);
 
-        // Synchronize.
-        originalCall.setCallService(handoffCall.getCallService());
-        setCallState(originalCall, handoffCall.getState());
+        if (wasSuccessful) {
+            // Disconnect.
+            originalCall.disconnect();
 
-        // Remove the transient handoff call object (don't disconnect because the call is still
-        // live).
-        removeCall(handoffCall);
+            // Synchronize.
+            originalCall.setCallService(handoffCall.getCallService(), handoffCall);
+            setCallState(originalCall, handoffCall.getState());
 
-        // Force the foreground call changed notification to be sent.
-        for (CallsManagerListener listener : mListeners) {
-            listener.onForegroundCallChanged(mForegroundCall, mForegroundCall);
+            // Force the foreground call changed notification to be sent.
+            for (CallsManagerListener listener : mListeners) {
+                listener.onForegroundCallChanged(mForegroundCall, mForegroundCall);
+            }
+
+            updateHandoffCallServiceDescriptor(originalCall, null);
+        } else {
+            updateHandoffCallServiceDescriptor(originalCall, null);
+            if (originalCall.getState() == CallState.DISCONNECTED ||
+                    originalCall.getState() == CallState.ABORTED) {
+                removeCall(originalCall);
+            }
         }
     }
 
-    /** Makes sure there are no dangling handoff calls. */
-    private void cleanUpHandoffCallsForOriginalCall(Call originalCall) {
-        for (Call handoffCall : ImmutableList.copyOf((mPendingHandoffCalls))) {
-            if (handoffCall.getOriginalCall() == originalCall) {
-                Log.d(this, "cancelling handoff call %s for originalCall: %s", handoffCall,
-                        originalCall);
-                if (handoffCall.getState() == CallState.NEW) {
-                    handoffCall.abort();
-                    handoffCall.setState(CallState.ABORTED);
-                } else {
-                    handoffCall.disconnect();
-                    handoffCall.setState(CallState.DISCONNECTED);
-                }
-                removeCall(handoffCall);
+    private void updateHandoffCallServiceDescriptor(
+            Call originalCall,
+            CallServiceDescriptor newDescriptor) {
+        CallServiceDescriptor oldDescriptor = originalCall.getHandoffCallServiceDescriptor();
+        Log.v(this, "updateHandoffCallServiceDescriptor, call: %s, pending descriptor: %s -> %s",
+                originalCall, oldDescriptor, newDescriptor);
+
+        if (!areDescriptorsEqual(oldDescriptor, newDescriptor)) {
+            originalCall.setHandoffCallServiceDescriptor(newDescriptor);
+            for (CallsManagerListener listener : mListeners) {
+                listener.onCallHandoffCallServiceDescriptorChanged(originalCall, oldDescriptor,
+                        newDescriptor);
             }
         }
     }
 
+    private static boolean areDescriptorsEqual(
+            CallServiceDescriptor descriptor1,
+            CallServiceDescriptor descriptor2) {
+        if (descriptor1 == null) {
+            return descriptor2 == null;
+        }
+        return descriptor1.equals(descriptor2);
+    }
+
     private static boolean areUriEqual(Uri handle1, Uri handle2) {
         if (handle1 == null) {
             return handle2 == null;
diff --git a/src/com/android/telecomm/CallsManagerListenerBase.java b/src/com/android/telecomm/CallsManagerListenerBase.java
index 6144651..8286857 100644
--- a/src/com/android/telecomm/CallsManagerListenerBase.java
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -18,6 +18,7 @@
 
 import android.net.Uri;
 import android.telecomm.CallAudioState;
+import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
 
 /**
@@ -37,6 +38,24 @@
     }
 
     @Override
+    public void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle) {
+    }
+
+    @Override
+    public void onCallServiceChanged(
+            Call call,
+            CallServiceWrapper oldCallServiceWrapper,
+            CallServiceWrapper newCallService) {
+    }
+
+    @Override
+    public void onCallHandoffCallServiceDescriptorChanged(
+            Call call,
+            CallServiceDescriptor oldDescriptor,
+            CallServiceDescriptor newDescriptor) {
+    }
+
+    @Override
     public void onIncomingCallAnswered(Call call) {
     }
 
@@ -45,10 +64,6 @@
     }
 
     @Override
-    public void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle) {
-    }
-
-    @Override
     public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
     }
 
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index beac0d5..395e92e 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -24,7 +24,10 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.telecomm.CallAudioState;
-import android.telecomm.CallInfo;
+import android.telecomm.CallCapabilities;
+import android.telecomm.CallServiceDescriptor;
+import android.telecomm.CallState;
+import android.telecomm.InCallCall;
 import android.telecomm.CallState;
 
 import com.android.internal.telecomm.IInCallService;
@@ -85,9 +88,8 @@
         } else {
             Log.i(this, "Adding call: %s", call);
             mCallIdMapper.addCall(call);
-            CallInfo callInfo = call.toCallInfo(mCallIdMapper.getCallId(call));
             try {
-                mInCallService.addCall(callInfo);
+                mInCallService.addCall(toInCallCall(call));
             } catch (RemoteException e) {
             }
         }
@@ -104,47 +106,28 @@
 
     @Override
     public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
-        if (mInCallService == null) {
-            return;
-        }
-
-        String callId = mCallIdMapper.getCallId(call);
-        switch (newState) {
-            case ACTIVE:
-                Log.i(this, "Mark call as ACTIVE: %s", callId);
-                try {
-                    mInCallService.setActive(callId);
-                } catch (RemoteException e) {
-                }
-                break;
-            case ON_HOLD:
-                Log.i(this, "Mark call as HOLD: %s", callId);
-                try {
-                    mInCallService.setOnHold(callId);
-                } catch (RemoteException e) {
-                }
-                break;
-            case DISCONNECTED:
-                Log.i(this, "Mark call as DISCONNECTED: %s", callId);
-                try {
-                    mInCallService.setDisconnected(callId, call.getDisconnectCause());
-                } catch (RemoteException e) {
-                }
-                break;
-            default:
-                break;
-        }
+        updateCall(call);
     }
 
     @Override
     public void onCallHandoffHandleChanged(Call call, Uri oldHandle, Uri newHandle) {
-        if (mInCallService != null) {
-            try {
-                mInCallService.setHandoffEnabled(mCallIdMapper.getCallId(call), newHandle != null);
-            } catch (RemoteException e) {
-                Log.e(this, e, "Exception attempting to call setHandoffEnabled.");
-            }
-        }
+        updateCall(call);
+    }
+
+    @Override
+    public void onCallServiceChanged(
+            Call call,
+            CallServiceWrapper oldCallServiceWrapper,
+            CallServiceWrapper newCallService) {
+        updateCall(call);
+    }
+
+    @Override
+    public void onCallHandoffCallServiceDescriptorChanged(
+            Call call,
+            CallServiceDescriptor oldDescriptor,
+            CallServiceDescriptor newDescriptor) {
+        updateCall(call);
     }
 
     @Override
@@ -219,7 +202,6 @@
         if (!calls.isEmpty()) {
             for (Call call : calls) {
                 onCallAdded(call);
-                onCallHandoffHandleChanged(call, null, call.getHandoffHandle());
             }
             onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
         } else {
@@ -234,4 +216,36 @@
         ThreadUtil.checkOnMainThread();
         mInCallService = null;
     }
+
+    private void updateCall(Call call) {
+        if (mInCallService != null) {
+            try {
+                mInCallService.updateCall(toInCallCall(call));
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    private InCallCall toInCallCall(Call call) {
+        String callId = mCallIdMapper.getCallId(call);
+        CallServiceDescriptor descriptor =
+                call.getCallService() != null ? call.getCallService().getDescriptor() : null;
+
+        boolean isHandoffCapable = call.getHandoffHandle() != null;
+        int capabilities = CallCapabilities.HOLD | CallCapabilities.MUTE;
+        if (call.getHandoffHandle() != null) {
+            capabilities |= CallCapabilities.CONNECTION_HANDOFF;
+        }
+        CallState state = call.getState();
+        if (state == CallState.ABORTED) {
+            state = CallState.DISCONNECTED;
+        }
+        // TODO(sail): Remove this and replace with final reconnecting code.
+        if (state == CallState.DISCONNECTED && call.getHandoffCallServiceDescriptor() != null) {
+            state = CallState.ACTIVE;
+        }
+        return new InCallCall(callId, state, call.getDisconnectCause(), capabilities,
+                call.getConnectTimeMillis(), call.getHandle(), call.getGatewayInfo(), descriptor,
+                call.getHandoffCallServiceDescriptor());
+    }
 }
diff --git a/src/com/android/telecomm/InCallTonePlayer.java b/src/com/android/telecomm/InCallTonePlayer.java
index 58e0423..258f8b0 100644
--- a/src/com/android/telecomm/InCallTonePlayer.java
+++ b/src/com/android/telecomm/InCallTonePlayer.java
@@ -65,7 +65,7 @@
 
     // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
     // value for a tone is exact duration of the tone itself.
-    private static final int TIMEOUT_BUFFER_MS = 20;
+    private static final int TIMEOUT_BUFFER_MILLIS = 20;
 
     // The tone state.
     private static final int STATE_OFF = 0;
@@ -109,19 +109,19 @@
 
             final int toneType;  // Passed to ToneGenerator.startTone.
             final int toneVolume;  // Passed to the ToneGenerator constructor.
-            final int toneLengthMs;
+            final int toneLengthMillis;
 
             switch (mToneId) {
                 case TONE_BUSY:
                     // TODO: CDMA-specific tones
                     toneType = ToneGenerator.TONE_SUP_BUSY;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
-                    toneLengthMs = 4000;
+                    toneLengthMillis = 4000;
                     break;
                 case TONE_CALL_ENDED:
                     toneType = ToneGenerator.TONE_PROP_PROMPT;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
-                    toneLengthMs = 4000;
+                    toneLengthMillis = 4000;
                     break;
                 case TONE_OTA_CALL_ENDED:
                     // TODO: fill in
@@ -132,42 +132,42 @@
                 case TONE_CDMA_DROP:
                     toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
                     toneVolume = RELATIVE_VOLUME_LOPRI;
-                    toneLengthMs = 375;
+                    toneLengthMillis = 375;
                     break;
                 case TONE_CONGESTION:
                     toneType = ToneGenerator.TONE_SUP_CONGESTION;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
-                    toneLengthMs = 4000;
+                    toneLengthMillis = 4000;
                     break;
                 case TONE_INTERCEPT:
                     toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
                     toneVolume = RELATIVE_VOLUME_LOPRI;
-                    toneLengthMs = 500;
+                    toneLengthMillis = 500;
                     break;
                 case TONE_OUT_OF_SERVICE:
                     toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
                     toneVolume = RELATIVE_VOLUME_LOPRI;
-                    toneLengthMs = 375;
+                    toneLengthMillis = 375;
                     break;
                 case TONE_REDIAL:
                     toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
                     toneVolume = RELATIVE_VOLUME_LOPRI;
-                    toneLengthMs = 5000;
+                    toneLengthMillis = 5000;
                     break;
                 case TONE_REORDER:
                     toneType = ToneGenerator.TONE_CDMA_REORDER;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
-                    toneLengthMs = 5000;
+                    toneLengthMillis = 5000;
                     break;
                 case TONE_RING_BACK:
                     toneType = ToneGenerator.TONE_SUP_RINGTONE;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
-                    toneLengthMs = Integer.MAX_VALUE - TIMEOUT_BUFFER_MS;
+                    toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
                     break;
                 case TONE_UNOBTAINABLE_NUMBER:
                     toneType = ToneGenerator.TONE_SUP_ERROR;
                     toneVolume = RELATIVE_VOLUME_HIPRI;
-                    toneLengthMs = 4000;
+                    toneLengthMillis = 4000;
                     break;
                 case TONE_VOICE_PRIVACY:
                     // TODO: fill in.
@@ -202,8 +202,8 @@
                     toneGenerator.startTone(toneType);
                     try {
                         Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
-                                toneLengthMs + TIMEOUT_BUFFER_MS);
-                        wait(toneLengthMs + TIMEOUT_BUFFER_MS);
+                                toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
+                        wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
                     } catch (InterruptedException e) {
                         Log.w(this, "wait interrupted", e);
                     }
diff --git a/src/com/android/telecomm/MissedCallNotifier.java b/src/com/android/telecomm/MissedCallNotifier.java
index 8100472..9414030 100644
--- a/src/com/android/telecomm/MissedCallNotifier.java
+++ b/src/com/android/telecomm/MissedCallNotifier.java
@@ -106,7 +106,7 @@
         // Create the notification.
         Notification.Builder builder = new Notification.Builder(mContext);
         builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
-                .setWhen(call.getCreationTimeMs())
+                .setWhen(call.getCreationTimeMillis())
                 .setContentTitle(mContext.getText(titleResId))
                 .setContentText(expandedText)
                 .setContentIntent(createCallLogPendingIntent())
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index 66e1269..bd6b449 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -264,7 +264,7 @@
      * Schedules the next tick invocation.
      */
     private void scheduleNextTick() {
-         mHandler.postDelayed(mTicker, Timeouts.getTickMs());
+         mHandler.postDelayed(mTicker, Timeouts.getTickMillis());
     }
 
     /**
@@ -359,11 +359,11 @@
             return;
         }
 
-        final long newCallTimeoutMs = Timeouts.getNewOutgoingCallMs();
+        final long newCallTimeoutMillis = Timeouts.getNewOutgoingCallMillis();
         Iterator<Call> iterator = calls.iterator();
         while (iterator.hasNext()) {
             Call call = iterator.next();
-            if (call.getAgeMs() >= newCallTimeoutMs) {
+            if (call.getAgeMillis() >= newCallTimeoutMillis) {
                 Log.d(this, "Call %s timed out.", call);
                 mOutgoingCallsManager.abort(call);
                 calls.remove(call);
diff --git a/src/com/android/telecomm/TelephonyUtil.java b/src/com/android/telecomm/TelephonyUtil.java
index be501f0..bffe7eb 100644
--- a/src/com/android/telecomm/TelephonyUtil.java
+++ b/src/com/android/telecomm/TelephonyUtil.java
@@ -26,7 +26,7 @@
  * differently from 3rd party services in some situations (emergency calls, audio focus, etc...).
  */
 public final class TelephonyUtil {
-    private static final String TAG = ThreadUtil.class.getSimpleName();
+    private static final String TAG = TelephonyUtil.class.getSimpleName();
 
     private static final String TELEPHONY_PACKAGE_NAME =
             "com.android.phone";
diff --git a/src/com/android/telecomm/Timeouts.java b/src/com/android/telecomm/Timeouts.java
index 4096c02..4f8dc1c 100644
--- a/src/com/android/telecomm/Timeouts.java
+++ b/src/com/android/telecomm/Timeouts.java
@@ -49,14 +49,14 @@
      * @return The longest period in milliseconds each {@link CallServiceProvider} lookup cycle is
      *     allowed to span over.
      */
-    public static long getProviderLookupMs() {
+    public static long getProviderLookupMillis() {
         return get("provider_lookup_ms", 1000);
     }
 
     /**
      * @return How frequently, in milliseconds, to run {@link Switchboard}'s clean-up "tick" cycle.
      */
-    public static long getTickMs() {
+    public static long getTickMillis() {
         return get("tick_ms", 250);
     }
 
@@ -67,7 +67,7 @@
      * @return The longest period, in milliseconds, each new call is allowed to wait before being
      *     established.
      */
-    public static long getNewOutgoingCallMs() {
+    public static long getNewOutgoingCallMillis() {
         return get("new_outgoing_call_ms", 5000);
     }
 }
diff --git a/tests/src/com/android/telecomm/testcallservice/CallServiceNotifier.java b/tests/src/com/android/telecomm/testcallservice/CallServiceNotifier.java
index a186e58..e88c293 100644
--- a/tests/src/com/android/telecomm/testcallservice/CallServiceNotifier.java
+++ b/tests/src/com/android/telecomm/testcallservice/CallServiceNotifier.java
@@ -26,12 +26,10 @@
 import android.util.Log;
 
 /**
- * Class used to create, update and cancel the notification used to display and update call state for
- * {@link TestCallService}.
+ * Class used to create, update and cancel the notification used to display and update call state
+ * for {@link TestCallService}.
  */
 public class CallServiceNotifier {
-    private static final String TAG = CallServiceNotifier.class.getSimpleName();
-
     private static final CallServiceNotifier INSTANCE = new CallServiceNotifier();
 
     /**
@@ -56,7 +54,7 @@
      * Updates the notification in the notification pane.
      */
     public void updateNotification(Context context) {
-        Log.i("CallServiceNotifier", "adding the notification ------------");
+        log("adding the notification ------------");
         getNotificationManager(context).notify(CALL_NOTIFICATION_ID, getNotification(context));
     }
 
@@ -64,7 +62,7 @@
      * Cancels the notification.
      */
     public void cancelNotification(Context context) {
-        Log.i(TAG, "canceling notification");
+        log("canceling notification");
         getNotificationManager(context).cancel(CALL_NOTIFICATION_ID);
     }
 
@@ -110,7 +108,7 @@
      * Creates the intent to add an incoming call through Telecomm.
      */
     private PendingIntent createIncomingCallIntent(Context context) {
-        Log.i(TAG, "Creating incoming call pending intent.");
+        log("Creating incoming call pending intent.");
         // Build descriptor for TestCallService.
         CallServiceDescriptor.Builder descriptorBuilder = CallServiceDescriptor.newBuilder(context);
         descriptorBuilder.setCallService(TestCallService.class);
@@ -141,4 +139,8 @@
     private void addExitAction(Notification.Builder builder, Context context) {
         builder.addAction(0, "Exit", createExitIntent(context));
     }
+
+    private static void log(String msg) {
+        Log.w("testcallservice", "[CallServiceNotifier] " + msg);
+    }
 }
diff --git a/tests/src/com/android/telecomm/testcallservice/DummyCallServiceSelector.java b/tests/src/com/android/telecomm/testcallservice/DummyCallServiceSelector.java
index c267867..86319a4 100644
--- a/tests/src/com/android/telecomm/testcallservice/DummyCallServiceSelector.java
+++ b/tests/src/com/android/telecomm/testcallservice/DummyCallServiceSelector.java
@@ -16,33 +16,100 @@
 
 package com.android.telecomm.testcallservice;
 
+import android.net.Uri;
+import android.os.Bundle;
 import android.telecomm.CallInfo;
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallServiceSelector;
 import android.telecomm.CallServiceSelectorAdapter;
+import android.util.Log;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 
 import java.util.List;
 
-/**
- * Dummy call-service selector which returns the list of call services in the same order in which it
- * was given. Also returns false for every request on switchability.
- */
+/** Simple selector to exercise Telecomm code. */
 public class DummyCallServiceSelector extends CallServiceSelector {
+    private static DummyCallServiceSelector sInstance;
+    private static final String SCHEME_TEL = "tel";
+    private static final String TELEPHONY_PACKAGE_NAME =
+            "com.android.phone";
+    private static final String CUSTOM_HANDOFF_KEY = "custom_handoff_key";
+    private static final String CUSTOM_HANDOFF_VALUE = "custom_handoff_value";
+
+    public DummyCallServiceSelector() {
+        log("constructor");
+        Preconditions.checkState(sInstance == null);
+        sInstance = this;
+    }
+
+    static DummyCallServiceSelector getInstance() {
+        Preconditions.checkNotNull(sInstance);
+        return sInstance;
+    }
 
     @Override
     protected void select(CallInfo callInfo, List<CallServiceDescriptor> descriptors) {
+        log("select");
         List<CallServiceDescriptor> orderedList = Lists.newLinkedList();
 
-        // Make sure that the test call services are the only ones
+        boolean shouldHandoffToPstn = false;
+        if (callInfo.getCurrentCallServiceDescriptor() != null) {
+            // If the current call service is TestCallService then handoff to PSTN, otherwise
+            // handoff to TestCallService.
+            shouldHandoffToPstn = isTestCallService(callInfo.getCurrentCallServiceDescriptor());
+            String extraValue = callInfo.getExtras().getString(CUSTOM_HANDOFF_KEY);
+            log("handing off, toPstn: " + shouldHandoffToPstn + ", extraValue: " + extraValue);
+            Preconditions.checkState(CUSTOM_HANDOFF_VALUE.equals(extraValue));
+        }
+
         for (CallServiceDescriptor descriptor : descriptors) {
-            String packageName = descriptor.getServiceComponent().getPackageName();
-            if (getPackageName().equals(packageName)) {
+            if (isTestCallService(descriptor) && !shouldHandoffToPstn) {
+                orderedList.add(0, descriptor);
+            } else if (isPstnCallService(descriptor)) {
                 orderedList.add(descriptor);
+            } else {
+                log("skipping call service: " + descriptor.getServiceComponent());
             }
         }
 
         getAdapter().setSelectedCallServices(callInfo.getId(), orderedList);
     }
+
+    void sendHandoffInfo(Uri remoteHandle, Uri handoffHandle) {
+        log("sendHandoffInfo");
+        String callId = findMatchingCall(remoteHandle);
+        Preconditions.checkNotNull(callId);
+        Bundle extras = new Bundle();
+        extras.putString(CUSTOM_HANDOFF_KEY, CUSTOM_HANDOFF_VALUE);
+        getAdapter().setHandoffInfo(callId, handoffHandle, extras);
+    }
+
+    private String findMatchingCall(Uri remoteHandle) {
+        for (CallInfo callInfo : getCalls()) {
+            if (remoteHandle.equals(callInfo.getOriginalHandle())) {
+                return callInfo.getId();
+            }
+        }
+        return null;
+    }
+
+    private boolean isTestCallService(CallServiceDescriptor descriptor) {
+        if (descriptor == null) {
+            return false;
+        }
+        return getPackageName().equals(descriptor.getServiceComponent().getPackageName());
+    }
+
+    private boolean isPstnCallService(CallServiceDescriptor descriptor) {
+        if (descriptor == null) {
+            return false;
+        }
+        return TELEPHONY_PACKAGE_NAME.equals(descriptor.getServiceComponent().getPackageName());
+    }
+
+    private static void log(String msg) {
+        Log.w("testcallservice", "[DummyCallServiceSelector] " + msg);
+    }
 }
diff --git a/tests/src/com/android/telecomm/testcallservice/TestCallService.java b/tests/src/com/android/telecomm/testcallservice/TestCallService.java
index f6e2da7..275c208 100644
--- a/tests/src/com/android/telecomm/testcallservice/TestCallService.java
+++ b/tests/src/com/android/telecomm/testcallservice/TestCallService.java
@@ -20,6 +20,7 @@
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.telecomm.CallAudioState;
 import android.telecomm.CallInfo;
 import android.telecomm.CallService;
@@ -31,35 +32,27 @@
 import com.android.telecomm.tests.R;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
-import com.google.common.collect.Sets;
+import com.google.common.collect.Maps;
 
-import java.util.Set;
+import java.util.Map;
 
 /**
  * Service which provides fake calls to test the ICallService interface.
  * TODO(santoscordon): Rename all classes in the directory to Dummy* (e.g., DummyCallService).
  */
 public class TestCallService extends CallService {
-    private static final String TAG = TestCallService.class.getSimpleName();
+    private static final String SCHEME_TEL = "tel";
 
-    /**
-     * Set of call IDs for live (active, ringing, dialing) calls.
-     * TODO(santoscordon): Reference CallState javadoc when available for the different call states.
-     */
-    private Set<String> mLiveCallIds;
+    private final Map<String, CallInfo> mCalls = Maps.newHashMap();
+    private final Handler mHandler = new Handler();
 
-    /**
-     * Used to play an audio tone during a call.
-     */
+    /** Used to play an audio tone during a call. */
     private MediaPlayer mMediaPlayer;
 
     /** {@inheritDoc} */
     @Override
     public void onAdapterAttached(CallServiceAdapter callServiceAdapter) {
-        Log.i(TAG, "setCallServiceAdapter()");
-
-        mLiveCallIds = Sets.newHashSet();
-
+        log("onAdapterAttached");
         mMediaPlayer = createMediaPlayer();
     }
 
@@ -71,75 +64,79 @@
      */
     @Override
     public void isCompatibleWith(CallInfo callInfo) {
-        Log.i(TAG, "isCompatibleWith(" + callInfo + ")");
+        log("isCompatibleWith, callInfo: " + callInfo);
         Preconditions.checkNotNull(callInfo.getHandle());
 
         // Is compatible if the handle doesn't start with 7.
         boolean isCompatible = !callInfo.getHandle().getSchemeSpecificPart().startsWith("7");
 
-        // Tell CallsManager whether this call service can place the call (is compatible).
-        // 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.
+        // Tell CallsManager whether this call service can place the call.
         getAdapter().setIsCompatibleWith(callInfo.getId(), isCompatible);
     }
 
     /**
-     * Starts a call by calling into the adapter. For testing purposes this methods acts as if a
-     * call was successfully connected every time.
+     * Starts a call by calling into the adapter.
      *
      * {@inheritDoc}
      */
     @Override
-    public void call(CallInfo callInfo) {
+    public void call(final CallInfo callInfo) {
         String number = callInfo.getHandle().getSchemeSpecificPart();
-        Log.i(TAG, "call(" + number + ")");
+        log("call, number: " + number);
 
         // Crash on 555-DEAD to test call service crashing.
         if ("5550340".equals(number)) {
             throw new RuntimeException("Goodbye, cruel world.");
         }
 
-        createCall(callInfo.getId());
+        mCalls.put(callInfo.getId(), callInfo);
         getAdapter().handleSuccessfulOutgoingCall(callInfo.getId());
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                activateCall(callInfo.getId());
+            }
+        }, 4000);
     }
 
     /** {@inheritDoc} */
     @Override
     public void abort(String callId) {
-        Log.i(TAG, "abort(" + callId + ")");
+        log("abort, callId: " + callId);
         destroyCall(callId);
     }
 
     /** {@inheritDoc} */
     @Override
     public void setIncomingCallId(String callId, Bundle extras) {
-        Log.i(TAG, "setIncomingCallId(" + callId + ", " + extras + ")");
+        log("setIncomingCallId, callId: " + callId + " extras: " + extras);
 
         // Use dummy number for testing incoming calls.
-        Uri handle = Uri.fromParts("tel", "5551234", null);
+        Uri handle = Uri.fromParts(SCHEME_TEL, "5551234", null);
 
         CallInfo callInfo = new CallInfo(callId, CallState.RINGING, handle);
+        mCalls.put(callInfo.getId(), callInfo);
         getAdapter().notifyIncomingCall(callInfo);
     }
 
     /** {@inheritDoc} */
     @Override
     public void answer(String callId) {
-        getAdapter().setActive(callId);
-        createCall(callId);
+        log("answer, callId: " + callId);
+        activateCall(callId);
     }
 
     /** {@inheritDoc} */
     @Override
     public void reject(String callId) {
+        log("reject, callId: " + callId);
         getAdapter().setDisconnected(callId, DisconnectCause.INCOMING_REJECTED, null);
     }
 
     /** {@inheritDoc} */
     @Override
     public void disconnect(String callId) {
-        Log.i(TAG, "disconnect(" + callId + ")");
+        log("disconnect, callId: " + callId);
 
         destroyCall(callId);
         getAdapter().setDisconnected(callId, DisconnectCause.LOCAL, null);
@@ -148,55 +145,45 @@
     /** {@inheritDoc} */
     @Override
     public void hold(String callId) {
-        Log.i(TAG, "hold(" + callId + ")");
+        log("hold, callId: " + callId);
         getAdapter().setOnHold(callId);
     }
 
     /** {@inheritDoc} */
     @Override
     public void unhold(String callId) {
-        Log.i(TAG, "unhold(" + callId + ")");
+        log("unhold, callId: " + callId);
         getAdapter().setActive(callId);
     }
 
     /** {@inheritDoc} */
     @Override
     public void playDtmfTone(String callId, char digit) {
-        Log.i(TAG, "playDtmfTone(" + callId + "," + digit + ")");
-        // TODO(ihab): Implement
+        log("playDtmfTone, callId: " + callId + " digit: " + digit);
     }
 
     /** {@inheritDoc} */
     @Override
     public void stopDtmfTone(String callId) {
-        Log.i(TAG, "stopDtmfTone(" + callId + ")");
-        // TODO(ihab): Implement
+        log("stopDtmfTone, callId: " + callId);
     }
 
     /** {@inheritDoc} */
     @Override
     public void onAudioStateChanged(String callId, CallAudioState audioState) {
+        log("onAudioStateChanged, callId: " + callId + " audioState: " + audioState);
     }
 
     /** {@inheritDoc} */
     @Override
     public boolean onUnbind(Intent intent) {
+        log("onUnbind");
         mMediaPlayer = null;
-
         return super.onUnbind(intent);
     }
 
-    /**
-     * Adds the specified call ID to the set of live call IDs and starts playing audio on the
-     * voice-call stream.
-     *
-     * @param callId The identifier of the call to create.
-     */
-    private void createCall(String callId) {
-        Preconditions.checkState(!Strings.isNullOrEmpty(callId));
-        mLiveCallIds.add(callId);
-
-        // Starts audio if not already started.
+    private void activateCall(String callId) {
+        getAdapter().setActive(callId);
         if (!mMediaPlayer.isPlaying()) {
             mMediaPlayer.start();
         }
@@ -210,10 +197,10 @@
      */
     private void destroyCall(String callId) {
         Preconditions.checkState(!Strings.isNullOrEmpty(callId));
-        mLiveCallIds.remove(callId);
+        mCalls.remove(callId);
 
         // Stops audio if there are no more calls.
-        if (mLiveCallIds.isEmpty() && mMediaPlayer.isPlaying()) {
+        if (mCalls.isEmpty() && mMediaPlayer.isPlaying()) {
             mMediaPlayer.stop();
             mMediaPlayer.release();
             mMediaPlayer = createMediaPlayer();
@@ -227,4 +214,7 @@
         return mediaPlayer;
     }
 
+    private static void log(String msg) {
+        Log.w("testcallservice", "[TestCallService] " + msg);
+    }
 }
diff --git a/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java b/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java
index 781f629..3a6646c 100644
--- a/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java
+++ b/tests/src/com/android/telecomm/testcallservice/TestCallServiceProvider.java
@@ -28,12 +28,10 @@
  * TODO(santoscordon): Build more dummy providers for more CallServiceDescriptor.FLAG_* types.
  */
 public class TestCallServiceProvider extends CallServiceProvider {
-    private static final String TAG = TestCallServiceProvider.class.getSimpleName();
-
     /** {@inheritDoc} */
     @Override
     public void lookupCallServices(CallServiceLookupResponse response) {
-        Log.i(TAG, "lookupCallServices()");
+        log("lookupCallServices");
 
         CallServiceDescriptor.Builder builder = CallServiceDescriptor.newBuilder(this);
         builder.setCallService(TestCallService.class);
@@ -41,4 +39,8 @@
 
         response.setCallServiceDescriptors(Lists.newArrayList(builder.build()));
     }
+
+    private static void log(String msg) {
+        Log.w("testcallservice", "[TestCallServiceProvider] " + msg);
+    }
 }