Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 69316c5a authored by Pranav Madapurmath's avatar Pranav Madapurmath
Browse files

Terminate ICS (for BT) after the disconnect tone finishes playing.

Currently, the disconnect tone and ICS termination occur independently
so we run into a scenario where the disconnect tone never plays over BT
(but we can hear the tone from the device).

We can introduce a future to hold the connection until after the tone is
done playing. The future can be configured to complete with a timeout of
4s (to account for the longest tone - TONE_CONGESTION) as a fallback. We
can perform manual completion when we cleanup the tone player and in
cases where we decide not to start the tone in the first place.

This requires introducing the future into the framework as there is no
other valid point of entry in the packages side. When we disconnect a
call, InCallController#updateCall is invoked which ends up terminating
the connection with the service. We cannot delay the workflow at this
point since Dialer would need to be immediately informed when the call
is being disconnected.

Bug: 194979745
Test: Manual
Change-Id: I5375f230e9300dc7ae5e9c819e8c4fa81991b4ae
parent 80a09a11
Loading
Loading
Loading
Loading
+29 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.media.ToneGenerator;
import android.os.UserHandle;
import android.telecom.CallAudioState;
import android.telecom.Log;
import android.telecom.Phone;
import android.telecom.PhoneAccount;
import android.telecom.VideoProfile;
import android.util.SparseArray;
@@ -43,6 +44,13 @@ public class CallAudioManager extends CallsManagerListenerBase {
        IAudioService getAudioService();
    }

    // success message to be logged when disconnected tone future is completed
    public static final String DISCONNECTED_TONE_SUCCESS_MSG = "Successfully completed "
            + "disconnected tone future.";
    // failure message to be logged when disconnected tone future cannot be MANUALLY completed
    public static final String DISCONNECTED_TONE_FAILURE_MSG = "Disconnected tone future timed"
            + " out.";

    private final String LOG_TAG = CallAudioManager.class.getSimpleName();

    private final LinkedHashSet<Call> mActiveDialingOrConnectingCalls;
@@ -837,16 +845,19 @@ public class CallAudioManager extends CallsManagerListenerBase {
    }

    private void playToneForDisconnectedCall(Call call) {
        String callId = call.getId();
        // If this call is being disconnected as a result of being handed over to another call,
        // we will not play a disconnect tone.
        if (call.isHandoverInProgress()) {
            Log.i(LOG_TAG, "Omitting tone because %s is being handed over.", call);
            completeDisconnectedToneFuture(callId);
            return;
        }

        if (mForegroundCall != null && call != mForegroundCall && mCalls.size() > 1) {
            Log.v(LOG_TAG, "Omitting tone because we are not foreground" +
                    " and there is another call.");
            completeDisconnectedToneFuture(callId);
            return;
        }

@@ -882,11 +893,16 @@ public class CallAudioManager extends CallsManagerListenerBase {
            Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);

            if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
                boolean didToneStart = mPlayerFactory.createPlayer(toneToPlay).startTone();
                InCallTonePlayer tonePlayer = mPlayerFactory.createPlayer(toneToPlay);
                // set call id in InCallTonePlayer to be used for future completion
                tonePlayer.setCallIdForDisconnectedToneFuture(callId);
                boolean didToneStart = tonePlayer.startTone();
                if (didToneStart) {
                    mCallsManager.onDisconnectedTonePlaying(true);
                    mIsDisconnectedTonePlaying = true;
                }
            } else {
                completeDisconnectedToneFuture(callId);
            }
        }
    }
