/*
 * 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.server.telecom;

import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.Context;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.TelephonyManager;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IConnectionService;
import com.android.internal.telecom.IConnectionServiceAdapter;
import com.android.internal.telecom.IVideoProvider;
import com.android.internal.telecom.RemoteServiceCallback;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps
 * track of when the object can safely be unbound. Other classes should not use
 * {@link IConnectionService} directly and instead should use this class to invoke methods of
 * {@link IConnectionService}.
 */
@VisibleForTesting
public class ConnectionServiceWrapper extends ServiceBinder implements
        ConnectionServiceFocusManager.ConnectionServiceFocus {

    private final class Adapter extends IConnectionServiceAdapter.Stub {

        @Override
        public void handleCreateConnectionComplete(String callId, ConnectionRequest request,
                ParcelableConnection connection, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("handleCreateConnectionComplete %s", callId);
                    ConnectionServiceWrapper.this
                            .handleCreateConnectionComplete(callId, request, connection);

                    if (mServiceInterface != null) {
                        logOutgoing("createConnectionComplete %s", callId);
                        try {
                            mServiceInterface.createConnectionComplete(callId,
                                    Log.getExternalSession());
                        } catch (RemoteException e) {
                        }
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setActive(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_ACTIVE);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setActive %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        mCallsManager.markCallAsActive(call);
                    } else {
                        // Log.w(this, "setActive, unknown call id: %s", msg.obj);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setRinging(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_RINGING);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setRinging %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        mCallsManager.markCallAsRinging(call);
                    } else {
                        // Log.w(this, "setRinging, unknown call id: %s", msg.obj);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setVideoProvider(String callId, IVideoProvider videoProvider,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sVP");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setVideoProvider %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setVideoProvider(videoProvider);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setDialing(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_DIALING);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setDialing %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        mCallsManager.markCallAsDialing(call);
                    } else {
                        // Log.w(this, "setDialing, unknown call id: %s", msg.obj);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setPulling(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_PULLING);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setPulling %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        mCallsManager.markCallAsPulling(call);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setDisconnected(String callId, DisconnectCause disconnectCause,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_DISCONNECTED);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setDisconnected %s %s", callId, disconnectCause);
                    Call call = mCallIdMapper.getCall(callId);
                    Log.d(this, "disconnect call %s %s", disconnectCause, call);
                    if (call != null) {
                        mCallsManager.markCallAsDisconnected(call, disconnectCause);
                    } else {
                        // Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setOnHold(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_ON_HOLD);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setOnHold %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        mCallsManager.markCallAsOnHold(call);
                    } else {
                        // Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setRingbackRequested(String callId, boolean ringback,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.SRR");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setRingbackRequested %s %b", callId, ringback);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setRingbackRequested(ringback);
                    } else {
                        // Log.w(this, "setRingback, unknown call id: %s", args.arg1);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void removeCall(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_REMOVE_CALL);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("removeCall %s", callId);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        if (call.isAlive()) {
                            mCallsManager.markCallAsDisconnected(
                                    call, new DisconnectCause(DisconnectCause.REMOTE));
                        } else {
                            mCallsManager.markCallAsRemoved(call);
                        }
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setConnectionCapabilities(String callId, int connectionCapabilities,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sCC");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setConnectionCapabilities %s %d", callId, connectionCapabilities);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setConnectionCapabilities(connectionCapabilities);
                    } else {
                        // Log.w(ConnectionServiceWrapper.this,
                        // "setConnectionCapabilities, unknown call id: %s", msg.obj);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setConnectionProperties(String callId, int connectionProperties,
                Session.Info sessionInfo) {
            Log.startSession("CSW.sCP");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setConnectionProperties %s %d", callId, connectionProperties);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setConnectionProperties(connectionProperties);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setIsConferenced(String callId, String conferenceCallId,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_SET_IS_CONFERENCED);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setIsConferenced %s %s", callId, conferenceCallId);
                    Call childCall = mCallIdMapper.getCall(callId);
                    if (childCall != null) {
                        if (conferenceCallId == null) {
                            Log.d(this, "unsetting parent: %s", conferenceCallId);
                            childCall.setParentAndChildCall(null);
                        } else {
                            Call conferenceCall = mCallIdMapper.getCall(conferenceCallId);
                            childCall.setParentAndChildCall(conferenceCall);
                        }
                    } else {
                        // Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setConferenceMergeFailed(String callId, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sCMF");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setConferenceMergeFailed %s", callId);
                    // TODO: we should move the UI for indication a merge failure here
                    // from CallNotifier.onSuppServiceFailed(). This way the InCallUI can
                    // deliver the message anyway that they want. b/20530631.
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.onConnectionEvent(Connection.EVENT_CALL_MERGE_FAILED, null);
                    } else {
                        Log.w(this, "setConferenceMergeFailed, unknown call id: %s", callId);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void addConferenceCall(String callId, ParcelableConference parcelableConference,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL);
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    if (mCallIdMapper.getCall(callId) != null) {
                        Log.w(this, "Attempting to add a conference call using an existing " +
                                "call id %s", callId);
                        return;
                    }
                    logIncoming("addConferenceCall %s %s [%s]", callId, parcelableConference,
                            parcelableConference.getConnectionIds());

                    // Make sure that there's at least one valid call. For remote connections
                    // we'll get a add conference msg from both the remote connection service
                    // and from the real connection service.
                    boolean hasValidCalls = false;
                    for (String connId : parcelableConference.getConnectionIds()) {
                        if (mCallIdMapper.getCall(connId) != null) {
                            hasValidCalls = true;
                        }
                    }
                    // But don't bail out if the connection count is 0, because that is a valid
                    // IMS conference state.
                    if (!hasValidCalls && parcelableConference.getConnectionIds().size() > 0) {
                        Log.d(this, "Attempting to add a conference with no valid calls");
                        return;
                    }

                    PhoneAccountHandle phAcc = null;
                    if (parcelableConference != null &&
                            parcelableConference.getPhoneAccount() != null) {
                        phAcc = parcelableConference.getPhoneAccount();
                    }

                    Bundle connectionExtras = parcelableConference.getExtras();

                    String connectIdToCheck = null;
                    if (connectionExtras != null && connectionExtras
                            .containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
                        // Conference was added via a connection manager, see if its original id is
                        // known.
                        connectIdToCheck = connectionExtras
                                .getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID);
                    } else {
                        connectIdToCheck = callId;
                    }

                    Call conferenceCall;
                    // Check to see if this conference has already been added.
                    Call alreadyAddedConnection = mCallsManager
                            .getAlreadyAddedConnection(connectIdToCheck);
                    if (alreadyAddedConnection != null && mCallIdMapper.getCall(callId) == null) {
                        // We are currently attempting to add the conference via a connection mgr,
                        // and the originating ConnectionService has already added it.  Instead of
                        // making a new Telecom call, we will simply add it to the ID mapper here,
                        // and replace the ConnectionService on the call.
                        mCallIdMapper.addCall(alreadyAddedConnection, callId);
                        alreadyAddedConnection.replaceConnectionService(
                                ConnectionServiceWrapper.this);
                        conferenceCall = alreadyAddedConnection;
                    } else {
                        // need to create a new Call
                        Call newConferenceCall = mCallsManager.createConferenceCall(callId,
                                phAcc, parcelableConference);
                        mCallIdMapper.addCall(newConferenceCall, callId);
                        newConferenceCall.setConnectionService(ConnectionServiceWrapper.this);
                        conferenceCall = newConferenceCall;
                    }

                    Log.d(this, "adding children to conference %s phAcc %s",
                            parcelableConference.getConnectionIds(), phAcc);
                    for (String connId : parcelableConference.getConnectionIds()) {
                        Call childCall = mCallIdMapper.getCall(connId);
                        Log.d(this, "found child: %s", connId);
                        if (childCall != null) {
                            childCall.setParentAndChildCall(conferenceCall);
                        }
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onPostDialWait(String callId, String remaining,
                Session.Info sessionInfo) throws RemoteException {
            Log.startSession(sessionInfo, "CSW.oPDW");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("onPostDialWait %s %s", callId, remaining);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.onPostDialWait(remaining);
                    } else {
                        // Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onPostDialChar(String callId, char nextChar,
                Session.Info sessionInfo) throws RemoteException {
            Log.startSession(sessionInfo, "CSW.oPDC");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("onPostDialChar %s %s", callId, nextChar);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.onPostDialChar(nextChar);
                    } else {
                        // Log.w(this, "onPostDialChar, unknown call id: %s", args.arg1);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void queryRemoteConnectionServices(RemoteServiceCallback callback,
                Session.Info sessionInfo) {
            final UserHandle callingUserHandle = Binder.getCallingUserHandle();
            Log.startSession(sessionInfo, "CSW.qRCS");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("queryRemoteConnectionServices %s", callback);
                    ConnectionServiceWrapper.this
                            .queryRemoteConnectionServices(callingUserHandle, callback);
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setVideoState(String callId, int videoState, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sVS");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setVideoState %s %d", callId, videoState);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setVideoState(videoState);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setIsVoipAudioMode(String callId, boolean isVoip, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sIVAM");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setIsVoipAudioMode %s %b", callId, isVoip);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setIsVoipAudioMode(isVoip);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setAudioRoute(String callId, int audioRoute,
                String bluetoothAddress, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sAR");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setAudioRoute %s %s", callId,
                            CallAudioState.audioRouteToString(audioRoute));
                    mCallsManager.setAudioRoute(audioRoute, bluetoothAddress);
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setStatusHints(String callId, StatusHints statusHints,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sSH");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setStatusHints %s %s", callId, statusHints);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setStatusHints(statusHints);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void putExtras(String callId, Bundle extras, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.pE");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    Bundle.setDefusable(extras, true);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.putExtras(Call.SOURCE_CONNECTION_SERVICE, extras);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void removeExtras(String callId, List<String> keys, Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.rE");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("removeExtra %s %s", callId, keys);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.removeExtras(Call.SOURCE_CONNECTION_SERVICE, keys);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setAddress(String callId, Uri address, int presentation,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sA");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setAddress %s %s %d", callId, address, presentation);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setHandle(address, presentation);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setCallerDisplayName(String callId, String callerDisplayName, int presentation,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sCDN");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    logIncoming("setCallerDisplayName %s %s %d", callId, callerDisplayName,
                            presentation);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setCallerDisplayName(callerDisplayName, presentation);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void setConferenceableConnections(String callId, List<String> conferenceableCallIds,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.sCC");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {

                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        logIncoming("setConferenceableConnections %s %s", callId,
                                conferenceableCallIds);
                        List<Call> conferenceableCalls =
                                new ArrayList<>(conferenceableCallIds.size());
                        for (String otherId : conferenceableCallIds) {
                            Call otherCall = mCallIdMapper.getCall(otherId);
                            if (otherCall != null && otherCall != call) {
                                conferenceableCalls.add(otherCall);
                            }
                        }
                        call.setConferenceableCalls(conferenceableCalls);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void addExistingConnection(String callId, ParcelableConnection connection,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.aEC");
            UserHandle userHandle = Binder.getCallingUserHandle();
            // Check that the Calling Package matches PhoneAccountHandle's Component Package
            PhoneAccountHandle callingPhoneAccountHandle = connection.getPhoneAccount();
            if (callingPhoneAccountHandle != null) {
                mAppOpsManager.checkPackage(Binder.getCallingUid(),
                        callingPhoneAccountHandle.getComponentName().getPackageName());
            }
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    // Make sure that the PhoneAccount associated with the incoming
                    // ParcelableConnection is in fact registered to Telecom and is being called
                    // from the correct user.
                    List<PhoneAccountHandle> accountHandles =
                            mPhoneAccountRegistrar.getCallCapablePhoneAccounts(null /*uriScheme*/,
                                    false /*includeDisabledAccounts*/, userHandle);
                    PhoneAccountHandle phoneAccountHandle = null;
                    for (PhoneAccountHandle accountHandle : accountHandles) {
                        if(accountHandle.equals(callingPhoneAccountHandle)) {
                            phoneAccountHandle = accountHandle;
                        }
                    }
                    // Allow the Sim call manager account as well, even if its disabled.
                    if (phoneAccountHandle == null && callingPhoneAccountHandle != null) {
                        if (callingPhoneAccountHandle.equals(
                                mPhoneAccountRegistrar.getSimCallManager(userHandle))) {
                            phoneAccountHandle = callingPhoneAccountHandle;
                        }
                    }
                    if (phoneAccountHandle != null) {
                        logIncoming("addExistingConnection %s %s", callId, connection);

                        Bundle connectionExtras = connection.getExtras();
                        String connectIdToCheck = null;
                        if (connectionExtras != null && connectionExtras
                                .containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
                            connectIdToCheck = connectionExtras
                                    .getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID);
                        } else {
                            connectIdToCheck = callId;
                        }
                        // Check to see if this Connection has already been added.
                        Call alreadyAddedConnection = mCallsManager
                                .getAlreadyAddedConnection(connectIdToCheck);

                        if (alreadyAddedConnection != null
                                && mCallIdMapper.getCall(callId) == null) {
                            mCallIdMapper.addCall(alreadyAddedConnection, callId);
                            alreadyAddedConnection
                                    .replaceConnectionService(ConnectionServiceWrapper.this);
                            return;
                        }

                        Call existingCall = mCallsManager
                                .createCallForExistingConnection(callId, connection);
                        mCallIdMapper.addCall(existingCall, callId);
                        existingCall.setConnectionService(ConnectionServiceWrapper.this);
                    } else {
                        Log.e(this, new RemoteException("The PhoneAccount being used is not " +
                                "currently registered with Telecom."), "Unable to " +
                                "addExistingConnection.");
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onConnectionEvent(String callId, String event, Bundle extras,
                Session.Info sessionInfo) {
            Log.startSession(sessionInfo, "CSW.oCE");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    Bundle.setDefusable(extras, true);
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.onConnectionEvent(event, extras);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onRttInitiationSuccess(String callId, Session.Info sessionInfo)
                throws RemoteException {

        }

        @Override
        public void onRttInitiationFailure(String callId, int reason, Session.Info sessionInfo)
                throws RemoteException {
            Log.startSession(sessionInfo, "CSW.oRIF");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.onRttConnectionFailure(reason);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onRttSessionRemotelyTerminated(String callId, Session.Info sessionInfo)
                throws RemoteException {

        }

        @Override
        public void onRemoteRttRequest(String callId, Session.Info sessionInfo)
                throws RemoteException {
            Log.startSession(sessionInfo, "CSW.oRRR");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.onRemoteRttRequest();
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onPhoneAccountChanged(String callId, PhoneAccountHandle pHandle,
                Session.Info sessionInfo) throws RemoteException {
            // Check that the Calling Package matches PhoneAccountHandle's Component Package
            if (pHandle != null) {
                mAppOpsManager.checkPackage(Binder.getCallingUid(),
                        pHandle.getComponentName().getPackageName());
            }
            Log.startSession(sessionInfo, "CSW.oPAC");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    Call call = mCallIdMapper.getCall(callId);
                    if (call != null) {
                        call.setTargetPhoneAccount(pHandle);
                    }
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }

        @Override
        public void onConnectionServiceFocusReleased(Session.Info sessionInfo)
                throws RemoteException {
            Log.startSession(sessionInfo, "CSW.oCSFR");
            long token = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    mConnSvrFocusListener.onConnectionServiceReleased(
                            ConnectionServiceWrapper.this);
                }
            } catch (Throwable t) {
                Log.e(ConnectionServiceWrapper.this, t, "");
                throw t;
            } finally {
                Binder.restoreCallingIdentity(token);
                Log.endSession();
            }
        }
    }

    private final Adapter mAdapter = new Adapter();
    private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getConnectionId);
    private final Map<String, CreateConnectionResponse> mPendingResponses = new HashMap<>();

    private Binder2 mBinder = new Binder2();
    private IConnectionService mServiceInterface;
    private final ConnectionServiceRepository mConnectionServiceRepository;
    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
    private final CallsManager mCallsManager;
    private final AppOpsManager mAppOpsManager;

    private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;

    /**
     * Creates a connection service.
     *
     * @param componentName The component name of the service with which to bind.
     * @param connectionServiceRepository Connection service repository.
     * @param phoneAccountRegistrar Phone account registrar
     * @param callsManager Calls manager
     * @param context The context.
     * @param userHandle The {@link UserHandle} to use when binding.
     */
    ConnectionServiceWrapper(
            ComponentName componentName,
            ConnectionServiceRepository connectionServiceRepository,
            PhoneAccountRegistrar phoneAccountRegistrar,
            CallsManager callsManager,
            Context context,
            TelecomSystem.SyncRoot lock,
            UserHandle userHandle) {
        super(ConnectionService.SERVICE_INTERFACE, componentName, context, lock, userHandle);
        mConnectionServiceRepository = connectionServiceRepository;
        phoneAccountRegistrar.addListener(new PhoneAccountRegistrar.Listener() {
            // TODO -- Upon changes to PhoneAccountRegistrar, need to re-wire connections
            // To do this, we must proxy remote ConnectionService objects
        });
        mPhoneAccountRegistrar = phoneAccountRegistrar;
        mCallsManager = callsManager;
        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
    }

    /** See {@link IConnectionService#addConnectionServiceAdapter}. */
    private void addConnectionServiceAdapter(IConnectionServiceAdapter adapter) {
        if (isServiceValid("addConnectionServiceAdapter")) {
            try {
                logOutgoing("addConnectionServiceAdapter %s", adapter);
                mServiceInterface.addConnectionServiceAdapter(adapter, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** See {@link IConnectionService#removeConnectionServiceAdapter}. */
    private void removeConnectionServiceAdapter(IConnectionServiceAdapter adapter) {
        if (isServiceValid("removeConnectionServiceAdapter")) {
            try {
                logOutgoing("removeConnectionServiceAdapter %s", adapter);
                mServiceInterface.removeConnectionServiceAdapter(adapter, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /**
     * Creates a new connection for a new outgoing call or to attach to an existing incoming call.
     */
    @VisibleForTesting
    public void createConnection(final Call call, final CreateConnectionResponse response) {
        Log.d(this, "createConnection(%s) via %s.", call, getComponentName());
        BindCallback callback = new BindCallback() {
            @Override
            public void onSuccess() {
                String callId = mCallIdMapper.getCallId(call);
                mPendingResponses.put(callId, response);

                GatewayInfo gatewayInfo = call.getGatewayInfo();
                Bundle extras = call.getIntentExtras();
                if (gatewayInfo != null && gatewayInfo.getGatewayProviderPackageName() != null &&
                        gatewayInfo.getOriginalAddress() != null) {
                    extras = (Bundle) extras.clone();
                    extras.putString(
                            TelecomManager.GATEWAY_PROVIDER_PACKAGE,
                            gatewayInfo.getGatewayProviderPackageName());
                    extras.putParcelable(
                            TelecomManager.GATEWAY_ORIGINAL_ADDRESS,
                            gatewayInfo.getOriginalAddress());
                }

                if (call.isIncoming() && mCallsManager.getEmergencyCallHelper()
                        .getLastEmergencyCallTimeMillis() > 0) {
                  // Add the last emergency call time to the connection request for incoming calls
                  if (extras == call.getIntentExtras()) {
                    extras = (Bundle) extras.clone();
                  }
                  extras.putLong(android.telecom.Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS,
                      mCallsManager.getEmergencyCallHelper().getLastEmergencyCallTimeMillis());
                }

                // Call is incoming and added because we're handing over from another; tell CS
                // that its expected to handover.
                if (call.isIncoming() && call.getHandoverSourceCall() != null) {
                    extras.putBoolean(TelecomManager.EXTRA_IS_HANDOVER, true);
                    extras.putParcelable(TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT,
                            call.getHandoverSourceCall().getTargetPhoneAccount());
                }

                Log.addEvent(call, LogUtils.Events.START_CONNECTION,
                        Log.piiHandle(call.getHandle()));

                ConnectionRequest connectionRequest = new ConnectionRequest.Builder()
                        .setAccountHandle(call.getTargetPhoneAccount())
                        .setAddress(call.getHandle())
                        .setExtras(extras)
                        .setVideoState(call.getVideoState())
                        .setTelecomCallId(callId)
                        // For self-managed incoming calls, if there is another ongoing call Telecom
                        // is responsible for showing a UI to ask the user if they'd like to answer
                        // this new incoming call.
                        .setShouldShowIncomingCallUi(
                                !mCallsManager.shouldShowSystemIncomingCallUi(call))
                        .setRttPipeFromInCall(call.getInCallToCsRttPipeForCs())
                        .setRttPipeToInCall(call.getCsToInCallRttPipeForCs())
                        .build();

                try {
                    mServiceInterface.createConnection(
                            call.getConnectionManagerPhoneAccount(),
                            callId,
                            connectionRequest,
                            call.shouldAttachToExistingConnection(),
                            call.isUnknown(),
                            Log.getExternalSession());

                } catch (RemoteException e) {
                    Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
                    mPendingResponses.remove(callId).handleCreateConnectionFailure(
                            new DisconnectCause(DisconnectCause.ERROR, e.toString()));
                }
            }

            @Override
            public void onFailure() {
                Log.e(this, new Exception(), "Failure to call %s", getComponentName());
                response.handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.ERROR));
            }
        };

        mBinder.bind(callback, call);
    }

    /**
     * Notifies the {@link ConnectionService} associated with a {@link Call} that the request to
     * create a connection has been denied or failed.
     * @param call The call.
     */
    void createConnectionFailed(final Call call) {
        Log.d(this, "createConnectionFailed(%s) via %s.", call, getComponentName());
        BindCallback callback = new BindCallback() {
            @Override
            public void onSuccess() {
                final String callId = mCallIdMapper.getCallId(call);
                // If still bound, tell the connection service create connection has failed.
                if (callId != null && isServiceValid("createConnectionFailed")) {
                    Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_FAILED,
                            Log.piiHandle(call.getHandle()));
                    try {
                        logOutgoing("createConnectionFailed %s", callId);
                        mServiceInterface.createConnectionFailed(
                                call.getConnectionManagerPhoneAccount(),
                                callId,
                                new ConnectionRequest(
                                        call.getTargetPhoneAccount(),
                                        call.getHandle(),
                                        call.getIntentExtras(),
                                        call.getVideoState(),
                                        callId,
                                        false),
                                call.isIncoming(),
                                Log.getExternalSession());
                        call.setDisconnectCause(new DisconnectCause(DisconnectCause.CANCELED));
                        call.disconnect();
                    } catch (RemoteException e) {
                    }
                }
            }

            @Override
            public void onFailure() {
                // Binding failed.  Oh no.
                Log.w(this, "onFailure - could not bind to CS for call %s", call.getId());
            }
        };

        mBinder.bind(callback, call);
    }

    void handoverFailed(final Call call, final int reason) {
        Log.d(this, "handoverFailed(%s) via %s.", call, getComponentName());
        BindCallback callback = new BindCallback() {
            @Override
            public void onSuccess() {
                final String callId = mCallIdMapper.getCallId(call);
                // If still bound, tell the connection service create connection has failed.
                if (callId != null && isServiceValid("handoverFailed")) {
                    Log.addEvent(call, LogUtils.Events.HANDOVER_FAILED,
                            Log.piiHandle(call.getHandle()));
                    try {
                        mServiceInterface.handoverFailed(
                                callId,
                                new ConnectionRequest(
                                        call.getTargetPhoneAccount(),
                                        call.getHandle(),
                                        call.getIntentExtras(),
                                        call.getVideoState(),
                                        callId,
                                        false), reason, Log.getExternalSession());
                    } catch (RemoteException e) {
                    }
                }
            }

            @Override
            public void onFailure() {
                // Binding failed.
                Log.w(this, "onFailure - could not bind to CS for call %s",
                        call.getId());
            }
        };

        mBinder.bind(callback, call);
    }

    void handoverComplete(final Call call) {
        Log.d(this, "handoverComplete(%s) via %s.", call, getComponentName());
        BindCallback callback = new BindCallback() {
            @Override
            public void onSuccess() {
                final String callId = mCallIdMapper.getCallId(call);
                // If still bound, tell the connection service create connection has failed.
                if (callId != null && isServiceValid("handoverComplete")) {
                    try {
                        mServiceInterface.handoverComplete(
                                callId,
                                Log.getExternalSession());
                    } catch (RemoteException e) {
                    }
                }
            }

            @Override
            public void onFailure() {
                // Binding failed.
                Log.w(this, "onFailure - could not bind to CS for call %s",
                        call.getId());
            }
        };

        mBinder.bind(callback, call);
    }

    /** @see IConnectionService#abort(String, Session.Info)  */
    void abort(Call call) {
        // Clear out any pending outgoing call data
        final String callId = mCallIdMapper.getCallId(call);

        // If still bound, tell the connection service to abort.
        if (callId != null && isServiceValid("abort")) {
            try {
                logOutgoing("abort %s", callId);
                mServiceInterface.abort(callId, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }

        removeCall(call, new DisconnectCause(DisconnectCause.LOCAL));
    }

    /** @see IConnectionService#silence(String, Session.Info) */
    void silence(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("silence")) {
            try {
                logOutgoing("silence %s", callId);
                mServiceInterface.silence(callId, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#hold(String, Session.Info) */
    void hold(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("hold")) {
            try {
                logOutgoing("hold %s", callId);
                mServiceInterface.hold(callId, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#unhold(String, Session.Info) */
    void unhold(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("unhold")) {
            try {
                logOutgoing("unhold %s", callId);
                mServiceInterface.unhold(callId, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#onCallAudioStateChanged(String, CallAudioState, Session.Info) */
    @VisibleForTesting
    public void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
        final String callId = mCallIdMapper.getCallId(activeCall);
        if (callId != null && isServiceValid("onCallAudioStateChanged")) {
            try {
                logOutgoing("onCallAudioStateChanged %s %s", callId, audioState);
                mServiceInterface.onCallAudioStateChanged(callId, audioState,
                        Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#disconnect(String, Session.Info) */
    void disconnect(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("disconnect")) {
            try {
                logOutgoing("disconnect %s", callId);
                mServiceInterface.disconnect(callId, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#answer(String, Session.Info) */
    void answer(Call call, int videoState) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("answer")) {
            try {
                logOutgoing("answer %s %d", callId, videoState);
                if (VideoProfile.isAudioOnly(videoState)) {
                    mServiceInterface.answer(callId, Log.getExternalSession());
                } else {
                    mServiceInterface.answerVideo(callId, videoState, Log.getExternalSession());
                }
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#deflect(String, Uri , Session.Info) */
    void deflect(Call call, Uri address) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("deflect")) {
            try {
                logOutgoing("deflect %s", callId);
                mServiceInterface.deflect(callId, address, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#reject(String, Session.Info) */
    void reject(Call call, boolean rejectWithMessage, String message) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("reject")) {
            try {
                logOutgoing("reject %s", callId);

                if (rejectWithMessage && call.can(
                        Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION)) {
                    mServiceInterface.rejectWithMessage(callId, message, Log.getExternalSession());
                } else {
                    mServiceInterface.reject(callId, Log.getExternalSession());
                }
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#playDtmfTone(String, char, Session.Info) */
    void playDtmfTone(Call call, char digit) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("playDtmfTone")) {
            try {
                logOutgoing("playDtmfTone %s %c", callId, digit);
                mServiceInterface.playDtmfTone(callId, digit, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    /** @see IConnectionService#stopDtmfTone(String, Session.Info) */
    void stopDtmfTone(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("stopDtmfTone")) {
            try {
                logOutgoing("stopDtmfTone %s", callId);
                mServiceInterface.stopDtmfTone(callId, Log.getExternalSession());
            } catch (RemoteException e) {
            }
        }
    }

    void addCall(Call call) {
        if (mCallIdMapper.getCallId(call) == null) {
            mCallIdMapper.addCall(call);
        }
    }

    /**
     * Associates newCall with this connection service by replacing callToReplace.
     */
    void replaceCall(Call newCall, Call callToReplace) {
        Preconditions.checkState(callToReplace.getConnectionService() == this);
        mCallIdMapper.replaceCall(newCall, callToReplace);
    }

    void removeCall(Call call) {
        removeCall(call, new DisconnectCause(DisconnectCause.ERROR));
    }

    void removeCall(String callId, DisconnectCause disconnectCause) {
        CreateConnectionResponse response = mPendingResponses.remove(callId);
        if (response != null) {
            response.handleCreateConnectionFailure(disconnectCause);
        }

        mCallIdMapper.removeCall(callId);
    }

    void removeCall(Call call, DisconnectCause disconnectCause) {
        CreateConnectionResponse response = mPendingResponses.remove(mCallIdMapper.getCallId(call));
        if (response != null) {
            response.handleCreateConnectionFailure(disconnectCause);
        }

        mCallIdMapper.removeCall(call);
    }

    void onPostDialContinue(Call call, boolean proceed) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("onPostDialContinue")) {
            try {
                logOutgoing("onPostDialContinue %s %b", callId, proceed);
                mServiceInterface.onPostDialContinue(callId, proceed, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void conference(final Call call, Call otherCall) {
        final String callId = mCallIdMapper.getCallId(call);
        final String otherCallId = mCallIdMapper.getCallId(otherCall);
        if (callId != null && otherCallId != null && isServiceValid("conference")) {
            try {
                logOutgoing("conference %s %s", callId, otherCallId);
                mServiceInterface.conference(callId, otherCallId, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void splitFromConference(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("splitFromConference")) {
            try {
                logOutgoing("splitFromConference %s", callId);
                mServiceInterface.splitFromConference(callId, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void mergeConference(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("mergeConference")) {
            try {
                logOutgoing("mergeConference %s", callId);
                mServiceInterface.mergeConference(callId, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void swapConference(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("swapConference")) {
            try {
                logOutgoing("swapConference %s", callId);
                mServiceInterface.swapConference(callId, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void pullExternalCall(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("pullExternalCall")) {
            try {
                logOutgoing("pullExternalCall %s", callId);
                mServiceInterface.pullExternalCall(callId, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void sendCallEvent(Call call, String event, Bundle extras) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("sendCallEvent")) {
            try {
                logOutgoing("sendCallEvent %s %s", callId, event);
                mServiceInterface.sendCallEvent(callId, event, extras, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void onExtrasChanged(Call call, Bundle extras) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("onExtrasChanged")) {
            try {
                logOutgoing("onExtrasChanged %s %s", callId, extras);
                mServiceInterface.onExtrasChanged(callId, extras, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void startRtt(Call call, ParcelFileDescriptor fromInCall, ParcelFileDescriptor toInCall) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("startRtt")) {
            try {
                logOutgoing("startRtt: %s %s %s", callId, fromInCall, toInCall);
                mServiceInterface.startRtt(callId, fromInCall, toInCall, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void stopRtt(Call call) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("stopRtt")) {
            try {
                logOutgoing("stopRtt: %s", callId);
                mServiceInterface.stopRtt(callId, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    void respondToRttRequest(
            Call call, ParcelFileDescriptor fromInCall, ParcelFileDescriptor toInCall) {
        final String callId = mCallIdMapper.getCallId(call);
        if (callId != null && isServiceValid("respondToRttRequest")) {
            try {
                logOutgoing("respondToRttRequest: %s %s %s", callId, fromInCall, toInCall);
                mServiceInterface.respondToRttUpgradeRequest(
                        callId, fromInCall, toInCall, Log.getExternalSession());
            } catch (RemoteException ignored) {
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void setServiceInterface(IBinder binder) {
        mServiceInterface = IConnectionService.Stub.asInterface(binder);
        Log.v(this, "Adding Connection Service Adapter.");
        addConnectionServiceAdapter(mAdapter);
    }

    /** {@inheritDoc} */
    @Override
    protected void removeServiceInterface() {
        Log.v(this, "Removing Connection Service Adapter.");
        removeConnectionServiceAdapter(mAdapter);
        // We have lost our service connection. Notify the world that this service is done.
        // We must notify the adapter before CallsManager. The adapter will force any pending
        // outgoing calls to try the next service. This needs to happen before CallsManager
        // tries to clean up any calls still associated with this service.
        handleConnectionServiceDeath();
        mCallsManager.handleConnectionServiceDeath(this);
        mServiceInterface = null;
    }

    @Override
    public void connectionServiceFocusLost() {
        // Immediately response to the Telecom that it has released the call resources.
        // TODO(mpq): Change back to the default implementation once b/69651192 done.
        if (mConnSvrFocusListener != null) {
            mConnSvrFocusListener.onConnectionServiceReleased(ConnectionServiceWrapper.this);
        }
        BindCallback callback = new BindCallback() {
            @Override
            public void onSuccess() {
                try {
                    mServiceInterface.connectionServiceFocusLost(Log.getExternalSession());
                } catch (RemoteException ignored) {
                    Log.d(this, "failed to inform the focus lost event");
                }
            }

            @Override
            public void onFailure() {}
        };
        mBinder.bind(callback, null /* null call */);
    }

    @Override
    public void connectionServiceFocusGained() {
        BindCallback callback = new BindCallback() {
            @Override
            public void onSuccess() {
                try {
                    mServiceInterface.connectionServiceFocusGained(Log.getExternalSession());
                } catch (RemoteException ignored) {
                    Log.d(this, "failed to inform the focus gained event");
                }
            }

            @Override
            public void onFailure() {}
        };
        mBinder.bind(callback, null /* null call */);
    }

    @Override
    public void setConnectionServiceFocusListener(
            ConnectionServiceFocusManager.ConnectionServiceFocusListener listener) {
        mConnSvrFocusListener = listener;
    }

    private void handleCreateConnectionComplete(
            String callId,
            ConnectionRequest request,
            ParcelableConnection connection) {
        // TODO: Note we are not using parameter "request", which is a side effect of our tacit
        // assumption that we have at most one outgoing connection attempt per ConnectionService.
        // This may not continue to be the case.
        if (connection.getState() == Connection.STATE_DISCONNECTED) {
            // A connection that begins in the DISCONNECTED state is an indication of
            // failure to connect; we handle all failures uniformly
            removeCall(callId, connection.getDisconnectCause());
        } else {
            // Successful connection
            if (mPendingResponses.containsKey(callId)) {
                mPendingResponses.remove(callId)
                        .handleCreateConnectionSuccess(mCallIdMapper, connection);
            }
        }
    }

    /**
     * Called when the associated connection service dies.
     */
    private void handleConnectionServiceDeath() {
        if (!mPendingResponses.isEmpty()) {
            CreateConnectionResponse[] responses = mPendingResponses.values().toArray(
                    new CreateConnectionResponse[mPendingResponses.values().size()]);
            mPendingResponses.clear();
            for (int i = 0; i < responses.length; i++) {
                responses[i].handleCreateConnectionFailure(
                        new DisconnectCause(DisconnectCause.ERROR, "CS_DEATH"));
            }
        }
        mCallIdMapper.clear();

        if (mConnSvrFocusListener != null) {
            mConnSvrFocusListener.onConnectionServiceDeath(this);
        }
    }

    private void logIncoming(String msg, Object... params) {
        Log.d(this, "ConnectionService -> Telecom[" + mComponentName.flattenToShortString() + "]: "
                + msg, params);
    }

    private void logOutgoing(String msg, Object... params) {
        Log.d(this, "Telecom -> ConnectionService[" + mComponentName.flattenToShortString() + "]: "
                + msg, params);
    }

    private void queryRemoteConnectionServices(final UserHandle userHandle,
            final RemoteServiceCallback callback) {
        // Only give remote connection services to this connection service if it is listed as
        // the connection manager.
        PhoneAccountHandle simCallManager = mPhoneAccountRegistrar.getSimCallManager(userHandle);
        Log.d(this, "queryRemoteConnectionServices finds simCallManager = %s", simCallManager);
        if (simCallManager == null ||
                !simCallManager.getComponentName().equals(getComponentName())) {
            noRemoteServices(callback);
            return;
        }

        // Make a list of ConnectionServices that are listed as being associated with SIM accounts
        final Set<ConnectionServiceWrapper> simServices = Collections.newSetFromMap(
                new ConcurrentHashMap<ConnectionServiceWrapper, Boolean>(8, 0.9f, 1));
        for (PhoneAccountHandle handle : mPhoneAccountRegistrar.getSimPhoneAccounts(userHandle)) {
            ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
                    handle.getComponentName(), handle.getUserHandle());
            if (service != null) {
                simServices.add(service);
            }
        }

        final List<ComponentName> simServiceComponentNames = new ArrayList<>();
        final List<IBinder> simServiceBinders = new ArrayList<>();

        Log.v(this, "queryRemoteConnectionServices, simServices = %s", simServices);

        for (ConnectionServiceWrapper simService : simServices) {
            if (simService == this) {
                // Only happens in the unlikely case that a SIM service is also a SIM call manager
                continue;
            }

            final ConnectionServiceWrapper currentSimService = simService;

            currentSimService.mBinder.bind(new BindCallback() {
                @Override
                public void onSuccess() {
                    Log.d(this, "Adding simService %s", currentSimService.getComponentName());
                    if (currentSimService.mServiceInterface == null) {
                        // The remote ConnectionService died, so do not add it.
                        // We will still perform maybeComplete() and notify the caller with an empty
                        // list of sim services via maybeComplete().
                        Log.w(this, "queryRemoteConnectionServices: simService %s died - Skipping.",
                                currentSimService.getComponentName());
                    } else {
                        simServiceComponentNames.add(currentSimService.getComponentName());
                        simServiceBinders.add(currentSimService.mServiceInterface.asBinder());
                    }
                    maybeComplete();
                }

                @Override
                public void onFailure() {
                    Log.d(this, "Failed simService %s", currentSimService.getComponentName());
                    // We know maybeComplete() will always be a no-op from now on, so go ahead and
                    // signal failure of the entire request
                    noRemoteServices(callback);
                }

                private void maybeComplete() {
                    if (simServiceComponentNames.size() == simServices.size()) {
                        setRemoteServices(callback, simServiceComponentNames, simServiceBinders);
                    }
                }
            }, null);
        }
    }

    private void setRemoteServices(
            RemoteServiceCallback callback,
            List<ComponentName> componentNames,
            List<IBinder> binders) {
        try {
            callback.onResult(componentNames, binders);
        } catch (RemoteException e) {
            Log.e(this, e, "Contacting ConnectionService %s",
                    ConnectionServiceWrapper.this.getComponentName());
        }
    }

    private void noRemoteServices(RemoteServiceCallback callback) {
        setRemoteServices(callback, Collections.EMPTY_LIST, Collections.EMPTY_LIST);
    }
}
