/* * 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.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.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.ParcelableConference; import android.telecom.ParcelableConnection; import android.telecom.PhoneAccountHandle; import android.telecom.StatusHints; import android.telecom.TelecomManager; import android.telecom.VideoProfile; 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 { private final class Adapter extends IConnectionServiceAdapter.Stub { @Override public void handleCreateConnectionComplete(String callId, ConnectionRequest request, ParcelableConnection connection) { Log.startSession("CSW.hCCC"); long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { logIncoming("handleCreateConnectionComplete %s", callId); ConnectionServiceWrapper.this .handleCreateConnectionComplete(callId, request, connection); } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setActive(String callId) { Log.startSession("CSW.sA"); 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setRinging(String callId) { Log.startSession("CSW.sR"); 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setVideoProvider(String callId, IVideoProvider videoProvider) { Log.startSession("CSW.sVP"); long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { logIncoming("setVideoProvider %s", callId); Call call = mCallIdMapper.getCall(callId); if (call != null) { call.setVideoProvider(videoProvider); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setDialing(String callId) { Log.startSession("CSW.sD"); 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setDisconnected(String callId, DisconnectCause disconnectCause) { Log.startSession("CSW.sD"); 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setOnHold(String callId) { Log.startSession("CSW.sOH"); 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setRingbackRequested(String callId, boolean ringback) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void removeCall(String callId) { Log.startSession("CSW.rC"); 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); } } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setConnectionCapabilities(String callId, int connectionCapabilities) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setConnectionProperties(String callId, int connectionProperties) { 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setIsConferenced(String callId, String conferenceCallId) { Log.startSession("CSW.sIC"); 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.setParentCall(null); } else { Call conferenceCall = mCallIdMapper.getCall(conferenceCallId); childCall.setParentCall(conferenceCall); } } else { // Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setConferenceMergeFailed(String callId) { Log.startSession("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) { // Just refresh the connection capabilities so that the UI // is forced to reenable the merge button as the capability // is still on the connection. Note when b/20530631 is fixed, we need // to revisit this fix to remove this hacky way of unhiding the merge // button (side effect of reprocessing the capabilities) and plumb // the failure event all the way to InCallUI instead of stopping // it here. That way we can also handle the UI of notifying that // the merged has failed. call.setConnectionCapabilities(call.getConnectionCapabilities(), true); } else { Log.w(this, "setConferenceMergeFailed, unknown call id: %s", callId); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void addConferenceCall(String callId, ParcelableConference parcelableConference) { Log.startSession("CSW.aCC"); 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; } // 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; } // need to create a new Call PhoneAccountHandle phAcc = null; if (parcelableConference != null && parcelableConference.getPhoneAccount() != null) { phAcc = parcelableConference.getPhoneAccount(); } Call conferenceCall = mCallsManager.createConferenceCall(callId, phAcc, parcelableConference); mCallIdMapper.addCall(conferenceCall, callId); conferenceCall.setConnectionService(ConnectionServiceWrapper.this); 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.setParentCall(conferenceCall); } } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void onPostDialWait(String callId, String remaining) throws RemoteException { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void onPostDialChar(String callId, char nextChar) throws RemoteException { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void queryRemoteConnectionServices(RemoteServiceCallback callback) { final UserHandle callingUserHandle = Binder.getCallingUserHandle(); Log.startSession("CSW.qRCS"); long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { logIncoming("queryRemoteConnectionServices %s", callback); ConnectionServiceWrapper.this .queryRemoteConnectionServices(callingUserHandle, callback); } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setVideoState(String callId, int videoState) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setIsVoipAudioMode(String callId, boolean isVoip) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setStatusHints(String callId, StatusHints statusHints) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void putExtras(String callId, Bundle extras) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void removeExtras(String callId, List keys) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setAddress(String callId, Uri address, int presentation) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setCallerDisplayName( String callId, String callerDisplayName, int presentation) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void setConferenceableConnections( String callId, List conferenceableCallIds) { Log.startSession("CSW.sCC"); long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { logIncoming("setConferenceableConnections %s %s", callId, conferenceableCallIds); Call call = mCallIdMapper.getCall(callId); if (call != null) { List 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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void addExistingConnection(String callId, ParcelableConnection connection) { Log.startSession("CSW.aEC"); long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { logIncoming("addExistingConnection %s %s", callId, connection); Call existingCall = mCallsManager .createCallForExistingConnection(callId, connection); mCallIdMapper.addCall(existingCall, callId); existingCall.setConnectionService(ConnectionServiceWrapper.this); } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } @Override public void onConnectionEvent(String callId, String event, Bundle extras) { Log.startSession("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); } } } finally { Binder.restoreCallingIdentity(token); Log.endSession(); } } } private final Adapter mAdapter = new Adapter(); private final CallIdMapper mCallIdMapper = new CallIdMapper(); private final Map mPendingResponses = new HashMap<>(); private Binder2 mBinder = new Binder2(); private IConnectionService mServiceInterface; private final ConnectionServiceRepository mConnectionServiceRepository; private final PhoneAccountRegistrar mPhoneAccountRegistrar; private final CallsManager mCallsManager; /** * 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; } /** See {@link IConnectionService#addConnectionServiceAdapter}. */ private void addConnectionServiceAdapter(IConnectionServiceAdapter adapter) { if (isServiceValid("addConnectionServiceAdapter")) { try { logOutgoing("addConnectionServiceAdapter %s", adapter); mServiceInterface.addConnectionServiceAdapter(adapter); } 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()); } Log.event(call, Log.Events.START_CONNECTION, Log.piiHandle(call.getHandle())); try { mServiceInterface.createConnection( call.getConnectionManagerPhoneAccount(), callId, new ConnectionRequest( call.getTargetPhoneAccount(), call.getHandle(), extras, call.getVideoState(), callId), call.shouldAttachToExistingConnection(), call.isUnknown()); } 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); } /** @see IConnectionService#abort(String) */ 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); } catch (RemoteException e) { } } removeCall(call, new DisconnectCause(DisconnectCause.LOCAL)); } /** @see IConnectionService#silence(String) */ void silence(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("silence")) { try { logOutgoing("silence %s", callId); mServiceInterface.silence(callId); } catch (RemoteException e) { } } } /** @see IConnectionService#hold(String) */ void hold(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("hold")) { try { logOutgoing("hold %s", callId); mServiceInterface.hold(callId); } catch (RemoteException e) { } } } /** @see IConnectionService#unhold(String) */ void unhold(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("unhold")) { try { logOutgoing("unhold %s", callId); mServiceInterface.unhold(callId); } catch (RemoteException e) { } } } /** @see IConnectionService#onCallAudioStateChanged(String, CallAudioState) */ @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); } catch (RemoteException e) { } } } /** @see IConnectionService#disconnect(String) */ void disconnect(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("disconnect")) { try { logOutgoing("disconnect %s", callId); mServiceInterface.disconnect(callId); } catch (RemoteException e) { } } } /** @see IConnectionService#answer(String) */ 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); } else { mServiceInterface.answerVideo(callId, videoState); } } catch (RemoteException e) { } } } /** @see IConnectionService#reject(String) */ 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); } else { mServiceInterface.reject(callId); } } catch (RemoteException e) { } } } /** @see IConnectionService#playDtmfTone(String, char) */ 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); } catch (RemoteException e) { } } } /** @see IConnectionService#stopDtmfTone(String) */ void stopDtmfTone(Call call) { final String callId = mCallIdMapper.getCallId(call); if (callId != null && isServiceValid("stopDtmfTone")) { try { logOutgoing("stopDtmfTone %s", callId); mServiceInterface.stopDtmfTone(callId); } 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); } 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); } 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); } 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); } 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); } 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); } 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); } 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); } catch (RemoteException ignored) { } } } /** {@inheritDoc} */ @Override protected void setServiceInterface(IBinder binder) { if (binder == null) { // 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; } else { mServiceInterface = IConnectionService.Stub.asInterface(binder); addConnectionServiceAdapter(mAdapter); } } 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)); } } mCallIdMapper.clear(); } private void logIncoming(String msg, Object... params) { Log.d(this, "ConnectionService -> Telecom: " + msg, params); } private void logOutgoing(String msg, Object... params) { Log.d(this, "Telecom -> ConnectionService: " + 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 simServices = Collections.newSetFromMap( new ConcurrentHashMap(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 simServiceComponentNames = new ArrayList<>(); final List 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()); 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 componentNames, List 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); } }