Merge "Implement new APIs to notify call audio routing information as a form of CallEndpoint"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d3196b9..d67df4b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -383,4 +383,16 @@
     <string name="cancel">Cancel</string>
     <!-- Button label for generic back action [CHAR LIMIT=20] -->
     <string name="back">Back</string>
+    <!-- The user-visible name of the earpiece type CallEndpoint -->
+    <string name="callendpoint_name_earpiece">Earpiece</string>
+    <!-- The user-visible name of the bluetooth type CallEndpoint -->
+    <string name="callendpoint_name_bluetooth">Bluetooth</string>
+    <!-- The user-visible name of the wired headset type CallEndpoint -->
+    <string name="callendpoint_name_wiredheadset">Wired headset</string>
+    <!-- The user-visible name of the speaker type CallEndpoint -->
+    <string name="callendpoint_name_speaker">Speaker</string>
+    <!-- The user-visible name of the streaming type CallEndpoint -->
+    <string name="callendpoint_name_streaming">External</string>
+    <!-- The user-visible name of the unknown new type CallEndpoint -->
+    <string name="callendpoint_name_unknown">Unknown</string>
 </resources>
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 672ee3b..4a15b0f 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -413,7 +413,8 @@
      * @param bluetoothAddress the address of the desired bluetooth device, if route is
      * {@link CallAudioState#ROUTE_BLUETOOTH}.
      */
-    void setAudioRoute(int route, String bluetoothAddress) {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+    public void setAudioRoute(int route, String bluetoothAddress) {
         Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
         switch (route) {
             case CallAudioState.ROUTE_BLUETOOTH:
diff --git a/src/com/android/server/telecom/CallEndpointController.java b/src/com/android/server/telecom/CallEndpointController.java
new file mode 100644
index 0000000..c8042ea
--- /dev/null
+++ b/src/com/android/server/telecom/CallEndpointController.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2022, 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.Context;
+import android.bluetooth.BluetoothDevice;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
+import android.telecom.CallEndpointException;
+import android.telecom.Log;
+import com.android.internal.annotations.VisibleForTesting;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides to {@link CallsManager} the service that can request change of CallEndpoint to the
+ * {@link CallAudioManager}. And notify change of CallEndpoint status to {@link CallsManager}
+ */
+public class CallEndpointController extends CallsManagerListenerBase {
+    public static final int CHANGE_TIMEOUT_SEC = 2;
+    public static final int RESULT_REQUEST_SUCCESS = 0;
+    public static final int RESULT_ENDPOINT_DOES_NOT_EXIST = 1;
+    public static final int RESULT_REQUEST_TIME_OUT = 2;
+    public static final int RESULT_ANOTHER_REQUEST = 3;
+    public static final int RESULT_UNSPECIFIED_ERROR = 4;
+
+    private final Context mContext;
+    private final CallsManager mCallsManager;
+    private final HashMap<Integer, Integer> mRouteToTypeMap;
+    private final HashMap<Integer, Integer> mTypeToRouteMap;
+    private final Map<ParcelUuid, String> mBluetoothAddressMap = new HashMap<>();
+    private final Set<CallEndpoint> mAvailableCallEndpoints = new HashSet<>();
+    private CallEndpoint mActiveCallEndpoint;
+    private ParcelUuid mRequestedEndpointId;
+    private CompletableFuture<Integer> mPendingChangeRequest;
+
+    public CallEndpointController(Context context, CallsManager callsManager) {
+        mContext = context;
+        mCallsManager = callsManager;
+
+        mRouteToTypeMap = new HashMap<>(5);
+        mRouteToTypeMap.put(CallAudioState.ROUTE_EARPIECE, CallEndpoint.TYPE_EARPIECE);
+        mRouteToTypeMap.put(CallAudioState.ROUTE_BLUETOOTH, CallEndpoint.TYPE_BLUETOOTH);
+        mRouteToTypeMap.put(CallAudioState.ROUTE_WIRED_HEADSET, CallEndpoint.TYPE_WIRED_HEADSET);
+        mRouteToTypeMap.put(CallAudioState.ROUTE_SPEAKER, CallEndpoint.TYPE_SPEAKER);
+
+        mTypeToRouteMap = new HashMap<>(5);
+        mTypeToRouteMap.put(CallEndpoint.TYPE_EARPIECE, CallAudioState.ROUTE_EARPIECE);
+        mTypeToRouteMap.put(CallEndpoint.TYPE_BLUETOOTH, CallAudioState.ROUTE_BLUETOOTH);
+        mTypeToRouteMap.put(CallEndpoint.TYPE_WIRED_HEADSET, CallAudioState.ROUTE_WIRED_HEADSET);
+        mTypeToRouteMap.put(CallEndpoint.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
+    }
+
+    @VisibleForTesting
+    public CallEndpoint getCurrentCallEndpoint() {
+        return mActiveCallEndpoint;
+    }
+
+    @VisibleForTesting
+    public Set<CallEndpoint> getAvailableEndpoints() {
+        return mAvailableCallEndpoints;
+    }
+
+    public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+        Log.d(this, "requestCallEndpointChange %s", endpoint);
+        int route = mTypeToRouteMap.get(endpoint.getEndpointType());
+        String bluetoothAddress = getBluetoothAddress(endpoint);
+
+        if (findMatchingTypeEndpoint(endpoint.getEndpointType()) == null ||
+                (route == CallAudioState.ROUTE_BLUETOOTH && bluetoothAddress == null)) {
+            callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED,
+                    getErrorResult(RESULT_ENDPOINT_DOES_NOT_EXIST));
+            return;
+        }
+
+        if (mPendingChangeRequest != null && !mPendingChangeRequest.isDone()) {
+            mPendingChangeRequest.complete(RESULT_ANOTHER_REQUEST);
+            mPendingChangeRequest = null;
+            mRequestedEndpointId = null;
+        }
+
+        mPendingChangeRequest = new CompletableFuture<Integer>()
+                .completeOnTimeout(RESULT_REQUEST_TIME_OUT, CHANGE_TIMEOUT_SEC, TimeUnit.SECONDS);
+
+        mPendingChangeRequest.thenAcceptAsync((result) -> {
+            if (result == RESULT_REQUEST_SUCCESS) {
+                callback.send(CallEndpoint.ENDPOINT_OPERATION_SUCCESS, new Bundle());
+            } else {
+                callback.send(CallEndpoint.ENDPOINT_OPERATION_FAILED, getErrorResult(result));
+            }
+        });
+        mRequestedEndpointId = endpoint.getIdentifier();
+        mCallsManager.getCallAudioManager().setAudioRoute(route, bluetoothAddress);
+    }
+
+    private Bundle getErrorResult(int result) {
+        String message;
+        int resultCode;
+        switch (result) {
+            case RESULT_ENDPOINT_DOES_NOT_EXIST:
+                message = "Requested CallEndpoint does not exist";
+                resultCode = CallEndpointException.ERROR_ENDPOINT_DOES_NOT_EXIST;
+                break;
+            case RESULT_REQUEST_TIME_OUT:
+                message = "The operation was not completed on time";
+                resultCode = CallEndpointException.ERROR_REQUEST_TIME_OUT;
+                break;
+            case RESULT_ANOTHER_REQUEST:
+                message = "The operation was canceled by another request";
+                resultCode = CallEndpointException.ERROR_ANOTHER_REQUEST;
+                break;
+            default:
+                message = "The operation has failed due to an unknown or unspecified error";
+                resultCode = CallEndpointException.ERROR_UNSPECIFIED;
+        }
+        CallEndpointException exception = new CallEndpointException(message, resultCode);
+        Bundle extras = new Bundle();
+        extras.putParcelable(CallEndpointException.CHANGE_ERROR, exception);
+        return extras;
+    }
+
+    @VisibleForTesting
+    public String getBluetoothAddress(CallEndpoint endpoint) {
+        return mBluetoothAddressMap.get(endpoint.getIdentifier());
+    }
+
+    private void notifyCallEndpointChange() {
+        if (mActiveCallEndpoint == null) {
+            Log.i(this, "notifyCallEndpointChange, invalid CallEndpoint");
+            return;
+        }
+
+        if (mRequestedEndpointId != null && mPendingChangeRequest != null &&
+                mRequestedEndpointId.equals(mActiveCallEndpoint.getIdentifier())) {
+            mPendingChangeRequest.complete(RESULT_REQUEST_SUCCESS);
+            mPendingChangeRequest = null;
+            mRequestedEndpointId = null;
+        }
+        mCallsManager.updateCallEndpoint(mActiveCallEndpoint);
+
+        Set<Call> calls = mCallsManager.getTrackedCalls();
+        for (Call call : calls) {
+            if (call != null && call.getConnectionService() != null) {
+                call.getConnectionService().onCallEndpointChanged(call, mActiveCallEndpoint);
+            }
+        }
+    }
+
+    private void notifyAvailableCallEndpointsChange() {
+        mCallsManager.updateAvailableCallEndpoints(mAvailableCallEndpoints);
+
+        Set<Call> calls = mCallsManager.getTrackedCalls();
+        for (Call call : calls) {
+            if (call != null && call.getConnectionService() != null) {
+                call.getConnectionService().onAvailableCallEndpointsChanged(call,
+                        mAvailableCallEndpoints);
+            }
+        }
+    }
+
+    private void notifyMuteStateChange(boolean isMuted) {
+        mCallsManager.updateMuteState(isMuted);
+
+        Set<Call> calls = mCallsManager.getTrackedCalls();
+        for (Call call : calls) {
+            if (call != null && call.getConnectionService() != null) {
+                call.getConnectionService().onMuteStateChanged(call, isMuted);
+            }
+        }
+    }
+
+    private void createAvailableCallEndpoints(CallAudioState state) {
+        Set<CallEndpoint> newAvailableEndpoints = new HashSet<>();
+        Map<ParcelUuid, String> newBluetoothDevices = new HashMap<>();
+
+        mRouteToTypeMap.forEach((route, type)->{
+            if ((state.getSupportedRouteMask() & route) != 0) {
+                if (type == CallEndpoint.TYPE_BLUETOOTH) {
+                    for (BluetoothDevice device : state.getSupportedBluetoothDevices()) {
+                        CallEndpoint endpoint = findMatchingBluetoothEndpoint(device);
+                        if (endpoint == null) {
+                            endpoint = new CallEndpoint(
+                                    device.getName() != null ? device.getName() : "",
+                                    CallEndpoint.TYPE_BLUETOOTH);
+                        }
+                        newAvailableEndpoints.add(endpoint);
+                        newBluetoothDevices.put(endpoint.getIdentifier(), device.getAddress());
+
+                        BluetoothDevice activeDevice = state.getActiveBluetoothDevice();
+                        if (state.getRoute() == route && device.equals(activeDevice)) {
+                            mActiveCallEndpoint = endpoint;
+                        }
+                    }
+                } else {
+                    CallEndpoint endpoint = findMatchingTypeEndpoint(type);
+                    if (endpoint == null) {
+                        endpoint = new CallEndpoint(
+                                getEndpointName(type) != null ? getEndpointName(type) : "", type);
+                    }
+                    newAvailableEndpoints.add(endpoint);
+                    if (state.getRoute() == route) {
+                        mActiveCallEndpoint = endpoint;
+                    }
+                }
+            }
+        });
+        mAvailableCallEndpoints.clear();
+        mAvailableCallEndpoints.addAll(newAvailableEndpoints);
+        mBluetoothAddressMap.clear();
+        mBluetoothAddressMap.putAll(newBluetoothDevices);
+    }
+
+    private CallEndpoint findMatchingTypeEndpoint(int targetType) {
+        for (CallEndpoint endpoint : mAvailableCallEndpoints) {
+            if (endpoint.getEndpointType() == targetType) {
+                return endpoint;
+            }
+        }
+        return null;
+    }
+
+    private CallEndpoint findMatchingBluetoothEndpoint(BluetoothDevice device) {
+        final String targetAddress = device.getAddress();
+        if (targetAddress != null) {
+            for (CallEndpoint endpoint : mAvailableCallEndpoints) {
+                final String address = mBluetoothAddressMap.get(endpoint.getIdentifier());
+                if (targetAddress.equals(address)) {
+                    return endpoint;
+                }
+            }
+        }
+        return null;
+    }
+
+    private boolean isAvailableEndpointChanged(CallAudioState oldState, CallAudioState newState) {
+        if (oldState == null) {
+            return true;
+        }
+        if ((oldState.getSupportedRouteMask() ^ newState.getSupportedRouteMask()) != 0) {
+            return true;
+        }
+        if (oldState.getSupportedBluetoothDevices().size() !=
+                newState.getSupportedBluetoothDevices().size()) {
+            return true;
+        }
+        for (BluetoothDevice device : newState.getSupportedBluetoothDevices()) {
+            if (!oldState.getSupportedBluetoothDevices().contains(device)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isEndpointChanged(CallAudioState oldState, CallAudioState newState) {
+        if (oldState == null) {
+            return true;
+        }
+        if (oldState.getRoute() != newState.getRoute()) {
+            return true;
+        }
+        if (newState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
+            return !oldState.getActiveBluetoothDevice().equals(newState.getActiveBluetoothDevice());
+        }
+        return false;
+    }
+
+    private boolean isMuteStateChanged(CallAudioState oldState, CallAudioState newState) {
+        if (oldState == null) {
+            return true;
+        }
+        return oldState.isMuted() != newState.isMuted();
+    }
+
+    private CharSequence getEndpointName(int endpointType) {
+        switch (endpointType) {
+            case CallEndpoint.TYPE_EARPIECE:
+                return mContext.getText(R.string.callendpoint_name_earpiece);
+            case CallEndpoint.TYPE_BLUETOOTH:
+                return mContext.getText(R.string.callendpoint_name_bluetooth);
+            case CallEndpoint.TYPE_WIRED_HEADSET:
+                return mContext.getText(R.string.callendpoint_name_wiredheadset);
+            case CallEndpoint.TYPE_SPEAKER:
+                return mContext.getText(R.string.callendpoint_name_speaker);
+            case CallEndpoint.TYPE_STREAMING:
+                return mContext.getText(R.string.callendpoint_name_streaming);
+            default:
+                return mContext.getText(R.string.callendpoint_name_unknown);
+        }
+    }
+
+    @Override
+    public void onCallAudioStateChanged(CallAudioState oldState, CallAudioState newState) {
+        Log.i(this, "onCallAudioStateChanged, audioState: %s -> %s", oldState, newState);
+
+        if (newState == null) {
+            Log.i(this, "onCallAudioStateChanged, invalid audioState");
+            return;
+        }
+
+        createAvailableCallEndpoints(newState);
+
+        boolean isforce = true;
+        if (isAvailableEndpointChanged(oldState, newState)) {
+            notifyAvailableCallEndpointsChange();
+            isforce = false;
+        }
+
+        if (isEndpointChanged(oldState, newState)) {
+            notifyCallEndpointChange();
+            isforce = false;
+        }
+
+        if (isMuteStateChanged(oldState, newState)) {
+            notifyMuteStateChange(newState.isMuted());
+            isforce = false;
+        }
+
+        if (isforce) {
+            notifyAvailableCallEndpointsChange();
+            notifyCallEndpointChange();
+            notifyMuteStateChange(newState.isMuted());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallEndpointControllerFactory.java b/src/com/android/server/telecom/CallEndpointControllerFactory.java
new file mode 100644
index 0000000..a9b03c3
--- /dev/null
+++ b/src/com/android/server/telecom/CallEndpointControllerFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022, 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.Context;
+
+/**
+ * Abstracts out creation of CallEndpointController for unit test purposes.
+ */
+public interface CallEndpointControllerFactory {
+    CallEndpointController create(Context context, TelecomSystem.SyncRoot lock,
+            CallsManager callsManager);
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index d3c374b..75b3864 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -66,6 +66,7 @@
 import android.os.Looper;
 import android.os.PersistableBundle;
 import android.os.Process;
+import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.os.SystemVibrator;
 import android.os.Trace;
@@ -77,6 +78,7 @@
 import android.provider.Settings;
 import android.sysprop.TelephonyProperties;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.CallScreeningService;
 import android.telecom.CallerInfo;
 import android.telecom.Conference;
@@ -174,6 +176,9 @@
         void onIncomingCallAnswered(Call call);
         void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
         void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
+        void onCallEndpointChanged(CallEndpoint callEndpoint);
+        void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints);
+        void onMuteStateChanged(boolean isMuted);
         void onRingbackRequested(Call call, boolean ringback);
         void onIsConferencedChanged(Call call);
         void onIsVoipAudioModeChanged(Call call);
@@ -376,6 +381,7 @@
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final EmergencyCallHelper mEmergencyCallHelper;
     private final RoleManagerAdapter mRoleManagerAdapter;
+    private final CallEndpointController mCallEndpointController;
 
     private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
             new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -496,7 +502,8 @@
             InCallControllerFactory inCallControllerFactory,
             CallDiagnosticServiceController callDiagnosticServiceController,
             RoleManagerAdapter roleManagerAdapter,
-            ToastFactory toastFactory) {
+            ToastFactory toastFactory,
+            CallEndpointControllerFactory callEndpointControllerFactory) {
         mContext = context;
         mLock = lock;
         mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
@@ -550,6 +557,7 @@
         mInCallController = inCallControllerFactory.create(context, mLock, this,
                 systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
                 emergencyCallHelper);
+        mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this);
         mCallDiagnosticServiceController = callDiagnosticServiceController;
         mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory);
         mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
@@ -581,6 +589,7 @@
         mListeners.add(statusBarNotifier);
         mListeners.add(mCallLogManager);
         mListeners.add(mInCallController);
+        mListeners.add(mCallEndpointController);
         mListeners.add(mCallDiagnosticServiceController);
         mListeners.add(mCallAudioManager);
         mListeners.add(mCallRecordingTonePlayer);
@@ -1193,6 +1202,10 @@
         return mInCallController;
     }
 
+    public CallEndpointController getCallEndpointController() {
+        return mCallEndpointController;
+    }
+
     EmergencyCallHelper getEmergencyCallHelper() {
         return mEmergencyCallHelper;
     }
@@ -3010,6 +3023,14 @@
         mCallAudioManager.setAudioRoute(route, bluetoothAddress);
     }
 
+    /**
+      * Called by the in-call UI to change the CallEndpoint
+      */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+        mCallEndpointController.requestCallEndpointChange(endpoint, callback);
+    }
+
     /** Called by the in-call UI to turn the proximity sensor on. */
     void turnOnProximitySensor() {
         mProximitySensorManager.turnOn();
@@ -3068,6 +3089,30 @@
         }
     }
 
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void updateCallEndpoint(CallEndpoint callEndpoint) {
+        Log.v(this, "updateCallEndpoint");
+        for (CallsManagerListener listener : mListeners) {
+            listener.onCallEndpointChanged(callEndpoint);
+        }
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void updateAvailableCallEndpoints(Set<CallEndpoint> availableCallEndpoints) {
+        Log.v(this, "updateAvailableCallEndpoints");
+        for (CallsManagerListener listener : mListeners) {
+            listener.onAvailableCallEndpointsChanged(availableCallEndpoints);
+        }
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void updateMuteState(boolean isMuted) {
+        Log.v(this, "updateMuteState");
+        for (CallsManagerListener listener : mListeners) {
+            listener.onMuteStateChanged(isMuted);
+        }
+    }
+
     /**
      * Called when disconnect tone is started or stopped, including any InCallTone
      * after disconnected call.
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index 55c7b53..85e6d65 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -18,7 +18,9 @@
 
 import android.telecom.AudioState;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.VideoProfile;
+import java.util.Set;
 
 /**
  * Provides a default implementation for listeners of CallsManager.
@@ -57,6 +59,18 @@
     }
 
     @Override
+    public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+    }
+
+    @Override
+    public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
+    }
+
+    @Override
+    public void onMuteStateChanged(boolean isMuted) {
+    }
+
+    @Override
     public void onRingbackRequested(Call call, boolean ringback) {
     }
 
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 0243b67..729aa0e 100755
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -29,8 +29,10 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.UserHandle;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.CallScreeningService;
 import android.telecom.Connection;
 import android.telecom.ConnectionRequest;
@@ -723,6 +725,26 @@
         }
 
         @Override
+        public void requestCallEndpointChange(String callId, CallEndpoint endpoint,
+                ResultReceiver callback, Session.Info sessionInfo) {
+            Log.startSession(sessionInfo, "CSW.rCEC", mPackageAbbreviation);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    logIncoming("requestCallEndpointChange %s %s", callId,
+                            endpoint.getEndpointName());
+                    mCallsManager.requestCallEndpointChange(endpoint, callback);
+                }
+            } 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", mPackageAbbreviation);
@@ -1673,6 +1695,54 @@
         }
     }
 
+    /** @see IConnectionService#onCallEndpointChanged(String, CallEndpoint, Session.Info) */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void onCallEndpointChanged(Call activeCall, CallEndpoint callEndpoint) {
+        final String callId = mCallIdMapper.getCallId(activeCall);
+        if (callId != null && isServiceValid("onCallEndpointChanged")) {
+            try {
+                logOutgoing("onCallEndpointChanged %s %s", callId, callEndpoint);
+                mServiceInterface.onCallEndpointChanged(callId, callEndpoint,
+                        Log.getExternalSession(TELECOM_ABBREVIATION));
+            } catch (RemoteException e) {
+                Log.d(this, "Remote exception calling onCallEndpointChanged");
+            }
+        }
+    }
+
+    /** @see IConnectionService#onAvailableCallEndpointsChanged(String, List, Session.Info) */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void onAvailableCallEndpointsChanged(Call activeCall,
+            Set<CallEndpoint> availableCallEndpoints) {
+        final String callId = mCallIdMapper.getCallId(activeCall);
+        if (callId != null && isServiceValid("onAvailableCallEndpointsChanged")) {
+            try {
+                logOutgoing("onAvailableCallEndpointsChanged %s", callId);
+                List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
+                mServiceInterface.onAvailableCallEndpointsChanged(callId, availableEndpoints,
+                        Log.getExternalSession(TELECOM_ABBREVIATION));
+            } catch (RemoteException e) {
+                Log.d(this,
+                        "Remote exception calling onAvailableCallEndpointsChanged");
+            }
+        }
+    }
+
+    /** @see IConnectionService#onMuteStateChanged(String, boolean, Session.Info) */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void onMuteStateChanged(Call activeCall, boolean isMuted) {
+        final String callId = mCallIdMapper.getCallId(activeCall);
+        if (callId != null && isServiceValid("onMuteStateChanged")) {
+            try {
+                logOutgoing("onMuteStateChanged %s %s", callId, isMuted);
+                mServiceInterface.onMuteStateChanged(callId, isMuted,
+                        Log.getExternalSession(TELECOM_ABBREVIATION));
+            } catch (RemoteException e) {
+                Log.d(this, "Remote exception calling onMuteStateChanged");
+            }
+        }
+    }
+
     /** @see IConnectionService#onUsingAlternativeUi(String, boolean, Session.Info) */
     @VisibleForTesting
     public void onUsingAlternativeUi(Call activeCall, boolean isUsingAlternativeUi) {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 0fda5f8..f52d489 100755
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -19,6 +19,8 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.telecom.CallEndpoint;
 import android.telecom.Log;
 import android.telecom.PhoneAccountHandle;
 
@@ -396,6 +398,23 @@
     }
 
     @Override
+    public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) {
+        try {
+            Log.startSession(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, mOwnerPackageAbbreviation);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    mCallsManager.requestCallEndpointChange(endpoint, callback);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
     public void enterBackgroundAudioProcessing(String callId) {
         try {
             Log.startSession(LogUtils.Sessions.ICA_ENTER_AUDIO_PROCESSING,
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index d830dfc..7a38583 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -52,6 +52,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.ConnectionService;
 import android.telecom.InCallService;
 import android.telecom.Log;
@@ -1379,6 +1380,49 @@
     }
 
     @Override
+    public void onCallEndpointChanged(CallEndpoint callEndpoint) {
+        if (!mInCallServices.isEmpty()) {
+            Log.i(this, "Calling onCallEndpointChanged");
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.onCallEndpointChanged(callEndpoint);
+                } catch (RemoteException ignored) {
+                    Log.d(this, "Remote exception calling onCallEndpointChanged");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
+        if (!mInCallServices.isEmpty()) {
+            Log.i(this, "Calling onAvailableCallEndpointsChanged");
+            List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.onAvailableCallEndpointsChanged(availableEndpoints);
+                } catch (RemoteException ignored) {
+                    Log.d(this, "Remote exception calling onAvailableCallEndpointsChanged");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onMuteStateChanged(boolean isMuted) {
+        if (!mInCallServices.isEmpty()) {
+            Log.i(this, "Calling onMuteStateChanged");
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.onMuteStateChanged(isMuted);
+                } catch (RemoteException ignored) {
+                    Log.d(this, "Remote exception calling onMuteStateChanged");
+                }
+            }
+        }
+    }
+
+    @Override
     public void onCanAddCallChanged(boolean canAddCall) {
         if (!mInCallServices.isEmpty()) {
             Log.i(this, "onCanAddCallChanged : %b", canAddCall);
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 237f039..b6c9925 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -269,6 +269,15 @@
                 }
             };
 
+            CallEndpointControllerFactory callEndpointControllerFactory =
+                    new CallEndpointControllerFactory() {
+                @Override
+                public CallEndpointController create(Context context, SyncRoot lock,
+                        CallsManager callsManager) {
+                    return new CallEndpointController(context, callsManager);
+                }
+            };
+
             CallDiagnosticServiceController callDiagnosticServiceController =
                     new CallDiagnosticServiceController(
                             new CallDiagnosticServiceController.ContextProxy() {
@@ -348,7 +357,8 @@
                     inCallControllerFactory,
                     callDiagnosticServiceController,
                     roleManagerAdapter,
-                    toastFactory);
+                    toastFactory,
+                    callEndpointControllerFactory);
 
             mIncomingCallNotifier = incomingCallNotifier;
             incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
diff --git a/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
new file mode 100644
index 0000000..b15d3c2
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallEndpointControllerTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2022, 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.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
+import android.test.mock.MockContext;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallEndpointController;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceWrapper;
+
+import org.junit.Before;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class CallEndpointControllerTest extends TelecomTestCase {
+    private static final BluetoothDevice bluetoothDevice1 =
+            BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
+    private static final BluetoothDevice bluetoothDevice2 =
+            BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:02");
+    private static final Collection<BluetoothDevice> availableBluetooth1 =
+            Arrays.asList(bluetoothDevice1, bluetoothDevice2);
+    private static final Collection<BluetoothDevice> availableBluetooth2 =
+            Arrays.asList(bluetoothDevice1);
+
+    private static final CallAudioState audioState1 = new CallAudioState(false,
+            CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+    private static final CallAudioState audioState2 = new CallAudioState(false,
+            CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+            availableBluetooth1);
+    private static final CallAudioState audioState3 = new CallAudioState(false,
+            CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL, bluetoothDevice2,
+            availableBluetooth1);
+    private static final CallAudioState audioState4 = new CallAudioState(false,
+            CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL, bluetoothDevice1,
+            availableBluetooth2);
+    private static final CallAudioState audioState5 = new CallAudioState(true,
+            CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_ALL, null, availableBluetooth1);
+    private static final CallAudioState audioState6 = new CallAudioState(false,
+            CallAudioState.ROUTE_EARPIECE, CallAudioState.ROUTE_EARPIECE, null,
+            availableBluetooth1);
+
+    private CallEndpointController mCallEndpointController;
+
+    @Mock private CallsManager mCallsManager;
+    @Mock private Call mCall;
+    @Mock private ConnectionServiceWrapper mConnectionService;
+    @Mock private CallAudioManager mCallAudioManager;
+    @Mock private MockContext mMockContext;
+    @Mock private ResultReceiver mResultReceiver;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCallEndpointController = new CallEndpointController(mMockContext, mCallsManager);
+        doReturn(new HashSet<>(Arrays.asList(mCall))).when(mCallsManager).getTrackedCalls();
+        doReturn(mConnectionService).when(mCall).getConnectionService();
+        doReturn(mCallAudioManager).when(mCallsManager).getCallAudioManager();
+        when(mMockContext.getText(R.string.callendpoint_name_earpiece)).thenReturn("Earpiece");
+        when(mMockContext.getText(R.string.callendpoint_name_bluetooth)).thenReturn("Bluetooth");
+        when(mMockContext.getText(R.string.callendpoint_name_wiredheadset))
+                .thenReturn("Wired headset");
+        when(mMockContext.getText(R.string.callendpoint_name_speaker)).thenReturn("Speaker");
+        when(mMockContext.getText(R.string.callendpoint_name_streaming)).thenReturn("External");
+        when(mMockContext.getText(R.string.callendpoint_name_unknown)).thenReturn("Unknown");
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testCurrentEndpointChangedToBluetooth() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(audioState1, audioState2);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+        String bluetoothAddress = mCallEndpointController.getBluetoothAddress(endpoint);
+
+        // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+        assertEquals(5, availableEndpoints.size());
+        // type of current CallEndpoint is Bluetooth
+        assertEquals(CallEndpoint.TYPE_BLUETOOTH, endpoint.getEndpointType());
+        assertEquals(bluetoothDevice1.getAddress(), bluetoothAddress);
+
+        verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+        verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+        verify(mCallsManager, never()).updateAvailableCallEndpoints(any());
+        verify(mConnectionService, never()).onAvailableCallEndpointsChanged(any(), any());
+        verify(mCallsManager, never()).updateMuteState(anyBoolean());
+        verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+    }
+
+    @Test
+    public void testCurrentEndpointChangedBetweenBluetooth() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(audioState2, audioState3);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+        String bluetoothAddress = mCallEndpointController.getBluetoothAddress(endpoint);
+
+        // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+        assertEquals(5, availableEndpoints.size());
+        // type of current CallEndpoint is Bluetooth
+        assertEquals(CallEndpoint.TYPE_BLUETOOTH, endpoint.getEndpointType());
+        assertEquals(bluetoothDevice2.getAddress(), bluetoothAddress);
+
+        verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+        verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+        verify(mCallsManager, never()).updateAvailableCallEndpoints(any());
+        verify(mConnectionService, never()).onAvailableCallEndpointsChanged(any(), any());
+        verify(mCallsManager, never()).updateMuteState(anyBoolean());
+        verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+    }
+
+    @Test
+    public void testAvailableEndpointChanged() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(audioState1, audioState6);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+        // Only Earpiece is available
+        assertEquals(1, availableEndpoints.size());
+        // type of current CallEndpoint is Earpiece
+        assertEquals(CallEndpoint.TYPE_EARPIECE, endpoint.getEndpointType());
+        assertTrue(availableEndpoints.contains(endpoint));
+
+        verify(mCallsManager, never()).updateCallEndpoint(any());
+        verify(mConnectionService, never()).onCallEndpointChanged(any(), any());
+        verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+        verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+                eq(availableEndpoints));
+        verify(mCallsManager, never()).updateMuteState(anyBoolean());
+        verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+    }
+
+    @Test
+    public void testAvailableBluetoothEndpointChanged() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(audioState2, audioState4);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+        String bluetoothAddress = mCallEndpointController.getBluetoothAddress(endpoint);
+
+        // Earpiece, Wired headset, Speaker and one Bluetooth endpoint is available
+        assertEquals(4, availableEndpoints.size());
+        // type of current CallEndpoint is Bluetooth
+        assertEquals(CallEndpoint.TYPE_BLUETOOTH, endpoint.getEndpointType());
+        assertEquals(bluetoothDevice1.getAddress(), bluetoothAddress);
+
+        verify(mCallsManager, never()).updateCallEndpoint(any());
+        verify(mConnectionService, never()).onCallEndpointChanged(any(), any());
+        verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+        verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+                eq(availableEndpoints));
+        verify(mCallsManager, never()).updateMuteState(anyBoolean());
+        verify(mConnectionService, never()).onMuteStateChanged(any(), anyBoolean());
+    }
+
+    @Test
+    public void testMuteStateChanged() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(audioState1, audioState5);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+        // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+        assertEquals(5, availableEndpoints.size());
+        // type of current CallEndpoint is Earpiece
+        assertEquals(CallEndpoint.TYPE_EARPIECE, endpoint.getEndpointType());
+
+        verify(mCallsManager, never()).updateCallEndpoint(any());
+        verify(mConnectionService, never()).onCallEndpointChanged(any(), any());
+        verify(mCallsManager, never()).updateAvailableCallEndpoints(any());
+        verify(mConnectionService, never()).onAvailableCallEndpointsChanged(any(), any());
+        verify(mCallsManager).updateMuteState(eq(true));
+        verify(mConnectionService, times(1)).onMuteStateChanged(eq(mCall), eq(true));
+    }
+
+    @Test
+    public void testNotifyForcely() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(audioState1, audioState1);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        Set<CallEndpoint> availableEndpoints = mCallEndpointController.getAvailableEndpoints();
+
+        // Earpiece, Wired headset, Speaker and two Bluetooth endpoint is available
+        assertEquals(5, availableEndpoints.size());
+        // type of current CallEndpoint is Earpiece
+        assertEquals(CallEndpoint.TYPE_EARPIECE, endpoint.getEndpointType());
+
+        verify(mCallsManager).updateCallEndpoint(eq(endpoint));
+        verify(mConnectionService, times(1)).onCallEndpointChanged(eq(mCall), eq(endpoint));
+        verify(mCallsManager).updateAvailableCallEndpoints(eq(availableEndpoints));
+        verify(mConnectionService, times(1)).onAvailableCallEndpointsChanged(eq(mCall),
+                eq(availableEndpoints));
+        verify(mCallsManager).updateMuteState(eq(false));
+        verify(mConnectionService, times(1)).onMuteStateChanged(eq(mCall), eq(false));
+    }
+
+    @Test
+    public void testEndpointChangeRequest() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(null, audioState1);
+        CallEndpoint endpoint1 = mCallEndpointController.getCurrentCallEndpoint();
+
+        mCallEndpointController.onCallAudioStateChanged(audioState1, audioState2);
+        CallEndpoint endpoint2 = mCallEndpointController.getCurrentCallEndpoint();
+
+        mCallEndpointController.requestCallEndpointChange(endpoint1, mResultReceiver);
+        verify(mCallAudioManager).setAudioRoute(eq(CallAudioState.ROUTE_EARPIECE), eq(null));
+
+        mCallEndpointController.requestCallEndpointChange(endpoint2, mResultReceiver);
+        verify(mCallAudioManager).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+                eq(bluetoothDevice1.getAddress()));
+    }
+
+    @Test
+    public void testEndpointChangeRequest_EndpointDoesNotExist() throws Exception {
+        mCallEndpointController.onCallAudioStateChanged(null, audioState2);
+        CallEndpoint endpoint = mCallEndpointController.getCurrentCallEndpoint();
+        mCallEndpointController.onCallAudioStateChanged(audioState2, audioState6);
+
+        mCallEndpointController.requestCallEndpointChange(endpoint, mResultReceiver);
+        verify(mCallAudioManager, never()).setAudioRoute(eq(CallAudioState.ROUTE_BLUETOOTH),
+                eq(bluetoothDevice1.getAddress()));
+        verify(mResultReceiver).send(eq(CallEndpoint.ENDPOINT_OPERATION_FAILED), any());
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 526ab69..8fe64e2 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -72,6 +72,8 @@
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.CallEndpointController;
+import com.android.server.telecom.CallEndpointControllerFactory;
 import com.android.server.telecom.CallDiagnosticServiceController;
 import com.android.server.telecom.CallState;
 import com.android.server.telecom.CallerInfoLookupHelper;
@@ -199,6 +201,8 @@
     @Mock private AudioProcessingNotification mAudioProcessingNotification;
     @Mock private InCallControllerFactory mInCallControllerFactory;
     @Mock private InCallController mInCallController;
+    @Mock private CallEndpointControllerFactory mCallEndpointControllerFactory;
+    @Mock private CallEndpointController mCallEndpointController;
     @Mock private ConnectionServiceFocusManager mConnectionSvrFocusMgr;
     @Mock private CallAudioRouteStateMachine mCallAudioRouteStateMachine;
     @Mock private CallAudioRouteStateMachine.Factory mCallAudioRouteStateMachineFactory;
@@ -225,6 +229,8 @@
                 mProximitySensorManager);
         when(mInCallControllerFactory.create(any(), any(), any(), any(), any(), any(),
                 any())).thenReturn(mInCallController);
+        when(mCallEndpointControllerFactory.create(any(), any(), any())).thenReturn(
+                mCallEndpointController);
         when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(),
                 anyInt())).thenReturn(mCallAudioRouteStateMachine);
         when(mCallAudioModeStateMachineFactory.create(any(), any()))
