Loading src/com/android/server/telecom/CallsManager.java +25 −23 Original line number Original line Diff line number Diff line Loading @@ -2447,6 +2447,7 @@ public class CallsManager extends Call.ListenerBase * Removes an existing disconnected call, and notifies the in-call app. * Removes an existing disconnected call, and notifies the in-call app. */ */ void markCallAsRemoved(Call call) { void markCallAsRemoved(Call call) { mInCallController.getBindingFuture().thenRunAsync(() -> { call.maybeCleanupHandover(); call.maybeCleanupHandover(); removeCall(call); removeCall(call); Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall(); Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall(); Loading @@ -2456,8 +2457,8 @@ public class CallsManager extends Call.ListenerBase + isDisconnectingChildCall + "call -> %s", call); + isDisconnectingChildCall + "call -> %s", call); mLocallyDisconnectingCalls.remove(call); mLocallyDisconnectingCalls.remove(call); // Auto-unhold the foreground call due to a locally disconnected call, except if the // Auto-unhold the foreground call due to a locally disconnected call, except if the // call which was disconnected is a member of a conference (don't want to auto un-hold // call which was disconnected is a member of a conference (don't want to auto // the conference if we remove a member of the conference). // un-hold the conference if we remove a member of the conference). if (!isDisconnectingChildCall && foregroundCall != null if (!isDisconnectingChildCall && foregroundCall != null && foregroundCall.getState() == CallState.ON_HOLD) { && foregroundCall.getState() == CallState.ON_HOLD) { foregroundCall.unhold(); foregroundCall.unhold(); Loading @@ -2467,11 +2468,12 @@ public class CallsManager extends Call.ListenerBase foregroundCall.getState() == CallState.ON_HOLD) { foregroundCall.getState() == CallState.ON_HOLD) { // The new foreground call is on hold, however the carrier does not display the hold // The new foreground call is on hold, however the carrier does not display the hold // button in the UI. Therefore, we need to auto unhold the held call since the user has // button in the UI. Therefore, we need to auto unhold the held call since the user // no means of unholding it themselves. // has no means of unholding it themselves. Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)"); Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)"); foregroundCall.unhold(); foregroundCall.unhold(); } } }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock)); } } /** /** Loading src/com/android/server/telecom/InCallController.java +19 −0 Original line number Original line Diff line number Diff line Loading @@ -54,6 +54,8 @@ import java.util.LinkedList; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; import java.util.Objects; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** /** * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it Loading Loading @@ -736,6 +738,10 @@ public class InCallController extends CallsManagerListenerBase { private CarSwappingInCallServiceConnection mInCallServiceConnection; private CarSwappingInCallServiceConnection mInCallServiceConnection; private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections; private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections; // Future that's in a completed state unless we're in the middle of binding to a service. // The future will complete with true if binding succeeds, false if it timed out. private CompletableFuture<Boolean> mBindingFuture = CompletableFuture.completedFuture(true); public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager, public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager, SystemStateHelper systemStateHelper, SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter, DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter, Loading Loading @@ -1134,6 +1140,10 @@ public class InCallController extends CallsManagerListenerBase { // Only connect to the non-ui InCallServices if we actually connected to the main UI // Only connect to the non-ui InCallServices if we actually connected to the main UI // one. // one. connectToNonUiInCallServices(call); connectToNonUiInCallServices(call); mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false, mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay( mContext.getContentResolver()), TimeUnit.MILLISECONDS); } else { } else { Log.i(this, "bindToServices: current UI doesn't support call; not binding."); Log.i(this, "bindToServices: current UI doesn't support call; not binding."); } } Loading Loading @@ -1401,6 +1411,7 @@ public class InCallController extends CallsManagerListenerBase { inCallService.onCanAddCallChanged(mCallsManager.canAddCall()); inCallService.onCanAddCallChanged(mCallsManager.canAddCall()); } catch (RemoteException ignored) { } catch (RemoteException ignored) { } } mBindingFuture.complete(true); Log.i(this, "%s calls sent to InCallService.", numCallsSent); Log.i(this, "%s calls sent to InCallService.", numCallsSent); Trace.endSection(); Trace.endSection(); return true; return true; Loading Loading @@ -1487,6 +1498,14 @@ public class InCallController extends CallsManagerListenerBase { return mInCallServiceConnection != null && mInCallServiceConnection.isConnected(); return mInCallServiceConnection != null && mInCallServiceConnection.isConnected(); } } /** * @return A future that is pending whenever we are in the middle of binding to an * incall service. */ public CompletableFuture<Boolean> getBindingFuture() { return mBindingFuture; } /** /** * Dumps the state of the {@link InCallController}. * Dumps the state of the {@link InCallController}. * * Loading tests/src/com/android/server/telecom/tests/InCallControllerTests.java +70 −0 Original line number Original line Diff line number Diff line Loading @@ -86,6 +86,25 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Collections; import java.util.LinkedList; import java.util.LinkedList; import java.util.List; import java.util.List; import java.util.concurrent.CompletableFuture; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; @RunWith(JUnit4.class) @RunWith(JUnit4.class) public class InCallControllerTests extends TelecomTestCase { public class InCallControllerTests extends TelecomTestCase { Loading Loading @@ -654,6 +673,57 @@ public class InCallControllerTests extends TelecomTestCase { assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName()); assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName()); } } /** * Make sure the InCallController completes its binding future when the in call service * finishes binding. */ @MediumTest @Test public void testBindingFuture() throws Exception { when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle); when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockCallsManager.hasEmergencyCall()).thenReturn(false); when(mMockCall.isIncoming()).thenReturn(false); when(mMockCall.isExternalCall()).thenReturn(false); when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG); when(mMockContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class), anyInt(), nullable(UserHandle.class))) .thenReturn(true); when(mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay( nullable(ContentResolver.class))).thenReturn(500L); when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall)); setupMockPackageManager(true /* default */, true /* system */, false /* external calls */); mInCallController.bindToServices(mMockCall); ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor<ServiceConnection> serviceConnectionCaptor = ArgumentCaptor.forClass(ServiceConnection.class); verify(mMockContext, times(1)).bindServiceAsUser( bindIntentCaptor.capture(), serviceConnectionCaptor.capture(), eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE), eq(UserHandle.CURRENT)); CompletableFuture<Boolean> bindTimeout = mInCallController.getBindingFuture(); assertFalse(bindTimeout.isDone()); // Start the connection, make sure we don't unbind, and make sure that we don't send // anything to the in-call service yet. ServiceConnection serviceConnection = serviceConnectionCaptor.getValue(); ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS); IBinder mockBinder = mock(IBinder.class); IInCallService mockInCallService = mock(IInCallService.class); when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService); serviceConnection.onServiceConnected(defDialerComponentName, mockBinder); verify(mockInCallService).setInCallAdapter(nullable(IInCallAdapter.class)); // Make sure that the future completed without timing out. assertTrue(bindTimeout.getNow(false)); } private void setupMocks(boolean isExternalCall) { private void setupMocks(boolean isExternalCall) { when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle); when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle); when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); Loading Loading
src/com/android/server/telecom/CallsManager.java +25 −23 Original line number Original line Diff line number Diff line Loading @@ -2447,6 +2447,7 @@ public class CallsManager extends Call.ListenerBase * Removes an existing disconnected call, and notifies the in-call app. * Removes an existing disconnected call, and notifies the in-call app. */ */ void markCallAsRemoved(Call call) { void markCallAsRemoved(Call call) { mInCallController.getBindingFuture().thenRunAsync(() -> { call.maybeCleanupHandover(); call.maybeCleanupHandover(); removeCall(call); removeCall(call); Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall(); Call foregroundCall = mCallAudioManager.getPossiblyHeldForegroundCall(); Loading @@ -2456,8 +2457,8 @@ public class CallsManager extends Call.ListenerBase + isDisconnectingChildCall + "call -> %s", call); + isDisconnectingChildCall + "call -> %s", call); mLocallyDisconnectingCalls.remove(call); mLocallyDisconnectingCalls.remove(call); // Auto-unhold the foreground call due to a locally disconnected call, except if the // Auto-unhold the foreground call due to a locally disconnected call, except if the // call which was disconnected is a member of a conference (don't want to auto un-hold // call which was disconnected is a member of a conference (don't want to auto // the conference if we remove a member of the conference). // un-hold the conference if we remove a member of the conference). if (!isDisconnectingChildCall && foregroundCall != null if (!isDisconnectingChildCall && foregroundCall != null && foregroundCall.getState() == CallState.ON_HOLD) { && foregroundCall.getState() == CallState.ON_HOLD) { foregroundCall.unhold(); foregroundCall.unhold(); Loading @@ -2467,11 +2468,12 @@ public class CallsManager extends Call.ListenerBase foregroundCall.getState() == CallState.ON_HOLD) { foregroundCall.getState() == CallState.ON_HOLD) { // The new foreground call is on hold, however the carrier does not display the hold // The new foreground call is on hold, however the carrier does not display the hold // button in the UI. Therefore, we need to auto unhold the held call since the user has // button in the UI. Therefore, we need to auto unhold the held call since the user // no means of unholding it themselves. // has no means of unholding it themselves. Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)"); Log.i(this, "Auto-unholding held foreground call (call doesn't support hold)"); foregroundCall.unhold(); foregroundCall.unhold(); } } }, new LoggedHandlerExecutor(mHandler, "CM.mCAR", mLock)); } } /** /** Loading
src/com/android/server/telecom/InCallController.java +19 −0 Original line number Original line Diff line number Diff line Loading @@ -54,6 +54,8 @@ import java.util.LinkedList; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; import java.util.Objects; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; /** /** * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it Loading Loading @@ -736,6 +738,10 @@ public class InCallController extends CallsManagerListenerBase { private CarSwappingInCallServiceConnection mInCallServiceConnection; private CarSwappingInCallServiceConnection mInCallServiceConnection; private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections; private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections; // Future that's in a completed state unless we're in the middle of binding to a service. // The future will complete with true if binding succeeds, false if it timed out. private CompletableFuture<Boolean> mBindingFuture = CompletableFuture.completedFuture(true); public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager, public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager, SystemStateHelper systemStateHelper, SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter, DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter, Loading Loading @@ -1134,6 +1140,10 @@ public class InCallController extends CallsManagerListenerBase { // Only connect to the non-ui InCallServices if we actually connected to the main UI // Only connect to the non-ui InCallServices if we actually connected to the main UI // one. // one. connectToNonUiInCallServices(call); connectToNonUiInCallServices(call); mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false, mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay( mContext.getContentResolver()), TimeUnit.MILLISECONDS); } else { } else { Log.i(this, "bindToServices: current UI doesn't support call; not binding."); Log.i(this, "bindToServices: current UI doesn't support call; not binding."); } } Loading Loading @@ -1401,6 +1411,7 @@ public class InCallController extends CallsManagerListenerBase { inCallService.onCanAddCallChanged(mCallsManager.canAddCall()); inCallService.onCanAddCallChanged(mCallsManager.canAddCall()); } catch (RemoteException ignored) { } catch (RemoteException ignored) { } } mBindingFuture.complete(true); Log.i(this, "%s calls sent to InCallService.", numCallsSent); Log.i(this, "%s calls sent to InCallService.", numCallsSent); Trace.endSection(); Trace.endSection(); return true; return true; Loading Loading @@ -1487,6 +1498,14 @@ public class InCallController extends CallsManagerListenerBase { return mInCallServiceConnection != null && mInCallServiceConnection.isConnected(); return mInCallServiceConnection != null && mInCallServiceConnection.isConnected(); } } /** * @return A future that is pending whenever we are in the middle of binding to an * incall service. */ public CompletableFuture<Boolean> getBindingFuture() { return mBindingFuture; } /** /** * Dumps the state of the {@link InCallController}. * Dumps the state of the {@link InCallController}. * * Loading
tests/src/com/android/server/telecom/tests/InCallControllerTests.java +70 −0 Original line number Original line Diff line number Diff line Loading @@ -86,6 +86,25 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Collections; import java.util.LinkedList; import java.util.LinkedList; import java.util.List; import java.util.List; import java.util.concurrent.CompletableFuture; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; @RunWith(JUnit4.class) @RunWith(JUnit4.class) public class InCallControllerTests extends TelecomTestCase { public class InCallControllerTests extends TelecomTestCase { Loading Loading @@ -654,6 +673,57 @@ public class InCallControllerTests extends TelecomTestCase { assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName()); assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName()); } } /** * Make sure the InCallController completes its binding future when the in call service * finishes binding. */ @MediumTest @Test public void testBindingFuture() throws Exception { when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle); when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockCallsManager.hasEmergencyCall()).thenReturn(false); when(mMockCall.isIncoming()).thenReturn(false); when(mMockCall.isExternalCall()).thenReturn(false); when(mDefaultDialerCache.getDefaultDialerApplication(CURRENT_USER_ID)).thenReturn(DEF_PKG); when(mMockContext.bindServiceAsUser(nullable(Intent.class), nullable(ServiceConnection.class), anyInt(), nullable(UserHandle.class))) .thenReturn(true); when(mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay( nullable(ContentResolver.class))).thenReturn(500L); when(mMockCallsManager.getCalls()).thenReturn(Collections.singletonList(mMockCall)); setupMockPackageManager(true /* default */, true /* system */, false /* external calls */); mInCallController.bindToServices(mMockCall); ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor<ServiceConnection> serviceConnectionCaptor = ArgumentCaptor.forClass(ServiceConnection.class); verify(mMockContext, times(1)).bindServiceAsUser( bindIntentCaptor.capture(), serviceConnectionCaptor.capture(), eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE), eq(UserHandle.CURRENT)); CompletableFuture<Boolean> bindTimeout = mInCallController.getBindingFuture(); assertFalse(bindTimeout.isDone()); // Start the connection, make sure we don't unbind, and make sure that we don't send // anything to the in-call service yet. ServiceConnection serviceConnection = serviceConnectionCaptor.getValue(); ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS); IBinder mockBinder = mock(IBinder.class); IInCallService mockInCallService = mock(IInCallService.class); when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService); serviceConnection.onServiceConnected(defDialerComponentName, mockBinder); verify(mockInCallService).setInCallAdapter(nullable(IInCallAdapter.class)); // Make sure that the future completed without timing out. assertTrue(bindTimeout.getNow(false)); } private void setupMocks(boolean isExternalCall) { private void setupMocks(boolean isExternalCall) { when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle); when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle); when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); Loading