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

Commit 5c0f037a authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Enhancements to conference for non-conference host scenarios.

When a device A creates a conference call containing device B and C, both
B and C can receive IMS signaling to indicate that they are in a conference
call.  This occurs on most domestic carriers; the Telephony framework
uses the "multiparty" indicator on the IMS call to switch the call to a
conference call.

We made some changes to how conference calls are logged in Q which improves
the accuracy of the call durations.  We used to log calls as they're merged
into a conference.  In the case of a participant in a remotely hosted
conference call, we'd log the call as soon as they are remotely added
to the conference.  This is unfortunate as the call durations are grossly
under-reported.

The conference call logging changes now assume we'll log the conference
event package children in the conference instead of the participants which
merge into the conference itself.  On domestic carriers, since the
call on B (or C) becomes a conference, we would then no longer log the
call.

This is FURTHER complicated because on some carriers, B and C will ALSO
receive a conference event package from the network showing all the
participants in the conference.  So if B hangs up on the conference, they
will have entries in their call log for A and C, which is really strange
because a call to C was never originated on their device.

In Telecom we need to ensure we do not log remotely hosted conference
participants, and we need to ensure that we DO log a remotely hosted
conference as if its just a single party call.

To accomplish this we need:
1. Telecom needs to track the address and name display for remotely hosted
conferences; this ensures we can log the remotely hosted conference
correctly.
2. Telecom needs to track whether a conference call had children or not;
if it had children it should not be logged.  If it did not have children
and it was remotely hosted it should be logged.

Test: Manual test.
Test: Add unit test to cover this logging scenario.
Bug: 132325382
Change-Id: I7ab193d2c4252b6a97c27ee81cdd7ae152b505fb
parent 1a2f2b89
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -399,6 +399,8 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,

    private boolean mIsConference = false;

    private boolean mHadChildren = false;

    private final boolean mShouldAttachToExistingConnection;

    private Call mParentCall = null;
@@ -1011,6 +1013,13 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
        return mIsConference;
    }

    /**
     * @return {@code true} if this call had children at some point, {@code false} otherwise.
     */
    public boolean hadChildren() {
        return mHadChildren;
    }

    public Uri getHandle() {
        return mHandle;
    }
