Add connection service focus manager

This changed introduce the ConnectionServiceFocusManager to maintain the
focus status of ConnectionService. When a ConnectionService gained the
focus, it can request the call resource. Also, the ConnectionService
should release the call resource when it lost the focus.

design doc: go/android-telecom-3p-enhancements

Bug: 69651192
Test: unit test

Change-Id: Iea7b4bfd896753ea9d6f399ba341e36150e4e621
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index c516274..47195cb 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -74,7 +74,8 @@
  *  connected etc).
  */
 @VisibleForTesting
-public class Call implements CreateConnectionResponse, EventManager.Loggable {
+public class Call implements CreateConnectionResponse, EventManager.Loggable,
+        ConnectionServiceFocusManager.CallFocus {
     public final static String CALL_ID_UNKNOWN = "-1";
     public final static long DATA_USAGE_NOT_SET = -1;
 
@@ -744,6 +745,11 @@
         return sb.toString();
     }
 
+    @Override
+    public ConnectionServiceFocusManager.ConnectionServiceFocus getConnectionServiceWrapper() {
+        return mConnectionService;
+    }
+
     @VisibleForTesting
     public int getState() {
         return mState;
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
new file mode 100644
index 0000000..60fbbd1
--- /dev/null
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2017 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.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class ConnectionServiceFocusManager {
+
+    private static final String TAG = "ConnectionServiceFocusManager";
+
+    /**
+     * Interface used by ConnectionServiceFocusManager to communicate with
+     * {@link ConnectionServiceWrapper}.
+     */
+    public interface ConnectionServiceFocus {
+        /**
+         * Notifies the {@link android.telecom.ConnectionService} that it has lose the connection
+         * service focus. It should release all call resource i.e camera, audio once it lost the
+         * focus.
+         */
+        void connectionServiceFocusLost();
+
+        /**
+         * Notifies the {@link android.telecom.ConnectionService} that it has gain the connection
+         * service focus. It can request the call resource i.e camera, audio as they expected to be
+         * free at the moment.
+         */
+        void connectionServiceFocusGained();
+
+        /**
+         * Sets the ConnectionServiceFocusListener.
+         *
+         * @see {@link ConnectionServiceFocusListener}.
+         */
+        void setConnectionServiceFocusListener(ConnectionServiceFocusListener listener);
+    }
+
+    /**
+     * Interface used to receive the changed of {@link android.telecom.ConnectionService} that
+     * ConnectionServiceFocusManager cares about.
+     */
+    public interface ConnectionServiceFocusListener {
+        /**
+         * Calls when {@link android.telecom.ConnectionService} has released the call resource. This
+         * usually happen after the {@link android.telecom.ConnectionService} lost the focus.
+         *
+         * @param connectionServiceFocus the {@link android.telecom.ConnectionService} that released
+         * the call resources.
+         */
+        void onConnectionServiceReleased(ConnectionServiceFocus connectionServiceFocus);
+
+        /**
+         * Calls when {@link android.telecom.ConnectionService} is disconnected.
+         *
+         * @param connectionServiceFocus the {@link android.telecom.ConnectionService} which is
+         * disconnected.
+         */
+        void onConnectionServiceDeath(ConnectionServiceFocus connectionServiceFocus);
+    }
+
+    /**
+     * Interface define to expose few information of {@link Call} that ConnectionServiceFocusManager
+     * cares about.
+     */
+    public interface CallFocus {
+        /**
+         * Returns the ConnectionService associated with the call.
+         */
+        ConnectionServiceFocus getConnectionServiceWrapper();
+
+        /**
+         * Returns the state of the call.
+         *
+         * @see {@link CallState}
+         */
+        int getState();
+    }
+
+    /** Interface define a call back for focus request event. */
+    public interface RequestFocusCallback {
+        /**
+         * Invokes after the focus request is done.
+         *
+         * @param call the call associated with the focus request.
+         */
+        void onRequestFocusDone(CallFocus call);
+    }
+
+    /**
+     * Interface define to allow the ConnectionServiceFocusManager to communicate with
+     * {@link CallsManager}.
+     */
+    public interface CallsManagerRequester {
+        /**
+         * Requests {@link CallsManager} to disconnect a {@link ConnectionServiceFocus}. This
+         * usually happen when the connection service doesn't respond to focus lost event.
+         */
+        void releaseConnectionService(ConnectionServiceFocus connectionService);
+
+        /**
+         * Sets the {@link com.android.server.telecom.CallsManager.CallsManagerListener} to listen
+         * the call event that ConnectionServiceFocusManager cares about.
+         */
+        void setCallsManagerListener(CallsManager.CallsManagerListener listener);
+    }
+
+    private static final int[] PRIORITY_FOCUS_CALL_STATE = new int[] {
+            CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING
+    };
+
+    private static final int MSG_REQUEST_FOCUS = 1;
+    private static final int MSG_RELEASE_CONNECTION_FOCUS = 2;
+    private static final int MSG_RELEASE_FOCUS_TIMEOUT = 3;
+    private static final int MSG_CONNECTION_SERVICE_DEATH = 4;
+    private static final int MSG_ADD_CALL = 5;
+    private static final int MSG_REMOVE_CALL = 6;
+    private static final int MSG_CALL_STATE_CHANGED = 7;
+
+    @VisibleForTesting
+    public static final int RELEASE_FOCUS_TIMEOUT_MS = 5000;
+
+    private final List<CallFocus> mCalls;
+
+    private final CallsManagerListenerBase mCallsManagerListener =
+            new CallsManagerListenerBase() {
+                @Override
+                public void onCallAdded(Call call) {
+                    if (callShouldBeIgnored(call)) {
+                        return;
+                    }
+
+                    mEventHandler.obtainMessage(MSG_ADD_CALL, call).sendToTarget();
+                }
+
+                @Override
+                public void onCallRemoved(Call call) {
+                    if (callShouldBeIgnored(call)) {
+                        return;
+                    }
+
+                    mEventHandler.obtainMessage(MSG_REMOVE_CALL, call).sendToTarget();
+                }
+
+                @Override
+                public void onCallStateChanged(Call call, int oldState, int newState) {
+                    if (callShouldBeIgnored(call)) {
+                        return;
+                    }
+
+                    mEventHandler.obtainMessage(MSG_CALL_STATE_CHANGED, oldState, newState, call)
+                            .sendToTarget();
+                }
+
+                @Override
+                public void onExternalCallChanged(Call call, boolean isExternalCall) {
+                    if (isExternalCall) {
+                        mEventHandler.obtainMessage(MSG_REMOVE_CALL, call).sendToTarget();
+                    } else {
+                        mEventHandler.obtainMessage(MSG_ADD_CALL, call).sendToTarget();
+                    }
+                }
+
+                boolean callShouldBeIgnored(Call call) {
+                    return call.isExternalCall();
+                }
+            };
+
+    private final ConnectionServiceFocusListener mConnectionServiceFocusListener =
+            new ConnectionServiceFocusListener() {
+        @Override
+        public void onConnectionServiceReleased(ConnectionServiceFocus connectionServiceFocus) {
+            mEventHandler.obtainMessage(MSG_RELEASE_CONNECTION_FOCUS, connectionServiceFocus)
+                    .sendToTarget();
+        }
+
+        @Override
+        public void onConnectionServiceDeath(ConnectionServiceFocus connectionServiceFocus) {
+            mEventHandler.obtainMessage(MSG_CONNECTION_SERVICE_DEATH, connectionServiceFocus)
+                    .sendToTarget();
+        }
+    };
+
+    private ConnectionServiceFocus mCurrentFocus;
+    private CallFocus mCurrentFocusCall;
+    private CallsManagerRequester mCallsManagerRequester;
+    private FocusRequest mCurrentFocusRequest;
+    private FocusManagerHandler mEventHandler;
+
+    public ConnectionServiceFocusManager(
+            CallsManagerRequester callsManagerRequester, Looper looper) {
+        mCallsManagerRequester = callsManagerRequester;
+        mCallsManagerRequester.setCallsManagerListener(mCallsManagerListener);
+        mEventHandler = new FocusManagerHandler(looper);
+        mCalls = new ArrayList<>();
+    }
+
+    /**
+     * Requests the call focus for the given call. The {@code callback} will be invoked once
+     * the request is done.
+     * @param focus the call need to be focus.
+     * @param callback the callback associated with this request.
+     */
+    public void requestFocus(CallFocus focus, RequestFocusCallback callback) {
+        mEventHandler.obtainMessage(
+                MSG_REQUEST_FOCUS, new FocusRequest(focus, callback)).sendToTarget();
+    }
+
+    /**
+     * Returns the current focus call. The {@link android.telecom.ConnectionService} of the focus
+     * call is the current connection service focus. Also the state of the focus call must be one
+     * of {@link #PRIORITY_FOCUS_CALL_STATE}.
+     */
+    public CallFocus getCurrentFocusCall() {
+        return mCurrentFocusCall;
+    }
+
+    /** Returns the current connection service focus. */
+    public ConnectionServiceFocus getCurrentFocusConnectionService() {
+        return mCurrentFocus;
+    }
+
+    @VisibleForTesting
+    public Handler getHandler() {
+        return mEventHandler;
+    }
+
+    @VisibleForTesting
+    public List<CallFocus> getAllCall() { return mCalls; }
+
+    private void updateConnectionServiceFocus(ConnectionServiceFocus connSvrFocus) {
+        if (!Objects.equals(mCurrentFocus, connSvrFocus)) {
+            if (connSvrFocus != null) {
+                connSvrFocus.setConnectionServiceFocusListener(mConnectionServiceFocusListener);
+                connSvrFocus.connectionServiceFocusGained();
+            }
+            mCurrentFocus = connSvrFocus;
+        }
+    }
+
+    private void updateCurrentFocusCall() {
+        mCurrentFocusCall = null;
+
+        if (mCurrentFocus == null) {
+            return;
+        }
+
+        List<CallFocus> calls = mCalls
+                .stream()
+                .filter(call -> mCurrentFocus.equals(call.getConnectionServiceWrapper()))
+                .collect(Collectors.toList());
+
+        for (int i = 0; i < PRIORITY_FOCUS_CALL_STATE.length; i++) {
+            for (CallFocus call : calls) {
+                if (call.getState() == PRIORITY_FOCUS_CALL_STATE[i]) {
+                    mCurrentFocusCall = call;
+                    return;
+                }
+            }
+        }
+    }
+
+    private void onRequestFocusDone(FocusRequest focusRequest) {
+        if (focusRequest.callback != null) {
+            focusRequest.callback.onRequestFocusDone(focusRequest.call);
+        }
+    }
+
+    private void handleRequestFocus(FocusRequest focusRequest) {
+        if (mCurrentFocus == null
+                || mCurrentFocus.equals(focusRequest.call.getConnectionServiceWrapper())) {
+            updateConnectionServiceFocus(focusRequest.call.getConnectionServiceWrapper());
+            updateCurrentFocusCall();
+            onRequestFocusDone(focusRequest);
+        } else {
+            mCurrentFocus.connectionServiceFocusLost();
+            mCurrentFocusRequest = focusRequest;
+            Message msg = mEventHandler.obtainMessage(MSG_RELEASE_FOCUS_TIMEOUT);
+            msg.obj = focusRequest;
+            mEventHandler.sendMessageDelayed(msg, RELEASE_FOCUS_TIMEOUT_MS);
+        }
+    }
+
+    private void handleReleasedFocus(ConnectionServiceFocus connectionServiceFocus) {
+        // The ConnectionService can call onConnectionServiceFocusReleased even if it's not the
+        // current focus connection service, nothing will be changed in this case.
+        if (Objects.equals(mCurrentFocus, connectionServiceFocus)) {
+            mEventHandler.removeMessages(MSG_RELEASE_FOCUS_TIMEOUT, mCurrentFocusRequest);
+            ConnectionServiceFocus newCSF = null;
+            if (mCurrentFocusRequest != null) {
+                newCSF = mCurrentFocusRequest.call.getConnectionServiceWrapper();
+            }
+            updateConnectionServiceFocus(newCSF);
+            updateCurrentFocusCall();
+            if (mCurrentFocusRequest != null) {
+                onRequestFocusDone(mCurrentFocusRequest);
+                mCurrentFocusRequest = null;
+            }
+        }
+    }
+
+    private void handleReleasedFocusTimeout(FocusRequest focusRequest) {
+        mCallsManagerRequester.releaseConnectionService(mCurrentFocus);
+        updateConnectionServiceFocus(focusRequest.call.getConnectionServiceWrapper());
+        updateCurrentFocusCall();
+        onRequestFocusDone(focusRequest);
+        mCurrentFocusRequest = null;
+    }
+
+    private void handleConnectionServiceDeath(ConnectionServiceFocus connectionServiceFocus) {
+        if (Objects.equals(connectionServiceFocus, mCurrentFocus)) {
+            updateConnectionServiceFocus(null);
+            updateCurrentFocusCall();
+        }
+    }
+
+    private void handleAddedCall(CallFocus call) {
+        if (!mCalls.contains(call)) {
+            mCalls.add(call);
+        }
+        if (Objects.equals(mCurrentFocus, call.getConnectionServiceWrapper())) {
+            updateCurrentFocusCall();
+        }
+    }
+
+    private void handleRemovedCall(CallFocus call) {
+        mCalls.remove(call);
+        if (call.equals(mCurrentFocusCall)) {
+            updateCurrentFocusCall();
+        }
+    }
+
+    private void handleCallStateChanged(CallFocus call, int oldState, int newState) {
+        if (mCalls.contains(call)
+                && Objects.equals(mCurrentFocus, call.getConnectionServiceWrapper())) {
+            updateCurrentFocusCall();
+        }
+    }
+
+    private final class FocusManagerHandler extends Handler {
+        FocusManagerHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_REQUEST_FOCUS:
+                    handleRequestFocus((FocusRequest) msg.obj);
+                    break;
+                case MSG_RELEASE_CONNECTION_FOCUS:
+                    handleReleasedFocus((ConnectionServiceFocus) msg.obj);
+                    break;
+                case MSG_RELEASE_FOCUS_TIMEOUT:
+                    handleReleasedFocusTimeout((FocusRequest) msg.obj);
+                    break;
+                case MSG_CONNECTION_SERVICE_DEATH:
+                    handleConnectionServiceDeath((ConnectionServiceFocus) msg.obj);
+                    break;
+                case MSG_ADD_CALL:
+                    handleAddedCall((CallFocus) msg.obj);
+                    break;
+                case MSG_REMOVE_CALL:
+                    handleRemovedCall((CallFocus) msg.obj);
+                    break;
+                case MSG_CALL_STATE_CHANGED:
+                    handleCallStateChanged((CallFocus) msg.obj, msg.arg1, msg.arg2);
+                    break;
+            }
+        }
+    }
+
+    private static final class FocusRequest {
+        CallFocus call;
+        @Nullable RequestFocusCallback callback;
+
+        FocusRequest(CallFocus call, RequestFocusCallback callback) {
+            this.call = call;
+            this.callback = callback;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index dc63bab..dce085c 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -64,7 +64,8 @@
  * {@link IConnectionService}.
  */
 @VisibleForTesting
-public class ConnectionServiceWrapper extends ServiceBinder {
+public class ConnectionServiceWrapper extends ServiceBinder implements
+        ConnectionServiceFocusManager.ConnectionServiceFocus {
 
     private final class Adapter extends IConnectionServiceAdapter.Stub {
 
@@ -874,6 +875,8 @@
     private final CallsManager mCallsManager;
     private final AppOpsManager mAppOpsManager;
 
+    private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
+
     /**
      * Creates a connection service.
      *
@@ -1413,6 +1416,24 @@
         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(this);
+        }
+    }
+
+    @Override
+    public void connectionServiceFocusGained() {}
+
+    @Override
+    public void setConnectionServiceFocusListener(
+            ConnectionServiceFocusManager.ConnectionServiceFocusListener listener) {
+        mConnSvrFocusListener = listener;
+    }
+
     private void handleCreateConnectionComplete(
             String callId,
             ConnectionRequest request,
@@ -1447,6 +1468,10 @@
             }
         }
         mCallIdMapper.clear();
+
+        if (mConnSvrFocusListener != null) {
+            mConnSvrFocusListener.onConnectionServiceDeath(this);
+        }
     }
 
     private void logIncoming(String msg, Object... params) {
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
new file mode 100644
index 0000000..affa4e9
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFocusManagerTest.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2017 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 android.os.Looper;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceFocusManager;
+import com.android.server.telecom.ConnectionServiceFocusManager.*;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ConnectionServiceFocusManagerTest extends TelecomTestCase {
+
+    @Mock CallsManagerRequester mockCallsManagerRequester;
+    @Mock RequestFocusCallback mockRequestFocusCallback;
+
+    @Mock ConnectionServiceFocus mNewConnectionService;
+    @Mock ConnectionServiceFocus mActiveConnectionService;
+
+    private static final int CHECK_HANDLER_INTERVAL_MS = 10;
+
+    private ConnectionServiceFocusManager mFocusManagerUT;
+    private CallFocus mNewCall;
+    private CallFocus mActiveCall;
+    private CallsManager.CallsManagerListener mCallsManagerListener;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mFocusManagerUT = new ConnectionServiceFocusManager(
+                mockCallsManagerRequester, Looper.getMainLooper());
+        mNewCall = createFakeCall(mNewConnectionService, CallState.NEW);
+        mActiveCall = createFakeCall(mActiveConnectionService, CallState.ACTIVE);
+        ArgumentCaptor<CallsManager.CallsManagerListener> captor =
+                ArgumentCaptor.forClass(CallsManager.CallsManagerListener.class);
+        verify(mockCallsManagerRequester).setCallsManagerListener(captor.capture());
+        mCallsManagerListener = captor.getValue();
+    }
+
+    @SmallTest
+    public void testRequestFocusWithoutActiveFocusExisted() {
+        // GIVEN the ConnectionServiceFocusManager without focus ConnectionService.
+
+        // WHEN request calling focus for the given call.
+        requestFocus(mNewCall, mockRequestFocusCallback);
+
+        // THEN the request is done and the ConnectionService of the given call has gain the focus.
+        verifyRequestFocusDone(mFocusManagerUT, mNewCall, mockRequestFocusCallback, true);
+    }
+
+    @SmallTest
+    public void testRequestFocusWithActiveFocusExisted() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService.
+        requestFocus(mActiveCall, null);
+        ConnectionServiceFocusListener connSvrFocusListener =
+                getConnectionServiceFocusListener(mActiveConnectionService);
+
+        // WHEN request calling focus for the given call.
+        requestFocus(mNewCall, mockRequestFocusCallback);
+
+        // THEN the current focus ConnectionService is informed it has lose the focus.
+        verify(mActiveConnectionService).connectionServiceFocusLost();
+        // and the focus request is not done.
+        verify(mockRequestFocusCallback, never())
+                .onRequestFocusDone(any(CallFocus.class));
+
+        // WHEN the current focus released the call resource.
+        connSvrFocusListener.onConnectionServiceReleased(mActiveConnectionService);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the request is done and the ConnectionService of the given call has gain the focus.
+        verifyRequestFocusDone(mFocusManagerUT, mNewCall, mockRequestFocusCallback, true);
+
+        // and the timeout event of the focus released is canceled.
+        waitForHandlerActionDelayed(
+                mFocusManagerUT.getHandler(),
+                mFocusManagerUT.RELEASE_FOCUS_TIMEOUT_MS,
+                CHECK_HANDLER_INTERVAL_MS);
+        verify(mockCallsManagerRequester, never()).releaseConnectionService(
+                any(ConnectionServiceFocus.class));
+    }
+
+    @SmallTest
+    public void testRequestConnectionServiceSameAsFocusConnectionService() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService.
+        requestFocus(mActiveCall, null);
+        reset(mActiveConnectionService);
+
+        // WHEN request calling focus for the given call that has the same ConnectionService as the
+        // active call.
+        when(mNewCall.getConnectionServiceWrapper()).thenReturn(mActiveConnectionService);
+        requestFocus(mNewCall, mockRequestFocusCallback);
+
+        // THEN the request is done without any change on the focus ConnectionService.
+        verify(mNewConnectionService, never()).connectionServiceFocusLost();
+        verifyRequestFocusDone(mFocusManagerUT, mNewCall, mockRequestFocusCallback, false);
+    }
+
+    @SmallTest
+    public void testFocusConnectionServiceDoesNotRespondToFocusLost() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService.
+        requestFocus(mActiveCall, null);
+
+        // WHEN request calling focus for the given call.
+        requestFocus(mNewCall, mockRequestFocusCallback);
+
+        // THEN the current focus ConnectionService is informed it has lose the focus.
+        verify(mActiveConnectionService).connectionServiceFocusLost();
+        // and the focus request is not done.
+        verify(mockRequestFocusCallback, never())
+                .onRequestFocusDone(any(CallFocus.class));
+
+        // but the current focus ConnectionService didn't respond to the focus lost.
+        waitForHandlerActionDelayed(
+                mFocusManagerUT.getHandler(),
+                CHECK_HANDLER_INTERVAL_MS,
+                mFocusManagerUT.RELEASE_FOCUS_TIMEOUT_MS + 100);
+
+        // THEN the focusManager sends a request to disconnect the focus ConnectionService
+        verify(mockCallsManagerRequester).releaseConnectionService(mActiveConnectionService);
+        // THEN the request is done and the ConnectionService of the given call has gain the focus.
+        verifyRequestFocusDone(mFocusManagerUT, mNewCall, mockRequestFocusCallback, true);
+    }
+
+    @SmallTest
+    public void testNonFocusConnectionServiceReleased() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService
+        requestFocus(mActiveCall, null);
+        ConnectionServiceFocusListener connSvrFocusListener =
+                getConnectionServiceFocusListener(mActiveConnectionService);
+
+        // WHEN there is a released request for a non focus ConnectionService.
+        connSvrFocusListener.onConnectionServiceReleased(mNewConnectionService);
+
+        // THEN nothing changed.
+        assertEquals(mActiveCall, mFocusManagerUT.getCurrentFocusCall());
+        assertEquals(mActiveConnectionService, mFocusManagerUT.getCurrentFocusConnectionService());
+    }
+
+    @SmallTest
+    public void testFocusConnectionServiceReleased() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService
+        requestFocus(mActiveCall, null);
+        ConnectionServiceFocusListener connSvrFocusListener =
+                getConnectionServiceFocusListener(mActiveConnectionService);
+
+        // WHEN the focus ConnectionService request to release.
+        connSvrFocusListener.onConnectionServiceReleased(mActiveConnectionService);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN both focus call and ConnectionService are null.
+        assertNull(mFocusManagerUT.getCurrentFocusCall());
+        assertNull(mFocusManagerUT.getCurrentFocusConnectionService());
+    }
+
+    @SmallTest
+    public void testCallStateChangedAffectCallFocus() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService.
+        CallFocus activeCall = createFakeCall(mActiveConnectionService, CallState.ACTIVE);
+        CallFocus newActivateCall = createFakeCall(mActiveConnectionService, CallState.ACTIVE);
+        requestFocus(activeCall, null);
+
+        // WHEN hold the active call.
+        int previousState = activeCall.getState();
+        when(activeCall.getState()).thenReturn(CallState.ON_HOLD);
+        mCallsManagerListener.onCallStateChanged(
+                (Call) activeCall, previousState, activeCall.getState());
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the focus call is null
+        assertNull(mFocusManagerUT.getCurrentFocusCall());
+        // and the focus ConnectionService is not changed.
+        assertEquals(mActiveConnectionService, mFocusManagerUT.getCurrentFocusConnectionService());
+
+        // WHEN a new active call is added.
+        when(newActivateCall.getState()).thenReturn(CallState.ACTIVE);
+        mCallsManagerListener.onCallAdded((Call) newActivateCall);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the focus call changed as excepted.
+        assertEquals(newActivateCall, mFocusManagerUT.getCurrentFocusCall());
+    }
+
+    @SmallTest
+    public void testCallStateChangedDoesNotAffectCallFocusIfConnectionServiceIsDifferent() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService
+        requestFocus(mActiveCall, null);
+
+        // WHEN a new active call is added (actually this should not happen).
+        when(mNewCall.getState()).thenReturn(CallState.ACTIVE);
+        mCallsManagerListener.onCallAdded((Call) mNewCall);
+
+        // THEN the call focus isn't changed.
+        assertEquals(mActiveCall, mFocusManagerUT.getCurrentFocusCall());
+
+        // WHEN the hold the active call.
+        when(mActiveCall.getState()).thenReturn(CallState.ON_HOLD);
+        mCallsManagerListener.onCallStateChanged(
+                (Call) mActiveCall, CallState.ACTIVE, CallState.ON_HOLD);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the focus call is null.
+        assertNull(mFocusManagerUT.getCurrentFocusCall());
+    }
+
+    @SmallTest
+    public void testFocusCallIsNullWhenRemoveTheFocusCall() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService
+        requestFocus(mActiveCall, null);
+        assertEquals(mActiveCall, mFocusManagerUT.getCurrentFocusCall());
+
+        // WHEN remove the active call
+        mCallsManagerListener.onCallRemoved((Call) mActiveCall);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the focus call is null
+        assertNull(mFocusManagerUT.getCurrentFocusCall());
+    }
+
+    @SmallTest
+    public void testConnectionServiceFocusDeath() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService
+        requestFocus(mActiveCall, null);
+        ConnectionServiceFocusListener connSvrFocusListener =
+                getConnectionServiceFocusListener(mActiveConnectionService);
+
+        // WHEN the active connection service focus is death
+        connSvrFocusListener.onConnectionServiceDeath(mActiveConnectionService);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN both connection service focus and call focus are null.
+        assertNull(mFocusManagerUT.getCurrentFocusConnectionService());
+        assertNull(mFocusManagerUT.getCurrentFocusCall());
+    }
+
+    @SmallTest
+    public void testNonExternalCallChangedToExternalCall() {
+        // GIVEN the ConnectionServiceFocusManager with the focus ConnectionService.
+        requestFocus(mActiveCall, null);
+        assertTrue(mFocusManagerUT.getAllCall().contains(mActiveCall));
+
+        // WHEN the non-external call changed to external call
+        mCallsManagerListener.onExternalCallChanged((Call) mActiveCall, true);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the call should be removed as it's an external call now.
+        assertFalse(mFocusManagerUT.getAllCall().contains(mActiveCall));
+    }
+
+    @SmallTest
+    public void testExternalCallChangedToNonExternalCall() {
+        // GIVEN the ConnectionServiceFocusManager without focus ConnectionService
+
+        // WHEN an external call changed to external call
+        mCallsManagerListener.onExternalCallChanged((Call) mActiveCall, false);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+
+        // THEN the call should be added as it's a non-external call now.
+        assertTrue(mFocusManagerUT.getAllCall().contains(mActiveCall));
+    }
+
+    private void requestFocus(CallFocus call, RequestFocusCallback callback) {
+        mCallsManagerListener.onCallAdded((Call) call);
+        mFocusManagerUT.requestFocus(call, callback);
+        waitForHandlerAction(mFocusManagerUT.getHandler(), CHECK_HANDLER_INTERVAL_MS);
+    }
+
+    private static void verifyRequestFocusDone(
+            ConnectionServiceFocusManager focusManager,
+            CallFocus call,
+            RequestFocusCallback callback,
+            boolean isConnectionServiceFocusChanged) {
+        verify(callback).onRequestFocusDone(call);
+        verify(call.getConnectionServiceWrapper(), times(isConnectionServiceFocusChanged ? 1 : 0))
+                .connectionServiceFocusGained();
+        assertEquals(
+                call.getConnectionServiceWrapper(),
+                focusManager.getCurrentFocusConnectionService());
+    }
+
+    /**
+     * Returns the {@link ConnectionServiceFocusListener} of the ConnectionServiceFocusManager.
+     * Make sure the given parameter {@code ConnectionServiceFocus} is a mock object and
+     * {@link ConnectionServiceFocus#setConnectionServiceFocusListener(
+     * ConnectionServiceFocusListener)} is called.
+     */
+    private static ConnectionServiceFocusListener getConnectionServiceFocusListener(
+            ConnectionServiceFocus connSvrFocus) {
+        ArgumentCaptor<ConnectionServiceFocusListener> captor =
+                ArgumentCaptor.forClass(ConnectionServiceFocusListener.class);
+        verify(connSvrFocus).setConnectionServiceFocusListener(captor.capture());
+        return captor.getValue();
+    }
+
+    private static Call createFakeCall(ConnectionServiceFocus connSvr, int state) {
+        Call call = Mockito.mock(Call.class);
+        when(call.getConnectionServiceWrapper()).thenReturn(connSvr);
+        when(call.getState()).thenReturn(state);
+        return call;
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
index b735df9..1892b99 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomTestCase.java
@@ -58,4 +58,16 @@
             }
         }
     }
+
+    protected final void waitForHandlerActionDelayed(Handler h, long timeoutMillis, long delayMs) {
+        final CountDownLatch lock = new CountDownLatch(1);
+        h.postDelayed(lock::countDown, delayMs);
+        while (lock.getCount() > 0) {
+            try {
+                lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+    }
 }