@@ -266,7 +272,8 @@
                 mInCallControllerFactory,
                 mCallDiagnosticServiceController,
                 mRoleManagerAdapter,
-                mToastFactory);
+                mToastFactory,
+                mCallEndpointControllerFactory);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 6e6646f..c8da78c 100755
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -34,6 +34,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.CallScreeningService;
 import android.telecom.Conference;
 import android.telecom.Connection;
@@ -343,6 +344,18 @@
                 throws RemoteException { }
 
         @Override
+        public void onCallEndpointChanged(String callId, CallEndpoint callEndpoint,
+                Session.Info sessionInfo) { }
+
+        @Override
+        public void onAvailableCallEndpointsChanged(String callId,
+                List<CallEndpoint> availableCallEndpoints, Session.Info sessionInfo) { }
+
+        @Override
+        public void onMuteStateChanged(String callId, boolean isMuted,
+                Session.Info sessionInfo) { }
+
+        @Override
         public void onUsingAlternativeUi(String activeCallId, boolean usingAlternativeUi,
                 Session.Info info) throws RemoteException { }
 
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index d114cb8..88b5bb5 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -26,9 +26,11 @@
 import android.os.IInterface;
 import android.os.RemoteException;
 import android.telecom.CallAudioState;
+import android.telecom.CallEndpoint;
 import android.telecom.ParcelableCall;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -105,6 +107,15 @@
         }
 
         @Override
+        public void onCallEndpointChanged(CallEndpoint callEndpoint) {}
+
+        @Override
+        public void onAvailableCallEndpointsChanged(List<CallEndpoint> availableCallEndpoints) {}
+
+        @Override
+        public void onMuteStateChanged(boolean isMuted) {}
+
+        @Override
         public void bringToForeground(boolean showDialpad) throws RemoteException {
             mBringToForeground = true;
             mShowDialpad = showDialpad;