@@ -973,6 +989,18 @@ public class CallAudioManager extends CallsManagerListenerBase {
                oldState == CallState.ON_HOLD;
    }

    @VisibleForTesting
    public boolean completeDisconnectedToneFuture(String callId){
        String logPrefix = "completeDisconnectedToneFuture: callId: %s; ";
        boolean result = Phone.completeDisconnectedToneFuture(callId);
        if (result) {
            Log.i(this, logPrefix + DISCONNECTED_TONE_SUCCESS_MSG, callId);
        } else {
            Log.w(this, logPrefix + DISCONNECTED_TONE_FAILURE_MSG, callId);
        }
        return result;
    }

    @VisibleForTesting
    public Set<Call> getTrackedCalls() {
        return mCalls;
+11 −0
Original line number Diff line number Diff line
@@ -206,6 +206,9 @@ public class InCallTonePlayer extends Thread {
    /** For tones which are not generated using ToneGenerator. */
    private MediaPlayerAdapter mToneMediaPlayer = null;

    /** Used for lookup in handling disconnected tone future completion*/
    private String mCallId;

    /** Telecom lock object. */
    private final TelecomSystem.SyncRoot mLock;

@@ -512,6 +515,10 @@ public class InCallTonePlayer extends Thread {
        sTonesPlaying.set(0);
    }

    public void setCallIdForDisconnectedToneFuture(String callId) {
        mCallId = callId;
    }

    private void cleanUpTonePlayer() {
        Log.d(this, "cleanUpTonePlayer(): posting cleanup");
        // Release focus on the main thread.
@@ -535,5 +542,9 @@ public class InCallTonePlayer extends Thread {
                }
            }
        }.prepare());
        // try to complete disconnected tone future for mCallId (if present)
        if (mCallId != null) {
            mCallAudioManager.completeDisconnectedToneFuture(mCallId);
        }
    }
}
+49 −0
Original line number Diff line number Diff line
@@ -18,10 +18,13 @@ package com.android.server.telecom.tests;

import android.media.ToneGenerator;
import android.telecom.DisconnectCause;
import android.telecom.Phone;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.ArrayMap;
import android.util.SparseArray;

import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallAudioModeStateMachine;
import com.android.server.telecom.CallAudioModeStateMachine.MessageArgs;
@@ -44,10 +47,14 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoSession;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;
@@ -80,6 +87,8 @@ public class CallAudioManagerTest extends TelecomTestCase {

    private CallAudioManager mCallAudioManager;

    private static final int DISCONNECTED_TONE_TIMEOUT = 4000;

    @Override
    @Before
    public void setUp() throws Exception {
@@ -602,6 +611,46 @@ public class CallAudioManagerTest extends TelecomTestCase {
        assertMessageArgEquality(expectedArgs, captor.getValue());
    }

    @SmallTest
    @Test
    public void testDisconnectedToneFuture() {
        Call call = mock(Call.class);
        when(call.getId()).thenReturn("testCallId");
        Map<String, CompletableFuture<Void>> disconnectedToneFutures = new ArrayMap<>();
        disconnectedToneFutures.put(call.getId(), new CompletableFuture<Void>()
                .completeOnTimeout(null, DISCONNECTED_TONE_TIMEOUT, TimeUnit.MILLISECONDS));

        MockitoSession session = ExtendedMockito.
                mockitoSession().spyStatic(Phone.class).startMocking();
        try {
            ExtendedMockito.doAnswer(invocation -> {
                String callId = invocation.getArgument(0);
                if (disconnectedToneFutures.containsKey(callId)) {
                    disconnectedToneFutures.get(callId).complete(null);
                    return true;
                } else {
                    return false;
                }
            }).when(() -> Phone.completeDisconnectedToneFuture(any(String.class)));

            // Add call
            mCallAudioManager.onCallAdded(call);
            // Disconnect call
            disconnectCall(call);
            // Simulate cleanup
            stopTone();
            // Verify that no future is completed on an invalid callId (i.e. not in map)
            boolean result = mCallAudioManager.completeDisconnectedToneFuture("");
            assertFalse(result);
            // Verify proper future completion
            result = mCallAudioManager.completeDisconnectedToneFuture(call.getId());
            assertTrue(result);
            assertTrue(disconnectedToneFutures.get(call.getId()).isDone());
        } finally {
            session.finishMocking();
        }
    }

    private Call createAudioProcessingCall() {
        Call call = mock(Call.class);
        when(call.getState()).thenReturn(CallState.AUDIO_PROCESSING);