Loading src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java +27 −1 Original line number Diff line number Diff line Loading @@ -100,6 +100,7 @@ import com.android.internal.telephony.TelephonyProperties; import com.android.internal.telephony.dataconnection.DataEnabledSettings; import com.android.internal.telephony.dataconnection.DataEnabledSettings.DataEnabledChangedReason; import com.android.internal.telephony.gsm.SuppServiceNotification; import com.android.internal.telephony.metrics.CallQualityMetrics; import com.android.internal.telephony.metrics.TelephonyMetrics; import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState; import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession; Loading @@ -114,6 +115,7 @@ import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; Loading Loading @@ -151,6 +153,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { new MmTelFeature.MmTelCapabilities(); private TelephonyMetrics mMetrics; private final Map<String, CallQualityMetrics> mCallQualityMetrics = new ConcurrentHashMap<>(); private final ConcurrentLinkedQueue<String> mLeastRecentCallId = new ConcurrentLinkedQueue<>(); private boolean mCarrierConfigLoaded = false; private final MmTelFeatureListener mMmTelFeatureListener = new MmTelFeatureListener(); Loading Loading @@ -278,6 +282,10 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { static final int MAX_CONNECTIONS = 7; static final int MAX_CONNECTIONS_PER_CALL = 5; // Max number of calls we will keep call quality history for (the history is saved in-memory and // included in bug reports). private static final int MAX_CALL_QUALITY_HISTORY = 10; private static final int EVENT_HANGUP_PENDINGMO = 18; private static final int EVENT_DIAL_PENDINGMO = 20; private static final int EVENT_EXIT_ECBM_BEFORE_PENDINGMO = 21; Loading Loading @@ -2319,8 +2327,10 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { cause = DisconnectCause.IMS_MERGED_SUCCESSFULLY; } String callId = imsCall.getSession().getCallId(); mMetrics.writeOnImsCallTerminated(mPhone.getPhoneId(), imsCall.getCallSession(), reasonInfo); reasonInfo, mCallQualityMetrics.get(callId)); pruneCallQualityMetricsHistory(); mPhone.notifyImsReason(reasonInfo); if (reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL Loading Loading @@ -2948,6 +2958,14 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { // convert ServiceState.radioTech to TelephonyManager.NetworkType constant mPhone.onCallQualityChanged(callQuality, ServiceState.rilRadioTechnologyToNetworkType(imsCall.getRadioTechnology())); String callId = imsCall.getSession().getCallId(); CallQualityMetrics cqm = mCallQualityMetrics.get(callId); if (cqm == null) { cqm = new CallQualityMetrics(mPhone); mLeastRecentCallId.add(callId); } cqm.saveCallQuality(callQuality); mCallQualityMetrics.put(callId, cqm); } }; Loading Loading @@ -3500,6 +3518,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { pw.println(" mDefaultDialerUid=" + mDefaultDialerUid.get()); pw.println(" mVtDataUsageSnapshot=" + mVtDataUsageSnapshot); pw.println(" mVtDataUsageUidSnapshot=" + mVtDataUsageUidSnapshot); pw.println(" mCallQualityMetrics=" + mCallQualityMetrics); pw.flush(); pw.println("++++++++++++++++++++++++++++++++"); Loading Loading @@ -4076,6 +4095,13 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { mIsDataEnabled = isDataEnabled; } // Removes old call quality metrics if mCallQualityMetrics exceeds its max size private void pruneCallQualityMetricsHistory() { if (mCallQualityMetrics.size() > MAX_CALL_QUALITY_HISTORY) { mCallQualityMetrics.remove(mLeastRecentCallId.poll()); } } private void handleFeatureCapabilityChanged(ImsFeature.Capabilities capabilities) { boolean tmpIsVideoCallEnabled = isVideoCallEnabled(); // Check enabledFeatures to determine capabilities. We ignore disabledFeatures. Loading src/java/com/android/internal/telephony/metrics/CallQualityMetrics.java 0 → 100644 +345 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.internal.telephony.metrics; import static com.android.internal.telephony.metrics.TelephonyMetrics.toCallQualityProto; import android.os.Build; import android.telephony.CallQuality; import android.telephony.CellInfo; import android.telephony.CellSignalStrengthLte; import android.telephony.Rlog; import android.telephony.SignalStrength; import android.util.Pair; import com.android.internal.telephony.Phone; import com.android.internal.telephony.ServiceStateTracker; import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession; import java.util.ArrayList; /** * CallQualityMetrics is a utility for tracking the CallQuality during an ongoing call session. It * processes snapshots throughout the call to keep track of info like the best and worst * ServiceStates, durations of good and bad quality, and other summary statistics. */ public class CallQualityMetrics { private static final String TAG = CallQualityMetrics.class.getSimpleName(); // certain metrics are only logged on userdebug private static final boolean USERDEBUG_MODE = Build.IS_USERDEBUG; // We only log the first MAX_SNAPSHOTS changes to CallQuality private static final int MAX_SNAPSHOTS = 5; // value of mCallQualityState which means the CallQuality is EXCELLENT/GOOD/FAIR private static final int GOOD_QUALITY = 0; // value of mCallQualityState which means the CallQuality is BAD/POOR private static final int BAD_QUALITY = 1; private Phone mPhone; /** Snapshots of the call quality and SignalStrength (LTE-SNR for IMS calls) */ // mUlSnapshots holds snapshots from uplink call quality changes. We log take snapshots of the // first MAX_SNAPSHOTS transitions between good and bad quality private ArrayList<Pair<CallQuality, Integer>> mUlSnapshots = new ArrayList<>(); // mDlSnapshots holds snapshots from downlink call quality changes. We log take snapshots of // the first MAX_SNAPSHOTS transitions between good and bad quality private ArrayList<Pair<CallQuality, Integer>> mDlSnapshots = new ArrayList<>(); // Current downlink call quality private int mDlCallQualityState = GOOD_QUALITY; // Current uplink call quality private int mUlCallQualityState = GOOD_QUALITY; // The last logged CallQuality private CallQuality mLastCallQuality; /** Snapshots taken at best and worst SignalStrengths*/ private Pair<CallQuality, Integer> mWorstSsWithGoodDlQuality; private Pair<CallQuality, Integer> mBestSsWithGoodDlQuality; private Pair<CallQuality, Integer> mWorstSsWithBadDlQuality; private Pair<CallQuality, Integer> mBestSsWithBadDlQuality; private Pair<CallQuality, Integer> mWorstSsWithGoodUlQuality; private Pair<CallQuality, Integer> mBestSsWithGoodUlQuality; 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. */ public CallQualityMetrics(Phone phone) { mPhone = phone; mLastCallQuality = new CallQuality(); } /** * Called when call quality changes. */ public void saveCallQuality(CallQuality cq) { if (cq.getUplinkCallQualityLevel() == CallQuality.CALL_QUALITY_NOT_AVAILABLE || cq.getDownlinkCallQualityLevel() == CallQuality.CALL_QUALITY_NOT_AVAILABLE) { return; } // uplink and downlink call quality are tracked separately int newUlCallQualityState = BAD_QUALITY; int newDlCallQualityState = BAD_QUALITY; if (isGoodQuality(cq.getUplinkCallQualityLevel())) { newUlCallQualityState = GOOD_QUALITY; } if (isGoodQuality(cq.getDownlinkCallQualityLevel())) { newDlCallQualityState = GOOD_QUALITY; } if (USERDEBUG_MODE) { if (newUlCallQualityState != mUlCallQualityState) { mUlSnapshots = addSnapshot(cq, mUlSnapshots); } if (newDlCallQualityState != mDlCallQualityState) { mDlSnapshots = addSnapshot(cq, mDlSnapshots); } } updateTotalDurations(newDlCallQualityState, newUlCallQualityState, cq); updateMinAndMaxSignalStrengthSnapshots(newDlCallQualityState, newUlCallQualityState, cq); mUlCallQualityState = newUlCallQualityState; mDlCallQualityState = newDlCallQualityState; mLastCallQuality = cq; } private static boolean isGoodQuality(int callQualityLevel) { return callQualityLevel < CallQuality.CALL_QUALITY_BAD; } /** * 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) { 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; } } /** * Updates the snapshots saved when signal strength is highest and lowest while the call quality * is good and bad for both uplink and downlink call quality. * <p> * At the end of the call we should have: * - for both UL and DL: * - snapshot of the best signal strength with bad call quality * - snapshot of the worst signal strength with bad call quality * - snapshot of the best signal strength with good call quality * - snapshot of the worst signal strength with good call quality */ private void updateMinAndMaxSignalStrengthSnapshots(int newDlCallQualityState, int newUlCallQualityState, CallQuality cq) { Integer ss = getLteSnr(); if (ss.equals(CellInfo.UNAVAILABLE)) { return; } // downlink if (newDlCallQualityState == GOOD_QUALITY) { if (mWorstSsWithGoodDlQuality == null || ss < mWorstSsWithGoodDlQuality.second) { mWorstSsWithGoodDlQuality = Pair.create(cq, ss); } if (mBestSsWithGoodDlQuality == null || ss > mBestSsWithGoodDlQuality.second) { mBestSsWithGoodDlQuality = Pair.create(cq, ss); } } else { if (mWorstSsWithBadDlQuality == null || ss < mWorstSsWithBadDlQuality.second) { mWorstSsWithBadDlQuality = Pair.create(cq, ss); } if (mBestSsWithBadDlQuality == null || ss > mBestSsWithBadDlQuality.second) { mBestSsWithBadDlQuality = Pair.create(cq, ss); } } // uplink if (newUlCallQualityState == GOOD_QUALITY) { if (mWorstSsWithGoodUlQuality == null || ss < mWorstSsWithGoodUlQuality.second) { mWorstSsWithGoodUlQuality = Pair.create(cq, ss); } if (mBestSsWithGoodUlQuality == null || ss > mBestSsWithGoodUlQuality.second) { mBestSsWithGoodUlQuality = Pair.create(cq, ss); } } else { if (mWorstSsWithBadUlQuality == null || ss < mWorstSsWithBadUlQuality.second) { mWorstSsWithBadUlQuality = Pair.create(cq, ss); } if (mBestSsWithBadUlQuality == null || ss > mBestSsWithBadUlQuality.second) { mBestSsWithBadUlQuality = Pair.create(cq, ss); } } } // Returns the LTE signal to noise ratio, or 0 if unavailable private Integer getLteSnr() { ServiceStateTracker sst = mPhone.getServiceStateTracker(); if (sst == null) { Rlog.e(TAG, "getLteSnr: unable to get SST for phone " + mPhone.getPhoneId()); return CellInfo.UNAVAILABLE; } SignalStrength ss = sst.getSignalStrength(); if (ss == null) { Rlog.e(TAG, "getLteSnr: unable to get SignalStrength for phone " + mPhone.getPhoneId()); return CellInfo.UNAVAILABLE; } // There may be multiple CellSignalStrengthLte, so try to use one with available SNR for (CellSignalStrengthLte lteSs : ss.getCellSignalStrengths(CellSignalStrengthLte.class)) { int snr = lteSs.getRssnr(); if (snr != CellInfo.UNAVAILABLE) { return snr; } } return CellInfo.UNAVAILABLE; } private static TelephonyCallSession.Event.SignalStrength toProto(int ss) { TelephonyCallSession.Event.SignalStrength ret = new TelephonyCallSession.Event.SignalStrength(); ret.lteSnr = ss; return ret; } /** * Return the full downlink CallQualitySummary using the saved CallQuality records. */ public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryDl() { TelephonyCallSession.Event.CallQualitySummary summary = new TelephonyCallSession.Event.CallQualitySummary(); summary.totalGoodQualityDurationInSeconds = mTotalDlGoodQualityTimeMs / 1000; summary.totalBadQualityDurationInSeconds = mTotalDlBadQualityTimeMs / 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 // another call summary.totalDurationWithQualityInformationInSeconds = mLastCallQuality.getCallDuration(); summary.snapshotOfWorstSsWithGoodQuality = toCallQualityProto(mWorstSsWithGoodDlQuality.first); summary.snapshotOfBestSsWithGoodQuality = toCallQualityProto(mBestSsWithGoodDlQuality.first); summary.snapshotOfWorstSsWithBadQuality = toCallQualityProto(mWorstSsWithBadDlQuality.first); summary.snapshotOfBestSsWithBadQuality = toCallQualityProto(mBestSsWithBadDlQuality.first); summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodDlQuality.second); summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodDlQuality.second); summary.worstSsWithBadQuality = toProto(mWorstSsWithBadDlQuality.second); summary.bestSsWithBadQuality = toProto(mBestSsWithBadDlQuality.second); summary.snapshotOfEnd = toCallQualityProto(mLastCallQuality); return summary; } /** * Return the full uplink CallQualitySummary using the saved CallQuality records. */ public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryUl() { TelephonyCallSession.Event.CallQualitySummary summary = new TelephonyCallSession.Event.CallQualitySummary(); summary.totalGoodQualityDurationInSeconds = mTotalUlGoodQualityTimeMs / 1000; summary.totalBadQualityDurationInSeconds = mTotalUlBadQualityTimeMs / 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 // another call summary.totalDurationWithQualityInformationInSeconds = mLastCallQuality.getCallDuration(); summary.snapshotOfWorstSsWithGoodQuality = toCallQualityProto(mWorstSsWithGoodUlQuality.first); summary.snapshotOfBestSsWithGoodQuality = toCallQualityProto(mBestSsWithGoodUlQuality.first); summary.snapshotOfWorstSsWithBadQuality = toCallQualityProto(mWorstSsWithBadUlQuality.first); summary.snapshotOfBestSsWithBadQuality = toCallQualityProto(mBestSsWithBadUlQuality.first); summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodUlQuality.second); summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodUlQuality.second); summary.worstSsWithBadQuality = toProto(mWorstSsWithBadUlQuality.second); summary.bestSsWithBadQuality = toProto(mBestSsWithBadUlQuality.second); summary.snapshotOfEnd = toCallQualityProto(mLastCallQuality); return summary; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("[CallQualityMetrics phone "); sb.append(mPhone.getPhoneId()); sb.append(" mUlSnapshots: {"); for (Pair<CallQuality, Integer> snapshot : mUlSnapshots) { sb.append(" {"); sb.append(snapshot.first); sb.append("}"); } sb.append("}"); sb.append(" mDlSnapshots:{"); for (Pair<CallQuality, Integer> snapshot : mDlSnapshots) { sb.append(" {"); sb.append(snapshot.first); sb.append("}"); } 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); sb.append("]"); return sb.toString(); } } src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java +65 −6 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import android.os.Build; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Telephony.Sms.Intents; import android.telephony.CallQuality; import android.telephony.Rlog; import android.telephony.ServiceState; import android.telephony.SmsManager; Loading Loading @@ -1988,6 +1989,53 @@ public class TelephonyMetrics { return ri; } /** * Convert CallQuality to proto. * * @param callQuality call quality to convert * @return Coverted proto */ public static TelephonyCallSession.Event.CallQuality toCallQualityProto( CallQuality callQuality) { TelephonyCallSession.Event.CallQuality cq = new TelephonyCallSession.Event.CallQuality(); if (callQuality != null) { cq.downlinkLevel = callQualityLevelToProtoEnum(callQuality .getDownlinkCallQualityLevel()); cq.uplinkLevel = callQualityLevelToProtoEnum(callQuality.getUplinkCallQualityLevel()); // callDuration is reported in millis, so convert to seconds cq.durationInSeconds = callQuality.getCallDuration() / 1000; cq.rtpPacketsTransmitted = callQuality.getNumRtpPacketsTransmitted(); cq.rtpPacketsReceived = callQuality.getNumRtpPacketsReceived(); cq.rtpPacketsTransmittedLost = callQuality.getNumRtpPacketsTransmittedLost(); cq.rtpPacketsNotReceived = callQuality.getNumRtpPacketsNotReceived(); cq.averageRelativeJitterMillis = callQuality.getAverageRelativeJitter(); cq.maxRelativeJitterMillis = callQuality.getMaxRelativeJitter(); cq.codecType = convertImsCodec(callQuality.getCodecType()); } return cq; } /** * Convert Call quality level into proto defined value. */ private static int callQualityLevelToProtoEnum(int level) { if (level == CallQuality.CALL_QUALITY_EXCELLENT) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.EXCELLENT; } else if (level == CallQuality.CALL_QUALITY_GOOD) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.GOOD; } else if (level == CallQuality.CALL_QUALITY_FAIR) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.FAIR; } else if (level == CallQuality.CALL_QUALITY_POOR) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.POOR; } else if (level == CallQuality.CALL_QUALITY_BAD) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.BAD; } else if (level == CallQuality.CALL_QUALITY_NOT_AVAILABLE) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.NOT_AVAILABLE; } else { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.UNDEFINED; } } /** * Write IMS call end event * Loading @@ -1996,17 +2044,28 @@ public class TelephonyMetrics { * @param reasonInfo Call end reason */ public void writeOnImsCallTerminated(int phoneId, ImsCallSession session, ImsReasonInfo reasonInfo) { ImsReasonInfo reasonInfo, CallQualityMetrics cqm) { InProgressCallSession callSession = mInProgressCallSessions.get(phoneId); if (callSession == null) { Rlog.e(TAG, "Call session is missing"); } else { if (cqm != null) { callSession.addEvent( new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED) new CallSessionEventBuilder( TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED) .setCallIndex(getCallId(session)) .setImsReasonInfo(toImsReasonInfoProto(reasonInfo)) .setCallQualitySummaryDl(cqm.getCallQualitySummaryDl()) .setCallQualitySummaryUl(cqm.getCallQualitySummaryUl())); } else { callSession.addEvent( new CallSessionEventBuilder( TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED) .setCallIndex(getCallId(session)) .setImsReasonInfo(toImsReasonInfoProto(reasonInfo))); } } } /** * Write IMS call hangover event Loading Loading @@ -2394,7 +2453,7 @@ public class TelephonyMetrics { * @param c IMS codec value * @return Codec value defined in call session proto */ private int convertImsCodec(int c) { private static int convertImsCodec(int c) { switch (c) { case ImsStreamMediaProfile.AUDIO_QUALITY_AMR: return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_AMR; Loading tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java +1 −0 Original line number Diff line number Diff line Loading @@ -174,6 +174,7 @@ public class ImsPhoneCallTrackerTest extends TelephonyTest { }).when(imsCall).hold(); imsCall.attachSession(mImsCallSession); doReturn("1").when(mImsCallSession).getCallId(); } @Before Loading Loading
src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java +27 −1 Original line number Diff line number Diff line Loading @@ -100,6 +100,7 @@ import com.android.internal.telephony.TelephonyProperties; import com.android.internal.telephony.dataconnection.DataEnabledSettings; import com.android.internal.telephony.dataconnection.DataEnabledSettings.DataEnabledChangedReason; import com.android.internal.telephony.gsm.SuppServiceNotification; import com.android.internal.telephony.metrics.CallQualityMetrics; import com.android.internal.telephony.metrics.TelephonyMetrics; import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState; import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession; Loading @@ -114,6 +115,7 @@ import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; Loading Loading @@ -151,6 +153,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { new MmTelFeature.MmTelCapabilities(); private TelephonyMetrics mMetrics; private final Map<String, CallQualityMetrics> mCallQualityMetrics = new ConcurrentHashMap<>(); private final ConcurrentLinkedQueue<String> mLeastRecentCallId = new ConcurrentLinkedQueue<>(); private boolean mCarrierConfigLoaded = false; private final MmTelFeatureListener mMmTelFeatureListener = new MmTelFeatureListener(); Loading Loading @@ -278,6 +282,10 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { static final int MAX_CONNECTIONS = 7; static final int MAX_CONNECTIONS_PER_CALL = 5; // Max number of calls we will keep call quality history for (the history is saved in-memory and // included in bug reports). private static final int MAX_CALL_QUALITY_HISTORY = 10; private static final int EVENT_HANGUP_PENDINGMO = 18; private static final int EVENT_DIAL_PENDINGMO = 20; private static final int EVENT_EXIT_ECBM_BEFORE_PENDINGMO = 21; Loading Loading @@ -2319,8 +2327,10 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { cause = DisconnectCause.IMS_MERGED_SUCCESSFULLY; } String callId = imsCall.getSession().getCallId(); mMetrics.writeOnImsCallTerminated(mPhone.getPhoneId(), imsCall.getCallSession(), reasonInfo); reasonInfo, mCallQualityMetrics.get(callId)); pruneCallQualityMetricsHistory(); mPhone.notifyImsReason(reasonInfo); if (reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL Loading Loading @@ -2948,6 +2958,14 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { // convert ServiceState.radioTech to TelephonyManager.NetworkType constant mPhone.onCallQualityChanged(callQuality, ServiceState.rilRadioTechnologyToNetworkType(imsCall.getRadioTechnology())); String callId = imsCall.getSession().getCallId(); CallQualityMetrics cqm = mCallQualityMetrics.get(callId); if (cqm == null) { cqm = new CallQualityMetrics(mPhone); mLeastRecentCallId.add(callId); } cqm.saveCallQuality(callQuality); mCallQualityMetrics.put(callId, cqm); } }; Loading Loading @@ -3500,6 +3518,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { pw.println(" mDefaultDialerUid=" + mDefaultDialerUid.get()); pw.println(" mVtDataUsageSnapshot=" + mVtDataUsageSnapshot); pw.println(" mVtDataUsageUidSnapshot=" + mVtDataUsageUidSnapshot); pw.println(" mCallQualityMetrics=" + mCallQualityMetrics); pw.flush(); pw.println("++++++++++++++++++++++++++++++++"); Loading Loading @@ -4076,6 +4095,13 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall { mIsDataEnabled = isDataEnabled; } // Removes old call quality metrics if mCallQualityMetrics exceeds its max size private void pruneCallQualityMetricsHistory() { if (mCallQualityMetrics.size() > MAX_CALL_QUALITY_HISTORY) { mCallQualityMetrics.remove(mLeastRecentCallId.poll()); } } private void handleFeatureCapabilityChanged(ImsFeature.Capabilities capabilities) { boolean tmpIsVideoCallEnabled = isVideoCallEnabled(); // Check enabledFeatures to determine capabilities. We ignore disabledFeatures. Loading
src/java/com/android/internal/telephony/metrics/CallQualityMetrics.java 0 → 100644 +345 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.internal.telephony.metrics; import static com.android.internal.telephony.metrics.TelephonyMetrics.toCallQualityProto; import android.os.Build; import android.telephony.CallQuality; import android.telephony.CellInfo; import android.telephony.CellSignalStrengthLte; import android.telephony.Rlog; import android.telephony.SignalStrength; import android.util.Pair; import com.android.internal.telephony.Phone; import com.android.internal.telephony.ServiceStateTracker; import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession; import java.util.ArrayList; /** * CallQualityMetrics is a utility for tracking the CallQuality during an ongoing call session. It * processes snapshots throughout the call to keep track of info like the best and worst * ServiceStates, durations of good and bad quality, and other summary statistics. */ public class CallQualityMetrics { private static final String TAG = CallQualityMetrics.class.getSimpleName(); // certain metrics are only logged on userdebug private static final boolean USERDEBUG_MODE = Build.IS_USERDEBUG; // We only log the first MAX_SNAPSHOTS changes to CallQuality private static final int MAX_SNAPSHOTS = 5; // value of mCallQualityState which means the CallQuality is EXCELLENT/GOOD/FAIR private static final int GOOD_QUALITY = 0; // value of mCallQualityState which means the CallQuality is BAD/POOR private static final int BAD_QUALITY = 1; private Phone mPhone; /** Snapshots of the call quality and SignalStrength (LTE-SNR for IMS calls) */ // mUlSnapshots holds snapshots from uplink call quality changes. We log take snapshots of the // first MAX_SNAPSHOTS transitions between good and bad quality private ArrayList<Pair<CallQuality, Integer>> mUlSnapshots = new ArrayList<>(); // mDlSnapshots holds snapshots from downlink call quality changes. We log take snapshots of // the first MAX_SNAPSHOTS transitions between good and bad quality private ArrayList<Pair<CallQuality, Integer>> mDlSnapshots = new ArrayList<>(); // Current downlink call quality private int mDlCallQualityState = GOOD_QUALITY; // Current uplink call quality private int mUlCallQualityState = GOOD_QUALITY; // The last logged CallQuality private CallQuality mLastCallQuality; /** Snapshots taken at best and worst SignalStrengths*/ private Pair<CallQuality, Integer> mWorstSsWithGoodDlQuality; private Pair<CallQuality, Integer> mBestSsWithGoodDlQuality; private Pair<CallQuality, Integer> mWorstSsWithBadDlQuality; private Pair<CallQuality, Integer> mBestSsWithBadDlQuality; private Pair<CallQuality, Integer> mWorstSsWithGoodUlQuality; private Pair<CallQuality, Integer> mBestSsWithGoodUlQuality; 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. */ public CallQualityMetrics(Phone phone) { mPhone = phone; mLastCallQuality = new CallQuality(); } /** * Called when call quality changes. */ public void saveCallQuality(CallQuality cq) { if (cq.getUplinkCallQualityLevel() == CallQuality.CALL_QUALITY_NOT_AVAILABLE || cq.getDownlinkCallQualityLevel() == CallQuality.CALL_QUALITY_NOT_AVAILABLE) { return; } // uplink and downlink call quality are tracked separately int newUlCallQualityState = BAD_QUALITY; int newDlCallQualityState = BAD_QUALITY; if (isGoodQuality(cq.getUplinkCallQualityLevel())) { newUlCallQualityState = GOOD_QUALITY; } if (isGoodQuality(cq.getDownlinkCallQualityLevel())) { newDlCallQualityState = GOOD_QUALITY; } if (USERDEBUG_MODE) { if (newUlCallQualityState != mUlCallQualityState) { mUlSnapshots = addSnapshot(cq, mUlSnapshots); } if (newDlCallQualityState != mDlCallQualityState) { mDlSnapshots = addSnapshot(cq, mDlSnapshots); } } updateTotalDurations(newDlCallQualityState, newUlCallQualityState, cq); updateMinAndMaxSignalStrengthSnapshots(newDlCallQualityState, newUlCallQualityState, cq); mUlCallQualityState = newUlCallQualityState; mDlCallQualityState = newDlCallQualityState; mLastCallQuality = cq; } private static boolean isGoodQuality(int callQualityLevel) { return callQualityLevel < CallQuality.CALL_QUALITY_BAD; } /** * 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) { 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; } } /** * Updates the snapshots saved when signal strength is highest and lowest while the call quality * is good and bad for both uplink and downlink call quality. * <p> * At the end of the call we should have: * - for both UL and DL: * - snapshot of the best signal strength with bad call quality * - snapshot of the worst signal strength with bad call quality * - snapshot of the best signal strength with good call quality * - snapshot of the worst signal strength with good call quality */ private void updateMinAndMaxSignalStrengthSnapshots(int newDlCallQualityState, int newUlCallQualityState, CallQuality cq) { Integer ss = getLteSnr(); if (ss.equals(CellInfo.UNAVAILABLE)) { return; } // downlink if (newDlCallQualityState == GOOD_QUALITY) { if (mWorstSsWithGoodDlQuality == null || ss < mWorstSsWithGoodDlQuality.second) { mWorstSsWithGoodDlQuality = Pair.create(cq, ss); } if (mBestSsWithGoodDlQuality == null || ss > mBestSsWithGoodDlQuality.second) { mBestSsWithGoodDlQuality = Pair.create(cq, ss); } } else { if (mWorstSsWithBadDlQuality == null || ss < mWorstSsWithBadDlQuality.second) { mWorstSsWithBadDlQuality = Pair.create(cq, ss); } if (mBestSsWithBadDlQuality == null || ss > mBestSsWithBadDlQuality.second) { mBestSsWithBadDlQuality = Pair.create(cq, ss); } } // uplink if (newUlCallQualityState == GOOD_QUALITY) { if (mWorstSsWithGoodUlQuality == null || ss < mWorstSsWithGoodUlQuality.second) { mWorstSsWithGoodUlQuality = Pair.create(cq, ss); } if (mBestSsWithGoodUlQuality == null || ss > mBestSsWithGoodUlQuality.second) { mBestSsWithGoodUlQuality = Pair.create(cq, ss); } } else { if (mWorstSsWithBadUlQuality == null || ss < mWorstSsWithBadUlQuality.second) { mWorstSsWithBadUlQuality = Pair.create(cq, ss); } if (mBestSsWithBadUlQuality == null || ss > mBestSsWithBadUlQuality.second) { mBestSsWithBadUlQuality = Pair.create(cq, ss); } } } // Returns the LTE signal to noise ratio, or 0 if unavailable private Integer getLteSnr() { ServiceStateTracker sst = mPhone.getServiceStateTracker(); if (sst == null) { Rlog.e(TAG, "getLteSnr: unable to get SST for phone " + mPhone.getPhoneId()); return CellInfo.UNAVAILABLE; } SignalStrength ss = sst.getSignalStrength(); if (ss == null) { Rlog.e(TAG, "getLteSnr: unable to get SignalStrength for phone " + mPhone.getPhoneId()); return CellInfo.UNAVAILABLE; } // There may be multiple CellSignalStrengthLte, so try to use one with available SNR for (CellSignalStrengthLte lteSs : ss.getCellSignalStrengths(CellSignalStrengthLte.class)) { int snr = lteSs.getRssnr(); if (snr != CellInfo.UNAVAILABLE) { return snr; } } return CellInfo.UNAVAILABLE; } private static TelephonyCallSession.Event.SignalStrength toProto(int ss) { TelephonyCallSession.Event.SignalStrength ret = new TelephonyCallSession.Event.SignalStrength(); ret.lteSnr = ss; return ret; } /** * Return the full downlink CallQualitySummary using the saved CallQuality records. */ public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryDl() { TelephonyCallSession.Event.CallQualitySummary summary = new TelephonyCallSession.Event.CallQualitySummary(); summary.totalGoodQualityDurationInSeconds = mTotalDlGoodQualityTimeMs / 1000; summary.totalBadQualityDurationInSeconds = mTotalDlBadQualityTimeMs / 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 // another call summary.totalDurationWithQualityInformationInSeconds = mLastCallQuality.getCallDuration(); summary.snapshotOfWorstSsWithGoodQuality = toCallQualityProto(mWorstSsWithGoodDlQuality.first); summary.snapshotOfBestSsWithGoodQuality = toCallQualityProto(mBestSsWithGoodDlQuality.first); summary.snapshotOfWorstSsWithBadQuality = toCallQualityProto(mWorstSsWithBadDlQuality.first); summary.snapshotOfBestSsWithBadQuality = toCallQualityProto(mBestSsWithBadDlQuality.first); summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodDlQuality.second); summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodDlQuality.second); summary.worstSsWithBadQuality = toProto(mWorstSsWithBadDlQuality.second); summary.bestSsWithBadQuality = toProto(mBestSsWithBadDlQuality.second); summary.snapshotOfEnd = toCallQualityProto(mLastCallQuality); return summary; } /** * Return the full uplink CallQualitySummary using the saved CallQuality records. */ public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryUl() { TelephonyCallSession.Event.CallQualitySummary summary = new TelephonyCallSession.Event.CallQualitySummary(); summary.totalGoodQualityDurationInSeconds = mTotalUlGoodQualityTimeMs / 1000; summary.totalBadQualityDurationInSeconds = mTotalUlBadQualityTimeMs / 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 // another call summary.totalDurationWithQualityInformationInSeconds = mLastCallQuality.getCallDuration(); summary.snapshotOfWorstSsWithGoodQuality = toCallQualityProto(mWorstSsWithGoodUlQuality.first); summary.snapshotOfBestSsWithGoodQuality = toCallQualityProto(mBestSsWithGoodUlQuality.first); summary.snapshotOfWorstSsWithBadQuality = toCallQualityProto(mWorstSsWithBadUlQuality.first); summary.snapshotOfBestSsWithBadQuality = toCallQualityProto(mBestSsWithBadUlQuality.first); summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodUlQuality.second); summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodUlQuality.second); summary.worstSsWithBadQuality = toProto(mWorstSsWithBadUlQuality.second); summary.bestSsWithBadQuality = toProto(mBestSsWithBadUlQuality.second); summary.snapshotOfEnd = toCallQualityProto(mLastCallQuality); return summary; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("[CallQualityMetrics phone "); sb.append(mPhone.getPhoneId()); sb.append(" mUlSnapshots: {"); for (Pair<CallQuality, Integer> snapshot : mUlSnapshots) { sb.append(" {"); sb.append(snapshot.first); sb.append("}"); } sb.append("}"); sb.append(" mDlSnapshots:{"); for (Pair<CallQuality, Integer> snapshot : mDlSnapshots) { sb.append(" {"); sb.append(snapshot.first); sb.append("}"); } 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); sb.append("]"); return sb.toString(); } }
src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java +65 −6 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import android.os.Build; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Telephony.Sms.Intents; import android.telephony.CallQuality; import android.telephony.Rlog; import android.telephony.ServiceState; import android.telephony.SmsManager; Loading Loading @@ -1988,6 +1989,53 @@ public class TelephonyMetrics { return ri; } /** * Convert CallQuality to proto. * * @param callQuality call quality to convert * @return Coverted proto */ public static TelephonyCallSession.Event.CallQuality toCallQualityProto( CallQuality callQuality) { TelephonyCallSession.Event.CallQuality cq = new TelephonyCallSession.Event.CallQuality(); if (callQuality != null) { cq.downlinkLevel = callQualityLevelToProtoEnum(callQuality .getDownlinkCallQualityLevel()); cq.uplinkLevel = callQualityLevelToProtoEnum(callQuality.getUplinkCallQualityLevel()); // callDuration is reported in millis, so convert to seconds cq.durationInSeconds = callQuality.getCallDuration() / 1000; cq.rtpPacketsTransmitted = callQuality.getNumRtpPacketsTransmitted(); cq.rtpPacketsReceived = callQuality.getNumRtpPacketsReceived(); cq.rtpPacketsTransmittedLost = callQuality.getNumRtpPacketsTransmittedLost(); cq.rtpPacketsNotReceived = callQuality.getNumRtpPacketsNotReceived(); cq.averageRelativeJitterMillis = callQuality.getAverageRelativeJitter(); cq.maxRelativeJitterMillis = callQuality.getMaxRelativeJitter(); cq.codecType = convertImsCodec(callQuality.getCodecType()); } return cq; } /** * Convert Call quality level into proto defined value. */ private static int callQualityLevelToProtoEnum(int level) { if (level == CallQuality.CALL_QUALITY_EXCELLENT) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.EXCELLENT; } else if (level == CallQuality.CALL_QUALITY_GOOD) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.GOOD; } else if (level == CallQuality.CALL_QUALITY_FAIR) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.FAIR; } else if (level == CallQuality.CALL_QUALITY_POOR) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.POOR; } else if (level == CallQuality.CALL_QUALITY_BAD) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.BAD; } else if (level == CallQuality.CALL_QUALITY_NOT_AVAILABLE) { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.NOT_AVAILABLE; } else { return TelephonyCallSession.Event.CallQuality.CallQualityLevel.UNDEFINED; } } /** * Write IMS call end event * Loading @@ -1996,17 +2044,28 @@ public class TelephonyMetrics { * @param reasonInfo Call end reason */ public void writeOnImsCallTerminated(int phoneId, ImsCallSession session, ImsReasonInfo reasonInfo) { ImsReasonInfo reasonInfo, CallQualityMetrics cqm) { InProgressCallSession callSession = mInProgressCallSessions.get(phoneId); if (callSession == null) { Rlog.e(TAG, "Call session is missing"); } else { if (cqm != null) { callSession.addEvent( new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED) new CallSessionEventBuilder( TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED) .setCallIndex(getCallId(session)) .setImsReasonInfo(toImsReasonInfoProto(reasonInfo)) .setCallQualitySummaryDl(cqm.getCallQualitySummaryDl()) .setCallQualitySummaryUl(cqm.getCallQualitySummaryUl())); } else { callSession.addEvent( new CallSessionEventBuilder( TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED) .setCallIndex(getCallId(session)) .setImsReasonInfo(toImsReasonInfoProto(reasonInfo))); } } } /** * Write IMS call hangover event Loading Loading @@ -2394,7 +2453,7 @@ public class TelephonyMetrics { * @param c IMS codec value * @return Codec value defined in call session proto */ private int convertImsCodec(int c) { private static int convertImsCodec(int c) { switch (c) { case ImsStreamMediaProfile.AUDIO_QUALITY_AMR: return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_AMR; Loading
tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java +1 −0 Original line number Diff line number Diff line Loading @@ -174,6 +174,7 @@ public class ImsPhoneCallTrackerTest extends TelephonyTest { }).when(imsCall).hold(); imsCall.attachSession(mImsCallSession); doReturn("1").when(mImsCallSession).getCallId(); } @Before Loading