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
+ }
+ }
+ }
}