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

Commit c2361e51 authored by Jordan Liu's avatar Jordan Liu
Browse files

Sort quality by timestamp

We can have duration calculation issues if the modem reports call
quality out of order (which it seems to sometimes do).

Instead of relying on the modem, since we don't need the total duration
calculations until end of call, we can just wait until the end and then
calculate duration with all of the quality information sorted by
timestamp.

Bug: 141769213
Test: atest CallQualityMetricsTest
Change-Id: I753d824f5bf518faf2da773c0d9510508e360bd1
parent 5c2a8680
Loading
Loading
Loading
Loading
+95 −45
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.internal.telephony.ServiceStateTracker;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;

import java.util.ArrayList;
import java.util.Collections;

/**
 * CallQualityMetrics is a utility for tracking the CallQuality during an ongoing call session. It
@@ -63,6 +64,12 @@ public class CallQualityMetrics {
    // the first MAX_SNAPSHOTS transitions between good and bad quality
    private ArrayList<Pair<CallQuality, Integer>> mDlSnapshots = new ArrayList<>();

    // holds lightweight history of call quality and durations, used for calculating total time
    // spent with bad and good quality for metrics and bugreports. This is separate from the
    // snapshots because those are capped at MAX_SNAPSHOTS to avoid excessive memory use.
    private ArrayList<TimestampedQualitySnapshot> mFullUplinkQuality = new ArrayList<>();
    private ArrayList<TimestampedQualitySnapshot> mFullDownlinkQuality = new ArrayList<>();

    // Current downlink call quality
    private int mDlCallQualityState = GOOD_QUALITY;

@@ -82,12 +89,6 @@ public class CallQualityMetrics {
    private Pair<CallQuality, Integer> mWorstSsWithBadUlQuality;
    private Pair<CallQuality, Integer> mBestSsWithBadUlQuality;

    /** Total durations of good and bad quality time for uplink and downlink */
    private int mTotalDlGoodQualityTimeMs = 0;
    private int mTotalDlBadQualityTimeMs = 0;
    private int mTotalUlGoodQualityTimeMs = 0;
    private int mTotalUlBadQualityTimeMs = 0;

    /**
     * Construct a CallQualityMetrics object to be used to keep track of call quality for a single
     * call session.
@@ -118,21 +119,31 @@ public class CallQualityMetrics {

        if (USERDEBUG_MODE) {
            if (newUlCallQualityState != mUlCallQualityState) {
                mUlSnapshots = addSnapshot(cq, mUlSnapshots);
                addSnapshot(cq, mUlSnapshots);
            }
            if (newDlCallQualityState != mDlCallQualityState) {
                mDlSnapshots = addSnapshot(cq, mDlSnapshots);
                addSnapshot(cq, mDlSnapshots);
            }
        }

        updateTotalDurations(newDlCallQualityState, newUlCallQualityState, cq);
        updateTotalDurations(cq);

        updateMinAndMaxSignalStrengthSnapshots(newDlCallQualityState, newUlCallQualityState, cq);

        mUlCallQualityState = newUlCallQualityState;
        mDlCallQualityState = newDlCallQualityState;
        // call duration updates sometimes come out of order
        if (cq.getCallDuration() > mLastCallQuality.getCallDuration()) {
            mLastCallQuality = cq;
        }
    }

    private void updateTotalDurations(CallQuality cq) {
        mFullDownlinkQuality.add(new TimestampedQualitySnapshot(cq.getCallDuration(),
                cq.getDownlinkCallQualityLevel()));
        mFullUplinkQuality.add(new TimestampedQualitySnapshot(cq.getCallDuration(),
                cq.getUplinkCallQualityLevel()));
    }

    private static boolean isGoodQuality(int callQualityLevel) {
        return callQualityLevel < CallQuality.CALL_QUALITY_BAD;
@@ -142,32 +153,11 @@ public class CallQualityMetrics {
     * Save a snapshot of the call quality and signal strength. This can be called with uplink or
     * downlink call quality level.
     */
    private ArrayList<Pair<CallQuality, Integer>> addSnapshot(CallQuality cq,
            ArrayList<Pair<CallQuality, Integer>> snapshots) {
    private void addSnapshot(CallQuality cq, ArrayList<Pair<CallQuality, Integer>> snapshots) {
        if (snapshots.size() < MAX_SNAPSHOTS) {
            Integer ss = getLteSnr();
            snapshots.add(Pair.create(cq, ss));
        }
        return snapshots;
    }

    /**
     * Updates the running total duration of good and bad call quality for uplink and downlink.
     */
    private void updateTotalDurations(int newDlCallQualityState, int newUlCallQualityState,
            CallQuality cq) {
        int timePassed = cq.getCallDuration() - mLastCallQuality.getCallDuration();
        if (newDlCallQualityState == GOOD_QUALITY) {
            mTotalDlGoodQualityTimeMs += timePassed;
        } else {
            mTotalDlBadQualityTimeMs += timePassed;
        }

        if (newUlCallQualityState == GOOD_QUALITY) {
            mTotalUlGoodQualityTimeMs += timePassed;
        } else {
            mTotalUlBadQualityTimeMs += timePassed;
        }
    }

    /**
@@ -261,8 +251,10 @@ public class CallQualityMetrics {
    public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryDl() {
        TelephonyCallSession.Event.CallQualitySummary summary =
                new TelephonyCallSession.Event.CallQualitySummary();
        summary.totalGoodQualityDurationInSeconds = mTotalDlGoodQualityTimeMs / 1000;
        summary.totalBadQualityDurationInSeconds = mTotalDlBadQualityTimeMs / 1000;
        Pair<Integer, Integer> totalGoodAndBadDurations = getTotalGoodAndBadQualityTimeMs(
                mFullDownlinkQuality);
        summary.totalGoodQualityDurationInSeconds = totalGoodAndBadDurations.first / 1000;
        summary.totalBadQualityDurationInSeconds = totalGoodAndBadDurations.second / 1000;
        // This value could be different from mLastCallQuality.getCallDuration if we support
        // handover from IMS->CS->IMS, but this is currently not possible
        // TODO(b/130302396) this also may be possible when we put a call on hold and continue with
@@ -299,8 +291,10 @@ public class CallQualityMetrics {
    public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryUl() {
        TelephonyCallSession.Event.CallQualitySummary summary =
                new TelephonyCallSession.Event.CallQualitySummary();
        summary.totalGoodQualityDurationInSeconds = mTotalUlGoodQualityTimeMs / 1000;
        summary.totalBadQualityDurationInSeconds = mTotalUlBadQualityTimeMs / 1000;
        Pair<Integer, Integer> totalGoodAndBadDurations = getTotalGoodAndBadQualityTimeMs(
                mFullUplinkQuality);
        summary.totalGoodQualityDurationInSeconds = totalGoodAndBadDurations.first / 1000;
        summary.totalBadQualityDurationInSeconds = totalGoodAndBadDurations.second / 1000;
        // This value could be different from mLastCallQuality.getCallDuration if we support
        // handover from IMS->CS->IMS, but this is currently not possible
        // TODO(b/130302396) this also may be possible when we put a call on hold and continue with
@@ -331,6 +325,60 @@ public class CallQualityMetrics {
        return summary;
    }


    /**
     * Container class for call quality level and signal strength at the time of snapshot. This
     * class implements compareTo so that it can be sorted by timestamp
     */
    private class TimestampedQualitySnapshot implements Comparable<TimestampedQualitySnapshot> {
        int mTimestampMs;
        int mCallQualityLevel;

        TimestampedQualitySnapshot(int timestamp, int cq) {
            mTimestampMs = timestamp;
            mCallQualityLevel = cq;
        }

        @Override
        public int compareTo(TimestampedQualitySnapshot o) {
            return this.mTimestampMs - o.mTimestampMs;
        }

        @Override
        public String toString() {
            return "mTimestampMs=" + mTimestampMs + " mCallQualityLevel=" + mCallQualityLevel;
        }
    }

    /**
     * Use a list of snapshots to calculate and return the total time spent in a call with good
     * quality and bad quality.
     * This is slightly expensive since it involves sorting the snapshots by timestamp.
     *
     * @param snapshots a list of uplink or downlink snapshots
     * @return a pair where the first element is the total good quality time and the second element
     * is the total bad quality time
     */
    private Pair<Integer, Integer> getTotalGoodAndBadQualityTimeMs(
            ArrayList<TimestampedQualitySnapshot> snapshots) {
        int totalGoodQualityTime = 0;
        int totalBadQualityTime = 0;
        int lastTimestamp = 0;
        // sort by timestamp using TimestampedQualitySnapshot.compareTo
        Collections.sort(snapshots);
        for (TimestampedQualitySnapshot snapshot : snapshots) {
            int timeSinceLastSnapshot = snapshot.mTimestampMs - lastTimestamp;
            if (isGoodQuality(snapshot.mCallQualityLevel)) {
                totalGoodQualityTime += timeSinceLastSnapshot;
            } else {
                totalBadQualityTime += timeSinceLastSnapshot;
            }
            lastTimestamp = snapshot.mTimestampMs;
        }

        return Pair.create(totalGoodQualityTime, totalBadQualityTime);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
@@ -355,14 +403,16 @@ public class CallQualityMetrics {
        }
        sb.append("}");
        sb.append(" ");
        sb.append(" mTotalDlGoodQualityTimeMs: ");
        sb.append(mTotalDlGoodQualityTimeMs);
        sb.append(" mTotalDlBadQualityTimeMs: ");
        sb.append(mTotalDlBadQualityTimeMs);
        sb.append(" mTotalUlGoodQualityTimeMs: ");
        sb.append(mTotalUlGoodQualityTimeMs);
        sb.append(" mTotalUlBadQualityTimeMs: ");
        sb.append(mTotalUlBadQualityTimeMs);
        Pair<Integer, Integer> dlTotals = getTotalGoodAndBadQualityTimeMs(mFullDownlinkQuality);
        Pair<Integer, Integer> ulTotals = getTotalGoodAndBadQualityTimeMs(mFullUplinkQuality);
        sb.append(" TotalDlGoodQualityTimeMs: ");
        sb.append(dlTotals.first);
        sb.append(" TotalDlBadQualityTimeMs: ");
        sb.append(dlTotals.second);
        sb.append(" TotalUlGoodQualityTimeMs: ");
        sb.append(ulTotals.first);
        sb.append(" TotalUlBadQualityTimeMs: ");
        sb.append(ulTotals.second);
        sb.append("]");
        return sb.toString();
    }
+125 −6
Original line number Diff line number Diff line
@@ -27,7 +27,6 @@ import android.telephony.CellSignalStrengthNr;
import android.telephony.CellSignalStrengthTdscdma;
import android.telephony.CellSignalStrengthWcdma;
import android.telephony.SignalStrength;
import android.test.suitebuilder.annotation.SmallTest;

import com.android.internal.telephony.TelephonyTest;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.CallQualitySummary;
@@ -68,7 +67,6 @@ public class CallQualityMetricsTest extends TelephonyTest {
     * Verify that good/bad quality and total duration stats are correct.
     */
    @Test
    @SmallTest
    public void testTotalDurations() {
        // Call quality in the following sequence:
        //
@@ -105,12 +103,109 @@ public class CallQualityMetricsTest extends TelephonyTest {
        assertEquals(14, ulSummary.totalDurationWithQualityInformationInSeconds);
    }

    /**
     * Verify that good/bad quality and total duration stats are correct.
     *
     * Similar to testTotalDurations, but getCallQualitySummaryUl/Dl will be called multiple times,
     * so verify that it continues to work after the first call.
     */
    @Test
    public void testTotalDurations_MultipleChecks() {
        // Call quality in the following sequence:
        //
        // DL: GOOD       GOOD      BAD
        // UL: GOOD       BAD       GOOD
        // |----------|----------|--------|
        // 0          5          10       14
        //
        // 0s = Start of call. Assumed to be good quality
        // 5s = Switches to UL bad quality
        // 10s = Switches to UL good quality, DL bad quality
        // 14s = End of call. Switches to UL bad quality, DL good quality
        CallQuality cq1 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 5000);
        CallQuality cq2 = constructCallQuality(CallQuality.CALL_QUALITY_BAD,
                CallQuality.CALL_QUALITY_EXCELLENT, 10000);
        CallQuality cq3 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 14000);

        mCallQualityMetrics.saveCallQuality(cq1);
        mCallQualityMetrics.saveCallQuality(cq2);
        mCallQualityMetrics.saveCallQuality(cq3);

        // verify UL quality durations
        CallQualitySummary dlSummary = mCallQualityMetrics.getCallQualitySummaryDl();
        assertEquals(9, dlSummary.totalGoodQualityDurationInSeconds);
        assertEquals(5, dlSummary.totalBadQualityDurationInSeconds);
        assertEquals(14, dlSummary.totalDurationWithQualityInformationInSeconds);

        // verify DL quality durations
        CallQualitySummary ulSummary = mCallQualityMetrics.getCallQualitySummaryUl();
        assertEquals(5, ulSummary.totalGoodQualityDurationInSeconds);
        assertEquals(9, ulSummary.totalBadQualityDurationInSeconds);
        assertEquals(14, ulSummary.totalDurationWithQualityInformationInSeconds);

        // verify UL quality durations
        dlSummary = mCallQualityMetrics.getCallQualitySummaryDl();
        assertEquals(9, dlSummary.totalGoodQualityDurationInSeconds);
        assertEquals(5, dlSummary.totalBadQualityDurationInSeconds);
        assertEquals(14, dlSummary.totalDurationWithQualityInformationInSeconds);

        // verify DL quality durations
        ulSummary = mCallQualityMetrics.getCallQualitySummaryUl();
        assertEquals(5, ulSummary.totalGoodQualityDurationInSeconds);
        assertEquals(9, ulSummary.totalBadQualityDurationInSeconds);
        assertEquals(14, ulSummary.totalDurationWithQualityInformationInSeconds);
    }


    /**
     * Verify that good/bad quality and total duration stats are correct.
     *
     * Similar to testTotalDurations but we report the call quality out of order.
     */
    @Test
    public void testTotalDurations_ReportedOutOfOrder() {
        // Call quality in the following sequence:
        //
        // DL: GOOD       GOOD      BAD
        // UL: GOOD       BAD       GOOD
        // |----------|----------|--------|
        // 0          5          10       14
        //
        // 0s = Start of call. Assumed to be good quality
        // 5s = Switches to UL bad quality
        // 10s = Switches to UL good quality, DL bad quality
        // 14s = End of call. Switches to UL bad quality, DL good quality
        CallQuality cq1 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 5000);
        CallQuality cq2 = constructCallQuality(CallQuality.CALL_QUALITY_BAD,
                CallQuality.CALL_QUALITY_EXCELLENT, 10000);
        CallQuality cq3 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 14000);

        mCallQualityMetrics.saveCallQuality(cq1);
        mCallQualityMetrics.saveCallQuality(cq3);
        mCallQualityMetrics.saveCallQuality(cq2);

        // verify UL quality durations
        CallQualitySummary dlSummary = mCallQualityMetrics.getCallQualitySummaryDl();
        assertEquals(9, dlSummary.totalGoodQualityDurationInSeconds);
        assertEquals(5, dlSummary.totalBadQualityDurationInSeconds);
        assertEquals(14, dlSummary.totalDurationWithQualityInformationInSeconds);

        // verify DL quality durations
        CallQualitySummary ulSummary = mCallQualityMetrics.getCallQualitySummaryUl();
        assertEquals(5, ulSummary.totalGoodQualityDurationInSeconds);
        assertEquals(9, ulSummary.totalBadQualityDurationInSeconds);
        assertEquals(14, ulSummary.totalDurationWithQualityInformationInSeconds);
    }

    /**
     * Verify that a new CallQualityMetrics object is able to return empty summaries if no
     * CallQuality is reported for the duration of a call.
     */
    @Test
    @SmallTest
    public void testNoQualityReported() {
        // getting the summary for a new CallQualityMetrics object should not fail, and all
        // durations should be 0
@@ -129,7 +224,6 @@ public class CallQualityMetricsTest extends TelephonyTest {
     * ignored.
     */
    @Test
    @SmallTest
    public void testNotAvailableIsIgnored() {
        // CallQuality updates from the IMS service with CALL_QUALITY_NOT_AVAILABLE should be
        // ignored
@@ -156,7 +250,6 @@ public class CallQualityMetricsTest extends TelephonyTest {
     * this just tests for good quality since the logic is the same.
     */
    @Test
    @SmallTest
    public void testBestAndWorstSs() {
        // save good quality with high rssnr
        CallQuality cq1 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
@@ -201,7 +294,6 @@ public class CallQualityMetricsTest extends TelephonyTest {
     * likely that one field would be preserved and others would be lost.
     */
    @Test
    @SmallTest
    public void testSnapshotOfEndDuration() {
        CallQuality cq1 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 5000);
@@ -220,4 +312,31 @@ public class CallQualityMetricsTest extends TelephonyTest {
        CallQualitySummary ulSummary = mCallQualityMetrics.getCallQualitySummaryUl();
        assertEquals(14, ulSummary.snapshotOfEnd.durationInSeconds);
    }

    /**
     * Verifies that the snapshot of the end (the last reported call quality) is correct.
     * Currently this just checks the duration since the logic is all the same and it doesn't seem
     * likely that one field would be preserved and others would be lost.
     *
     * Similar to testSnapshotOfEndDuration but we report the call quality out of order
     */
    @Test
    public void testSnapshotOfEndDuration_ReportedOutOfOrder() {
        CallQuality cq1 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 5000);
        CallQuality cq2 = constructCallQuality(CallQuality.CALL_QUALITY_BAD,
                CallQuality.CALL_QUALITY_EXCELLENT, 10000);
        CallQuality cq3 = constructCallQuality(CallQuality.CALL_QUALITY_EXCELLENT,
                CallQuality.CALL_QUALITY_BAD, 14000);

        mCallQualityMetrics.saveCallQuality(cq1);
        mCallQualityMetrics.saveCallQuality(cq3);
        mCallQualityMetrics.saveCallQuality(cq2);

        // verify snapshot of end
        CallQualitySummary dlSummary = mCallQualityMetrics.getCallQualitySummaryDl();
        assertEquals(14, dlSummary.snapshotOfEnd.durationInSeconds);
        CallQualitySummary ulSummary = mCallQualityMetrics.getCallQualitySummaryUl();
        assertEquals(14, ulSummary.snapshotOfEnd.durationInSeconds);
    }
}