Fix incoming callback and binding issues.

- Created queues to handle cases where callbacks were getting dropped while
the service was connecting.
- Fixed DeadObjectExceptions when the in-call ui was being re-installed.
- Change to a single connection object to fix connection leak where every
re-bind caused connection to double.
- Split caller information from Call into CallIdentification object.

Bug: 10468815
Bug: 10457750

Change-Id: Icaef130db2f6893df3175eed07d0d483a3c4c455
diff --git a/common/src/com/android/services/telephony/common/Call.java b/common/src/com/android/services/telephony/common/Call.java
index 4c68913..f397f62 100644
--- a/common/src/com/android/services/telephony/common/Call.java
+++ b/common/src/com/android/services/telephony/common/Call.java
@@ -16,32 +16,25 @@
 
 package com.android.services.telephony.common;
 
-import com.google.android.collect.Lists;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.telephony.PhoneConstants;
 import com.google.android.collect.Sets;
-import com.google.common.collect.ImmutableList;
+import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.primitives.Ints;
 
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.os.SystemClock;
-import android.text.format.DateUtils;
-
-import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.SortedSet;
-
-import com.android.internal.telephony.PhoneConstants;
+import java.util.TreeSet;
 
 /**
  * Class object used across CallHandlerService APIs.
  * Describes a single call and its state.
  */
