| /* |
| * 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.getDefaultPhone().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() / 1000; |
| if (mWorstSsWithGoodDlQuality != null) { |
| summary.snapshotOfWorstSsWithGoodQuality = |
| toCallQualityProto(mWorstSsWithGoodDlQuality.first); |
| summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodDlQuality.second); |
| } |
| if (mBestSsWithGoodDlQuality != null) { |
| summary.snapshotOfBestSsWithGoodQuality = |
| toCallQualityProto(mBestSsWithGoodDlQuality.first); |
| summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodDlQuality.second); |
| } |
| if (mWorstSsWithBadDlQuality != null) { |
| summary.snapshotOfWorstSsWithBadQuality = |
| toCallQualityProto(mWorstSsWithBadDlQuality.first); |
| summary.worstSsWithBadQuality = toProto(mWorstSsWithBadDlQuality.second); |
| } |
| if (mBestSsWithBadDlQuality != null) { |
| summary.snapshotOfBestSsWithBadQuality = |
| toCallQualityProto(mBestSsWithBadDlQuality.first); |
| 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() / 1000; |
| if (mWorstSsWithGoodUlQuality != null) { |
| summary.snapshotOfWorstSsWithGoodQuality = |
| toCallQualityProto(mWorstSsWithGoodUlQuality.first); |
| summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodUlQuality.second); |
| } |
| if (mBestSsWithGoodUlQuality != null) { |
| summary.snapshotOfBestSsWithGoodQuality = |
| toCallQualityProto(mBestSsWithGoodUlQuality.first); |
| summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodUlQuality.second); |
| } |
| if (mWorstSsWithBadUlQuality != null) { |
| summary.snapshotOfWorstSsWithBadQuality = |
| toCallQualityProto(mWorstSsWithBadUlQuality.first); |
| summary.worstSsWithBadQuality = toProto(mWorstSsWithBadUlQuality.second); |
| } |
| if (mBestSsWithBadUlQuality != null) { |
| summary.snapshotOfBestSsWithBadQuality = |
| toCallQualityProto(mBestSsWithBadUlQuality.first); |
| 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(" {cq="); |
| sb.append(snapshot.first); |
| sb.append(" ss="); |
| sb.append(snapshot.second); |
| sb.append("}"); |
| } |
| sb.append("}"); |
| sb.append(" mDlSnapshots:{"); |
| for (Pair<CallQuality, Integer> snapshot : mDlSnapshots) { |
| sb.append(" {cq="); |
| sb.append(snapshot.first); |
| sb.append(" ss="); |
| sb.append(snapshot.second); |
| 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(); |
| } |
| } |