blob: cf98acb871ba32db1e1db478f7b08bbe71ce055f [file] [log] [blame]
/*
* 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.telephony.CallQuality;
import android.telephony.CellInfo;
import android.telephony.CellSignalStrengthLte;
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 com.android.internal.telephony.util.TelephonyUtils;
import com.android.telephony.Rlog;
import java.util.ArrayList;
import java.util.Collections;
/**
* 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 IS_DEBUGGABLE = TelephonyUtils.IS_DEBUGGABLE;
// 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<>();
// 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;
// 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;
/**
* 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 (IS_DEBUGGABLE) {
if (newUlCallQualityState != mUlCallQualityState) {
addSnapshot(cq, mUlSnapshots);
}
if (newDlCallQualityState != mDlCallQualityState) {
addSnapshot(cq, mDlSnapshots);
}
}
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;
}
/**
* Save a snapshot of the call quality and signal strength. This can be called with uplink or
* downlink call quality level.
*/
private void addSnapshot(CallQuality cq, ArrayList<Pair<CallQuality, Integer>> snapshots) {
if (snapshots.size() < MAX_SNAPSHOTS) {
Integer ss = getLteSnr();
snapshots.add(Pair.create(cq, ss));
}
}
/**
* 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();
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
// 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();
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
// 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;
}
/**
* 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();
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(" ");
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();
}
}