-final public class Call implements Parcelable {
+public final class Call implements Parcelable {
 
     public static final int INVALID_CALL_ID = -1;
     public static final int MAX_CONFERENCED_CALLS = 5;
@@ -159,23 +152,13 @@
     public static int PRESENTATION_PAYPHONE = PhoneConstants.PRESENTATION_PAYPHONE;
 
     // Unique identifier for the call
-    private int mCallId = INVALID_CALL_ID;
+    private int mCallId;
 
-    // The phone number on the other end of the connection
-    private String mNumber = "";
+    private CallIdentification mIdentification;
 
     // The current state of the call
     private int mState = State.INVALID;
 
-    // Number presentation received from the carrier
-    private int mNumberPresentation = PRESENTATION_ALLOWED;
-
-    // Name presentation mode received from the carrier
-    private int mCnapNamePresentation = PRESENTATION_ALLOWED;
-
-    // Name associated with the other end of the connection; from the carrier.
-    private String mCnapName = "";
-
     // Reason for disconnect. Valid when the call state is DISCONNECTED.
     private DisconnectCause mDisconnectCause = DisconnectCause.UNKNOWN;
 
@@ -196,18 +179,35 @@
 
     public Call(int callId) {
         mCallId = callId;
+        mIdentification = new CallIdentification(mCallId);
+    }
+
+    public Call(Call call) {
+        mCallId = call.mCallId;
+        mIdentification = new CallIdentification(call.mIdentification);
+        mState = call.mState;
+        mDisconnectCause = call.mDisconnectCause;
+        mCapabilities = call.mCapabilities;
+        mConnectTime = call.mConnectTime;
+        mChildCallIds = new TreeSet<Integer>(call.mChildCallIds);
+        mGatewayNumber = call.mGatewayNumber;
+        mGatewayPackage = call.mGatewayPackage;
     }
 
     public int getCallId() {
         return mCallId;
     }
 
+    public CallIdentification getIdentification() {
+        return mIdentification;
+    }
+
     public String getNumber() {
-        return mNumber;
+        return mIdentification.getNumber();
     }
 
     public void setNumber(String number) {
-        mNumber = number;
+        mIdentification.setNumber(number);
     }
 
     public int getState() {
@@ -219,27 +219,27 @@
     }
 
     public int getNumberPresentation() {
-        return mNumberPresentation;
+        return mIdentification.getNumberPresentation();
     }
 
     public void setNumberPresentation(int presentation) {
-        mNumberPresentation = presentation;
+        mIdentification.setNumberPresentation(presentation);
     }
 
     public int getCnapNamePresentation() {
-        return mCnapNamePresentation;
+        return mIdentification.getCnapNamePresentation();
     }
 
     public void setCnapNamePresentation(int presentation) {
-        mCnapNamePresentation = presentation;
+        mIdentification.setCnapNamePresentation(presentation);
     }
 
     public String getCnapName() {
-        return mCnapName;
+        return mIdentification.getCnapName();
     }
 
     public void setCnapName(String cnapName) {
-        mCnapName = cnapName;
+        mIdentification.setCnapName(cnapName);
     }
 
     public DisconnectCause getDisconnectCause() {
@@ -321,17 +321,14 @@
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(mCallId);
-        dest.writeString(mNumber);
         dest.writeInt(mState);
-        dest.writeInt(mNumberPresentation);
-        dest.writeInt(mCnapNamePresentation);
-        dest.writeString(mCnapName);
         dest.writeString(getDisconnectCause().toString());
         dest.writeInt(getCapabilities());
         dest.writeLong(getConnectTime());
         dest.writeIntArray(Ints.toArray(mChildCallIds));
         dest.writeString(getGatewayNumber());
         dest.writeString(getGatewayPackage());
+        dest.writeParcelable(mIdentification, 0);
     }
 
     /**
@@ -339,17 +336,14 @@
      */
     private Call(Parcel in) {
         mCallId = in.readInt();
-        mNumber = in.readString();
         mState = in.readInt();
-        mNumberPresentation = in.readInt();
-        mCnapNamePresentation = in.readInt();
-        mCnapName = in.readString();
         mDisconnectCause = DisconnectCause.valueOf(in.readString());
         mCapabilities = in.readInt();
         mConnectTime = in.readLong();
         mChildCallIds.addAll(Ints.asList(in.createIntArray()));
         mGatewayNumber = in.readString();
         mGatewayPackage = in.readString();
+        mIdentification = in.readParcelable(CallIdentification.class.getClassLoader());
     }
 
     @Override
@@ -376,38 +370,16 @@
 
     @Override
     public String toString() {
-        return toString(true);
-    }
-
-    public String toString(boolean safe) {
-        StringBuffer buffer = new StringBuffer();
-        buffer.append("callId: ");
-        buffer.append(mCallId);
-        if (!safe) {
-            buffer.append(", number: ");
-            buffer.append(mNumber);
-            buffer.append(", name: ");
-            buffer.append(mCnapName);
-        }
-        buffer.append(", state: ");
-        buffer.append(STATE_MAP.get(mState));
-        buffer.append(", disconnect_cause: ");
-        buffer.append(getDisconnectCause().toString());
-        buffer.append(", capabilities: ");
-        buffer.append(Integer.toHexString(getCapabilities()));
-
-        final long duration = System.currentTimeMillis() - getConnectTime();
-        buffer.append(", elapsedTime: ");
-        buffer.append(DateUtils.formatElapsedTime(duration / 1000));
-
-        buffer.append(", childCalls: ");
-        buffer.append(mChildCallIds.toString());
-        buffer.append(", gateway: ");
-        if (!safe) {
-            buffer.append(mGatewayNumber);
-        }
-        buffer.append(" [").append(mGatewayPackage).append("]");
-
-        return buffer.toString();
+        return Objects.toStringHelper(this)
+                .add("mCallId", mCallId)
+                .add("mState", mState)
+                .add("mDisconnectCause", mDisconnectCause)
+                .add("mCapabilities", mCapabilities)
+                .add("mConnectTime", mConnectTime)
+                .add("mChildCallIds", mChildCallIds)
+                .add("mGatewayNumber", MoreStrings.toSafeString(mGatewayNumber))
+                .add("mGatewayPackage", mGatewayPackage)
+                .add("mIdentification", mIdentification)
+                .toString();
     }
 }
diff --git a/common/src/com/android/services/telephony/common/CallIdentification.java b/common/src/com/android/services/telephony/common/CallIdentification.java
new file mode 100644
index 0000000..4a9884c
--- /dev/null
+++ b/common/src/com/android/services/telephony/common/CallIdentification.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2013 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.services.telephony.common;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.telephony.PhoneConstants;
+import com.google.common.base.Objects;
+
+/**
+ * Class object used across CallHandlerService APIs. Describes a single call and its state.
+ */
+public final class CallIdentification implements Parcelable {
+
+    // Unique identifier for the call
+    private int mCallId;
+
+    // The phone number on the other end of the connection
+    private String mNumber = "";
+
+    // Number presentation received from the carrier
+    private int mNumberPresentation = Call.PRESENTATION_ALLOWED;
+
+    // Name presentation mode received from the carrier
+    private int mCnapNamePresentation = Call.PRESENTATION_ALLOWED;
+
+    // Name associated with the other end of the connection; from the carrier.
+    private String mCnapName = "";
+
+    public CallIdentification(int callId) {
+        mCallId = callId;
+    }
+
+    public CallIdentification(CallIdentification identification) {
+        mCallId = identification.mCallId;
+        mNumber = identification.mNumber;
+        mNumberPresentation = identification.mNumberPresentation;
+        mCnapNamePresentation = identification.mCnapNamePresentation;
+        mCnapName = identification.mCnapName;
+    }
+
+    public int getCallId() {
+        return mCallId;
+    }
+
+    public String getNumber() {
+        return mNumber;
+    }
+
+    public void setNumber(String number) {
+        mNumber = number;
+    }
+
+    public int getNumberPresentation() {
+        return mNumberPresentation;
+    }
+
+    public void setNumberPresentation(int presentation) {
+        mNumberPresentation = presentation;
+    }
+
+    public int getCnapNamePresentation() {
+        return mCnapNamePresentation;
+    }
+
+    public void setCnapNamePresentation(int presentation) {
+        mCnapNamePresentation = presentation;
+    }
+
+    public String getCnapName() {
+        return mCnapName;
+    }
+
+    public void setCnapName(String cnapName) {
+        mCnapName = cnapName;
+    }
+
+    /**
+     * Parcelable implementation
+     */
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mCallId);
+        dest.writeString(mNumber);
+        dest.writeInt(mNumberPresentation);
+        dest.writeInt(mCnapNamePresentation);
+        dest.writeString(mCnapName);
+    }
+
+    /**
+     * Constructor for Parcelable implementation.
+     */
+    private CallIdentification(Parcel in) {
+        mCallId = in.readInt();
+        mNumber = in.readString();
+        mNumberPresentation = in.readInt();
+        mCnapNamePresentation = in.readInt();
+        mCnapName = in.readString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Creates Call objects for Parcelable implementation.
+     */
+    public static final Creator<CallIdentification> CREATOR = new Creator<CallIdentification>() {
+
+        @Override
+        public CallIdentification createFromParcel(Parcel in) {
+            return new CallIdentification(in);
+        }
+
+        @Override
+        public CallIdentification[] newArray(int size) {
+            return new CallIdentification[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("mCallId", mCallId)
+                .add("mNumber", MoreStrings.toSafeString(mNumber))
+                .add("mNumberPresentation", mNumberPresentation)
+                .add("mCnapName", MoreStrings.toSafeString(mCnapName))
+                .add("mCnapNamePresentation", mCnapNamePresentation)
+                .toString();
+    }
+}
diff --git a/common/src/com/android/services/telephony/common/ICallHandlerService.aidl b/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
index df2e2de..cf2e0b2 100644
--- a/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
+++ b/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
@@ -44,7 +44,7 @@
     /**
      * Called when the state of a call changes.
      */
-    void onUpdate(in List<Call> call, boolean fullUpdate);
+    void onUpdate(in List<Call> call);
 
     /**
      * Called when a call disconnects.
diff --git a/common/src/com/android/services/telephony/common/MoreStrings.java b/common/src/com/android/services/telephony/common/MoreStrings.java
new file mode 100644
index 0000000..5e6e4b4
--- /dev/null
+++ b/common/src/com/android/services/telephony/common/MoreStrings.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 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.services.telephony.common;
+
+/**
+ * Static utility methods for Strings.
+ */
+public class MoreStrings {
+
+    public static String toSafeString(String value) {
+        if (value == null) {
+            return null;
+        }
+
+        // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+        // sanitized phone numbers.
+        final StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < value.length(); i++) {
+            final char c = value.charAt(i);
+            if (c == '-' || c == '@' || c == '.') {
+                builder.append(c);
+            } else {
+                builder.append('x');
+            }
+        }
+        return builder.toString();
+    }
+
+}
diff --git a/src/com/android/phone/BluetoothManager.java b/src/com/android/phone/BluetoothManager.java
index f5106c8..dd38632 100644
--- a/src/com/android/phone/BluetoothManager.java
+++ b/src/com/android/phone/BluetoothManager.java
@@ -408,14 +408,14 @@
     }
 
     @Override
