blob: a3f287c9a326e076821a77e8827cb6f419b1aa62 [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.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();
}
}