@@ -2476,6 +2485,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,

    private void addChildCall(Call call) {
        if (!mChildCalls.contains(call)) {
            mHadChildren = true;
            // Set the pseudo-active call to the latest child added to the conference.
            // See definition of mConferenceLevelActiveCall for more detail.
            mConferenceLevelActiveCall = call;
+27 −18
Original line number Diff line number Diff line
@@ -182,44 +182,53 @@ public final class CallLogManager extends CallsManagerListenerBase {

    /**
     * Log newly disconnected calls only if all of below conditions are met:
     * 1) Call was NOT in the "choose account" phase when disconnected
     * 2) Call is NOT a conference call
     * 3) Call is NOT simulating a single party conference.
     * 4) Call was NOT explicitly canceled, except for disconnecting from a conference.
     * 5) Call is NOT an external call
     * 6) Call is NOT disconnected because of merging into a conference.
     * 7) Call is NOT a self-managed call OR call is a self-managed call which has indicated it
     * Call was NOT in the "choose account" phase when disconnected
     * Call is NOT a conference call which had children (unless it was remotely hosted).
     * Call is NOT a child call from a conference which was remotely hosted.
     * Call is NOT simulating a single party conference.
     * Call was NOT explicitly canceled, except for disconnecting from a conference.
     * Call is NOT an external call
     * Call is NOT disconnected because of merging into a conference.
     * Call is NOT a self-managed call OR call is a self-managed call which has indicated it
     * should be logged in its PhoneAccount
     */
    private boolean shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled) {
        // 1) "Choose account" phase when disconnected
    @VisibleForTesting
    public boolean shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled) {
        // "Choose account" phase when disconnected
        if (oldState == CallState.SELECT_PHONE_ACCOUNT) {
            return false;
        }
        // 2) A conference call
        if (call.isConference()) {
        // A conference call which had children should not be logged, unless it was remotely hosted.
        if (call.isConference() && call.hadChildren() &&
                !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
            return false;
        }

        // A child call of a conference which was remotely hosted; these didn't originate on this
        // device and should not be logged.
        if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) {
            return false;
        }

        DisconnectCause cause = call.getDisconnectCause();
        if (isCallCanceled) {
            // 3) No log when disconnecting to simulate a single party conference.
            // No log when disconnecting to simulate a single party conference.
            if (cause != null
                    && DisconnectCause.REASON_EMULATING_SINGLE_CALL.equals(cause.getReason())) {
                return false;
            }
            // 4) Explicitly canceled
            // Explicitly canceled
            // Conference children connections only have CAPABILITY_DISCONNECT_FROM_CONFERENCE.
            // Log them when they are disconnected from conference.
            return Connection.can(call.getConnectionCapabilities(),
                    Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE);
        }
        // 5) An external call
        // An external call
        if (call.isExternalCall()) {
            return false;
        }

        // 6) Call merged into conferences.
        // Call merged into conferences.
        if (cause != null && android.telephony.DisconnectCause.toString(
                android.telephony.DisconnectCause.IMS_MERGED_SUCCESSFULLY)
                .equals(cause.getReason())) {
@@ -229,7 +238,7 @@ public final class CallLogManager extends CallsManagerListenerBase {
        boolean shouldCallSelfManagedLogged = call.isLoggedSelfManaged()
                && (call.getHandoverState() == HandoverState.HANDOVER_NONE
                || call.getHandoverState() == HandoverState.HANDOVER_COMPLETE);
        // 7) Call is NOT a self-managed call OR call is a self-managed call which has indicated it
        // Call is NOT a self-managed call OR call is a self-managed call which has indicated it
        // should be logged in its PhoneAccount
        return !call.isSelfManaged() || shouldCallSelfManagedLogged;
    }
+2 −0
Original line number Diff line number Diff line
@@ -2814,6 +2814,8 @@ public class CallsManager extends Call.ListenerBase

        setCallState(call, Call.getStateFromConnectionState(parcelableConference.getState()),
                "new conference call");
        call.setHandle(parcelableConference.getHandle(),
                parcelableConference.getHandlePresentation());
        call.setConnectionCapabilities(parcelableConference.getConnectionCapabilities());
        call.setConnectionProperties(parcelableConference.getConnectionProperties());
        call.setVideoState(parcelableConference.getVideoState());
+135 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.telecom.tests;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -48,6 +49,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -781,6 +783,136 @@ public class CallLogManagerTest extends TelecomTestCase {
        assertEquals(TEST_ISO_2, resultIso2);
    }

    @SmallTest
    @Test
    public void testLogConferenceWithNoChildren() {
        Call fakeCall = makeFakeCall(
                DisconnectCause.LOCAL, // disconnectCauseCode
                true, // isConference
                true, // isIncoming
                1L, // creationTimeMillis
                1000L, // ageMillis
                TEL_PHONEHANDLE, // callHandle
                mDefaultAccountHandle, // phoneAccountHandle
                NO_VIDEO_STATE, // callVideoState
                POST_DIAL_STRING, // postDialDigits
                VIA_NUMBER_STRING, // viaNumber
                UserHandle.of(CURRENT_USER_ID)
        );
        when(fakeCall.hadChildren()).thenReturn(false);

        assertTrue(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
                false /* isCanceled */));
    }

    @SmallTest
    @Test
    public void testDoNotLogConferenceWithChildren() {
        Call fakeCall = makeFakeCall(
                DisconnectCause.LOCAL, // disconnectCauseCode
                true, // isConference
                true, // isIncoming
                1L, // creationTimeMillis
                1000L, // ageMillis
                TEL_PHONEHANDLE, // callHandle
                mDefaultAccountHandle, // phoneAccountHandle
                NO_VIDEO_STATE, // callVideoState
                POST_DIAL_STRING, // postDialDigits
                VIA_NUMBER_STRING, // viaNumber
                UserHandle.of(CURRENT_USER_ID)
        );
        when(fakeCall.hadChildren()).thenReturn(true);

        assertFalse(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
                false /* isCanceled */));
    }

    @SmallTest
    @Test
    public void testLogRemotelyHostedConferenceWithChildren() {
        Call fakeCall = makeFakeCall(
                DisconnectCause.LOCAL, // disconnectCauseCode
                true, // isConference
                true, // isIncoming
                1L, // creationTimeMillis
                1000L, // ageMillis
                TEL_PHONEHANDLE, // callHandle
                mDefaultAccountHandle, // phoneAccountHandle
                NO_VIDEO_STATE, // callVideoState
                POST_DIAL_STRING, // postDialDigits
                VIA_NUMBER_STRING, // viaNumber
                UserHandle.of(CURRENT_USER_ID)
        );
        when(fakeCall.hadChildren()).thenReturn(true);
        when(fakeCall.hasProperty(eq(Connection.PROPERTY_REMOTELY_HOSTED))).thenReturn(true);

        assertTrue(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
                false /* isCanceled */));
    }

    @SmallTest
    @Test
    public void testLogRemotelyHostedConferenceWithNoChildren() {
        Call fakeCall = makeFakeCall(
                DisconnectCause.LOCAL, // disconnectCauseCode
                true, // isConference
                true, // isIncoming
                1L, // creationTimeMillis
                1000L, // ageMillis
                TEL_PHONEHANDLE, // callHandle
                mDefaultAccountHandle, // phoneAccountHandle
                NO_VIDEO_STATE, // callVideoState
                POST_DIAL_STRING, // postDialDigits
                VIA_NUMBER_STRING, // viaNumber
                UserHandle.of(CURRENT_USER_ID)
        );
        when(fakeCall.hadChildren()).thenReturn(false);
        when(fakeCall.hasProperty(eq(Connection.PROPERTY_REMOTELY_HOSTED))).thenReturn(true);

        assertTrue(mCallLogManager.shouldLogDisconnectedCall(fakeCall, CallState.DISCONNECTED,
                false /* isCanceled */));
    }

    @SmallTest
    @Test
    public void testDoNotLogChildOfRemotelyHostedConference() {
        Call fakeConfCall = makeFakeCall(
                DisconnectCause.LOCAL, // disconnectCauseCode
                true, // isConference
                true, // isIncoming
                1L, // creationTimeMillis
                1000L, // ageMillis
                TEL_PHONEHANDLE, // callHandle
                mDefaultAccountHandle, // phoneAccountHandle
                NO_VIDEO_STATE, // callVideoState
                POST_DIAL_STRING, // postDialDigits
                VIA_NUMBER_STRING, // viaNumber
                UserHandle.of(CURRENT_USER_ID)
        );
        when(fakeConfCall.hadChildren()).thenReturn(true);
        when(fakeConfCall.hasProperty(eq(Connection.PROPERTY_REMOTELY_HOSTED))).thenReturn(true);

        Call fakeChild = makeFakeCall(
                DisconnectCause.LOCAL, // disconnectCauseCode
                false, // isConference
                true, // isIncoming
                1L, // creationTimeMillis
                1000L, // ageMillis
                TEL_PHONEHANDLE, // callHandle
                mDefaultAccountHandle, // phoneAccountHandle
                NO_VIDEO_STATE, // callVideoState
                POST_DIAL_STRING, // postDialDigits
                VIA_NUMBER_STRING, // viaNumber
                UserHandle.of(CURRENT_USER_ID)
        );
        when(fakeChild.hadChildren()).thenReturn(false);
        when(fakeChild.getParentCall()).thenReturn(fakeConfCall);
        when(fakeChild.hasProperty(eq(Connection.PROPERTY_REMOTELY_HOSTED))).thenReturn(true);

        assertFalse(mCallLogManager.shouldLogDisconnectedCall(fakeChild, CallState.DISCONNECTED,
                false /* isCanceled */));
    }

    private ArgumentCaptor<CountryListener> verifyCountryIso(CountryDetector mockDetector,
            String resultIso) {
        ArgumentCaptor<CountryListener> captor = ArgumentCaptor.forClass(CountryListener.class);
@@ -862,6 +994,9 @@ public class CallLogManagerTest extends TelecomTestCase {
        when(fakeCall.getCallDataUsage()).thenReturn(callDataUsage);
        when(fakeCall.isEmergencyCall()).thenReturn(
                phoneAccountHandle.equals(EMERGENCY_ACCT_HANDLE));
        when(fakeCall.getParentCall()).thenReturn(null);
        when(fakeCall.hadChildren()).thenReturn(true);
        when(fakeCall.hasProperty(eq(Connection.PROPERTY_REMOTELY_HOSTED))).thenReturn(false);
        return fakeCall;
    }

+5 −1
Original line number Diff line number Diff line
@@ -670,7 +670,11 @@ public class ConnectionServiceFixture implements TestFixture<IConnectionService>
                c.connectTimeMillis,
                c.connectElapsedTimeMillis,
                c.statusHints,
                c.extras);
                c.extras,
                null,
                0,
                null,
                0);
    }

    private ParcelableConnection parcelable(ConnectionInfo c) {