-    public void onIncoming(Call call, ArrayList<String> messages) {
+    public void onIncoming(Call call) {
         // An incoming call can affect bluetooth indicator, so we update it whenever there is
         // a change to any of the calls.
         updateBluetoothIndication();
     }
 
     @Override
-    public void onUpdate(List<Call> calls, boolean fullUpdate) {
+    public void onUpdate(List<Call> calls) {
         updateBluetoothIndication();
     }
 
diff --git a/src/com/android/phone/CallHandlerServiceProxy.java b/src/com/android/phone/CallHandlerServiceProxy.java
index 12e039e..75bb26c 100644
--- a/src/com/android/phone/CallHandlerServiceProxy.java
+++ b/src/com/android/phone/CallHandlerServiceProxy.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.os.AsyncResult;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
@@ -32,31 +31,51 @@
 import com.android.services.telephony.common.AudioMode;
 import com.android.services.telephony.common.Call;
 import com.android.services.telephony.common.ICallHandlerService;
-import com.android.services.telephony.common.ICallCommandService;
+import com.google.common.collect.Lists;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
  * This class is responsible for passing through call state changes to the CallHandlerService.
  */
-public class CallHandlerServiceProxy extends Handler implements CallModeler.Listener,
-        AudioModeListener {
+public class CallHandlerServiceProxy extends Handler
+        implements CallModeler.Listener, AudioModeListener {
 
     private static final String TAG = CallHandlerServiceProxy.class.getSimpleName();
-    private static final boolean DBG =
-            (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+    private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt(
+            "ro.debuggable", 0) == 1);
 
+    public static final int RETRY_DELAY_MILLIS = 2000;
+    private static final int BIND_RETRY_MSG = 1;
+    private static final int MAX_RETRY_COUNT = 5;
 
     private AudioRouter mAudioRouter;
     private CallCommandService mCallCommandService;
     private CallModeler mCallModeler;
     private Context mContext;
-    private ICallHandlerService mCallHandlerService;
-    private ServiceConnection mConnection;
+    private ICallHandlerService mCallHandlerServiceGuarded;  // Guarded by mServiceAndQueueLock
+    private List<Call> mIncomingCallQueueGuarded;            // Guarded by mServiceAndQueueLock
+    private List<List<Call>> mUpdateCallQueueGuarded;        // Guarded by mServiceAndQueueLock
+    private List<Call> mDisconnectCallQueueGuarded;        // Guarded by mServiceAndQueueLock
+    private final Object mServiceAndQueueLock = new Object();
+    private int mBindRetryCount = 0;
+
+    @Override
+    public void handleMessage(Message msg) {
+        super.handleMessage(msg);
+
+        switch (msg.what) {
+            case BIND_RETRY_MSG:
+                setupServiceConnection();
+                break;
+        }
+    }
 
     public CallHandlerServiceProxy(Context context, CallModeler callModeler,
             CallCommandService callCommandService, AudioRouter audioRouter) {
+        if (DBG) {
+            Log.d(TAG, "init CallHandlerServiceProxy");
+        }
         mContext = context;
         mCallCommandService = callCommandService;
         mCallModeler = callModeler;
@@ -64,155 +83,263 @@
 
         mAudioRouter.addAudioModeListener(this);
         mCallModeler.addListener(this);
+
+        setupServiceConnection();
     }
 
     @Override
     public void onDisconnect(Call call) {
-        if (mCallHandlerService != null) {
-            try {
-                if (DBG) Log.d(TAG, "onDisconnect: " + call);
-                mCallHandlerService.onDisconnect(call);
-                maybeUnbind();
-            } catch (RemoteException e) {
-                Log.e(TAG, "Remote exception handling onDisconnect ", e);
+        try {
+            synchronized (mServiceAndQueueLock) {
+                if (mCallHandlerServiceGuarded == null) {
+                    if (DBG) {
+                        Log.d(TAG, "CallHandlerService not connected.  Enqueue disconnect");
+                    }
+                    enqueueDisconnect(call);
+                    return;
+                }
             }
+            if (DBG) {
+                Log.d(TAG, "onDisconnect: " + call);
+            }
+            mCallHandlerServiceGuarded.onDisconnect(call);
+        } catch (Exception e) {
+            Log.e(TAG, "Remote exception handling onDisconnect ", e);
         }
     }
 
     @Override
-    public void onIncoming(Call call, ArrayList<String> textResponses) {
-        if (maybeBindToService() && mCallHandlerService != null) {
-            try {
-                if (DBG) Log.d(TAG, "onIncoming: " + call);
-                mCallHandlerService.onIncoming(call, textResponses);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Remote exception handling onUpdate", e);
+    public void onIncoming(Call call) {
+        try {
+            synchronized (mServiceAndQueueLock) {
+                if (mCallHandlerServiceGuarded == null) {
+                    if (DBG) {
+                        Log.d(TAG, "CallHandlerService not connected.  Enqueue incoming.");
+                    }
+                    enqueueIncoming(call);
+                    return;
+                }
             }
+            if (DBG) {
+                Log.d(TAG, "onIncoming: " + call);
+            }
+            // TODO(klp): check RespondViaSmsManager.allowRespondViaSmsForCall()
+            // must refactor call method to accept proper call object.
+            mCallHandlerServiceGuarded.onIncoming(call,
+                    RejectWithTextMessageManager.loadCannedResponses());
+        } catch (Exception e) {
+            Log.e(TAG, "Remote exception handling onUpdate", e);
         }
     }
 
     @Override
-    public void onUpdate(List<Call> calls, boolean fullUpdate) {
-        if (maybeBindToService() && mCallHandlerService != null) {
-            try {
-                if (DBG) Log.d(TAG, "onUpdate: " + calls.toString());
-                mCallHandlerService.onUpdate(calls, fullUpdate);
-                maybeUnbind();
-            } catch (RemoteException e) {
-                Log.e(TAG, "Remote exception handling onUpdate", e);
+    public void onUpdate(List<Call> calls) {
+        try {
+            synchronized (mServiceAndQueueLock) {
+                if (mCallHandlerServiceGuarded == null) {
+                    if (DBG) {
+                        Log.d(TAG, "CallHandlerService not connected.  Enqueue update.");
+                    }
+                    enqueueUpdate(calls);
+                    return;
+                }
             }
+
+            if (DBG) {
+                Log.d(TAG, "onUpdate: " + calls.toString());
+            }
+            mCallHandlerServiceGuarded.onUpdate(calls);
+        } catch (Exception e) {
+            Log.e(TAG, "Remote exception handling onUpdate", e);
         }
     }
 
     @Override
     public void onAudioModeChange(int previousMode, int newMode) {
-        // Just do a simple log for now.
-        Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) +
-                " from " + AudioMode.toString(previousMode));
-
-        if (mCallHandlerService != null) {
-            try {
-                if (DBG) Log.d(TAG, "onSupportAudioModeChange");
-
-                mCallHandlerService.onAudioModeChange(newMode);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+        try {
+            synchronized (mServiceAndQueueLock) {
+                // TODO(klp): does this need to be enqueued?
+                if (mCallHandlerServiceGuarded == null) {
+                    if (DBG) {
+                        Log.d(TAG, "CallHandlerService not conneccted. Skipping "
+                                + "onAudioModeChange().");
+                    }
+                    return;
+                }
             }
+
+            // Just do a simple log for now.
+            Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) +
+                    " from " + AudioMode.toString(previousMode));
+
+            if (DBG) {
+                Log.d(TAG, "onSupportAudioModeChange");
+            }
+
+            mCallHandlerServiceGuarded.onAudioModeChange(newMode);
+        } catch (Exception e) {
+            Log.e(TAG, "Remote exception handling onAudioModeChange", e);
         }
     }
 
     @Override
     public void onSupportedAudioModeChange(int modeMask) {
-        if (mCallHandlerService != null) {
-            try {
-                if (DBG) Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask));
+        try {
+            synchronized (mServiceAndQueueLock) {
+                // TODO(klp): does this need to be enqueued?
+                if (mCallHandlerServiceGuarded == null) {
+                    if (DBG) {
+                        Log.d(TAG, "CallHandlerService not conneccted. Skipping"
+                                + "onSupportedAudioModeChange().");
+                    }
+                    return;
+                }
+            }
 
-                mCallHandlerService.onSupportedAudioModeChange(modeMask);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+            if (DBG) {
+                Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask));
+            }
+
+            mCallHandlerServiceGuarded.onSupportedAudioModeChange(modeMask);
+        } catch (Exception e) {
+            Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+        }
+
+    }
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override public void onServiceConnected (ComponentName className, IBinder service){
+            if (DBG) {
+                Log.d(TAG, "Service Connected");
+            }
+            onCallHandlerServiceConnected(ICallHandlerService.Stub.asInterface(service));
+        }
+
+        @Override public void onServiceDisconnected (ComponentName className){
+            Log.i(TAG, "Disconnected from UI service.");
+            synchronized (mServiceAndQueueLock) {
+                mCallHandlerServiceGuarded = null;
+
+                // Technically, unbindService is un-necessary since the framework will schedule and
+                // restart the crashed service.  But there is a exponential backoff for the restart.
+                // Unbind explicitly and setup again to avoid the backoff since it's important to
+                // always have an in call ui.
+                mContext.unbindService(mConnection);
+                setupServiceConnection();
             }
         }
     }
 
+    ;
+
     /**
      * Sets up the connection with ICallHandlerService
      */
     private void setupServiceConnection() {
-        mConnection = new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName className, IBinder service) {
+        synchronized (mServiceAndQueueLock) {
+            if (mCallHandlerServiceGuarded == null) {
+
+
+                final Intent serviceIntent = new Intent(ICallHandlerService.class.getName());
+                final ComponentName component = new ComponentName(mContext.getResources().getString(
+                        R.string.incall_ui_default_package), mContext.getResources().getString(
+                        R.string.incall_ui_default_class));
+                serviceIntent.setComponent(component);
+
                 if (DBG) {
-                    Log.d(TAG, "Service Connected");
+                    Log.d(TAG, "binding to service " + serviceIntent);
                 }
-                onCallHandlerServiceConnected(ICallHandlerService.Stub.asInterface(service));
-            }
-
-            @Override
-            public void onServiceDisconnected(ComponentName className) {
-                Log.i(TAG, "Disconnected from UI service.");
-                mCallHandlerService = null;
-
-                // clean up our current binding.
-                mContext.unbindService(mConnection);
-                mConnection = null;
-
-                // potentially attempt to rebind if there are still active calls.
-                maybeBindToService();
-            }
-        };
-
-        final Intent serviceIntent = new Intent(ICallHandlerService.class.getName());
-        final ComponentName component = new ComponentName(
-                mContext.getResources().getString(R.string.incall_ui_default_package),
-                mContext.getResources().getString(R.string.incall_ui_default_class));
-        serviceIntent.setComponent(component);
-        if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) {
-            Log.e(TAG, "Cound not bind to ICallHandlerService");
-        }
-    }
-
-    /**
-     * Checks To see if there are any calls left.  If not, unbind the callhandler service.
-     */
-    private void maybeUnbind() {
-        if (!mCallModeler.hasLiveCall()) {
-            if (mConnection != null) {
-                mContext.unbindService(mConnection);
-                mConnection = null;
+                if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) {
+                    // This happens when the in-call package is in the middle of being installed.
+                    // Delay the retry.
+                    mBindRetryCount++;
+                    if (mBindRetryCount < MAX_RETRY_COUNT) {
+                        Log.e(TAG, "bindService failed on " + serviceIntent + ".  Retrying in " +
+                                RETRY_DELAY_MILLIS + " ms.");
+                        sendMessageDelayed(Message.obtain(this, BIND_RETRY_MSG),
+                                RETRY_DELAY_MILLIS);
+                    } else {
+                        Log.wtf(TAG, "Tried to bind to in-call UI " + MAX_RETRY_COUNT + " times."
+                                + " Giving up.");
+                    }
+                }
             }
         }
     }
 
     /**
-     * Checks to see if there are any active calls.  If so, binds the call handler service.
-     * @return true if already bound. False otherwise.
-     */
-    private boolean maybeBindToService() {
-        if (mCallModeler.hasLiveCall()) {
-            // mConnection is set to non-null once an attempt is made to connect.
-            // We do not check against mCallHandlerService here because we could potentially
-            // create multiple bindings to the UI.
-            if (mConnection != null) {
-                return true;
-            }
-            setupServiceConnection();
-        }
-        return false;
-    }
-
-    /**
      * Called when the in-call UI service is connected.  Send command interface to in-call.
      */
     private void onCallHandlerServiceConnected(ICallHandlerService callHandlerService) {
-        mCallHandlerService = callHandlerService;
+        synchronized (mServiceAndQueueLock) {
+            mCallHandlerServiceGuarded = callHandlerService;
+
+            // TODO(klp): combine queues into a single ordered queue.
+            processIncomingCallQueue();
+            processUpdateCallQueue();
+            processDisconnectQueue();
+        }
 
         try {
-            mCallHandlerService.setCallCommandService(mCallCommandService);
-
-            // start with a full update
-            onUpdate(mCallModeler.getFullList(), true);
+            mCallHandlerServiceGuarded.setCallCommandService(mCallCommandService);
         } catch (RemoteException e) {
             Log.e(TAG, "Remote exception calling CallHandlerService::setCallCommandService", e);
         }
     }
+
+
+    private void enqueueDisconnect(Call call) {
+        if (mDisconnectCallQueueGuarded == null) {
+            mDisconnectCallQueueGuarded = Lists.newArrayList();
+        }
+        mDisconnectCallQueueGuarded.add(new Call(call));
+    }
+
+    private void enqueueIncoming(Call call) {
+        if (mIncomingCallQueueGuarded == null) {
+            mIncomingCallQueueGuarded = Lists.newArrayList();
+        }
+        mIncomingCallQueueGuarded.add(new Call(call));
+    }
+
+    private void enqueueUpdate(List<Call> calls) {
+        if (mUpdateCallQueueGuarded == null) {
+            mUpdateCallQueueGuarded = Lists.newArrayList();
+        }
+        final List<Call> copy = Lists.newArrayList();
+        for (Call call : calls) {
+            copy.add(new Call(call));
+        }
+        mUpdateCallQueueGuarded.add(copy);
+    }
+
+    private void processDisconnectQueue() {
+        if (mDisconnectCallQueueGuarded != null) {
+            for (Call call : mDisconnectCallQueueGuarded) {
+                onDisconnect(call);
+            }
+            mDisconnectCallQueueGuarded.clear();
+            mDisconnectCallQueueGuarded = null;
+        }
+    }
+
+    private void processIncomingCallQueue() {
+        if (mIncomingCallQueueGuarded != null) {
+            for (Call call : mIncomingCallQueueGuarded) {
+                onIncoming(call);
+            }
+            mIncomingCallQueueGuarded.clear();
+            mIncomingCallQueueGuarded = null;
+        }
+    }
+
+    private void processUpdateCallQueue() {
+        if (mUpdateCallQueueGuarded != null) {
+            for (List<Call> calls : mUpdateCallQueueGuarded) {
+                onUpdate(calls);
+            }
+            mUpdateCallQueueGuarded.clear();
+            mUpdateCallQueueGuarded = null;
+        }
+    }
 }
diff --git a/src/com/android/phone/CallModeler.java b/src/com/android/phone/CallModeler.java
index 94584a2..6b7a137 100644
--- a/src/com/android/phone/CallModeler.java
+++ b/src/com/android/phone/CallModeler.java
@@ -203,8 +203,7 @@
 
         for (int i = 0; i < mListeners.size(); ++i) {
             if (call != null) {
-              mListeners.get(i).onIncoming(call,
-                      mRejectWithTextMessageManager.loadCannedResponses());
+              mListeners.get(i).onIncoming(call);
             }
         }
     }
@@ -246,7 +245,7 @@
         doUpdate(false, updatedCalls);
 
         for (int i = 0; i < mListeners.size(); ++i) {
-            mListeners.get(i).onUpdate(updatedCalls, false);
+            mListeners.get(i).onUpdate(updatedCalls);
         }
     }
 
@@ -680,8 +679,8 @@
      */
     public interface Listener {
         void onDisconnect(Call call);
-        void onIncoming(Call call, ArrayList<String> textReponses);
-        void onUpdate(List<Call> calls, boolean fullUpdate);
+        void onIncoming(Call call);
+        void onUpdate(List<Call> calls);
     }
 
     /**
diff --git a/src/com/android/phone/DTMFTonePlayer.java b/src/com/android/phone/DTMFTonePlayer.java
index 87c071a..d3ed53e 100644
--- a/src/com/android/phone/DTMFTonePlayer.java
+++ b/src/com/android/phone/DTMFTonePlayer.java
@@ -116,11 +116,11 @@
     }
 
     @Override
-    public void onIncoming(Call call, ArrayList<String> textResponses) {
+    public void onIncoming(Call call) {
     }
 
     @Override
-    public void onUpdate(List<Call> calls, boolean full) {
+    public void onUpdate(List<Call> calls) {
         logD("Call updated");
         checkCallState();
     }
diff --git a/src/com/android/phone/RejectWithTextMessageManager.java b/src/com/android/phone/RejectWithTextMessageManager.java
index f032169..8324bd4 100644
--- a/src/com/android/phone/RejectWithTextMessageManager.java
+++ b/src/com/android/phone/RejectWithTextMessageManager.java
@@ -104,7 +104,7 @@
      *
      * @see com.android.phone.RejectWithTextMessageManager.Settings
      */
-    public ArrayList<String> loadCannedResponses() {
+    public static ArrayList<String> loadCannedResponses() {
         if (DBG) log("loadCannedResponses()...");
 
         final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(