1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.telephony.metrics; 18 19 import static com.android.internal.telephony.metrics.TelephonyMetrics.toCallQualityProto; 20 21 import android.telephony.CallQuality; 22 import android.telephony.CellInfo; 23 import android.telephony.CellSignalStrengthLte; 24 import android.telephony.SignalStrength; 25 import android.util.Pair; 26 27 import com.android.internal.telephony.Phone; 28 import com.android.internal.telephony.ServiceStateTracker; 29 import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession; 30 import com.android.internal.telephony.util.TelephonyUtils; 31 import com.android.telephony.Rlog; 32 33 import java.util.ArrayList; 34 import java.util.Collections; 35 36 /** 37 * CallQualityMetrics is a utility for tracking the CallQuality during an ongoing call session. It 38 * processes snapshots throughout the call to keep track of info like the best and worst 39 * ServiceStates, durations of good and bad quality, and other summary statistics. 40 */ 41 public class CallQualityMetrics { 42 43 private static final String TAG = CallQualityMetrics.class.getSimpleName(); 44 45 // certain metrics are only logged on userdebug 46 private static final boolean IS_DEBUGGABLE = TelephonyUtils.IS_DEBUGGABLE; 47 48 // We only log the first MAX_SNAPSHOTS changes to CallQuality 49 private static final int MAX_SNAPSHOTS = 5; 50 51 // value of mCallQualityState which means the CallQuality is EXCELLENT/GOOD/FAIR 52 private static final int GOOD_QUALITY = 0; 53 54 // value of mCallQualityState which means the CallQuality is BAD/POOR 55 private static final int BAD_QUALITY = 1; 56 57 private Phone mPhone; 58 59 /** Snapshots of the call quality and SignalStrength (LTE-SNR for IMS calls) */ 60 // mUlSnapshots holds snapshots from uplink call quality changes. We log take snapshots of the 61 // first MAX_SNAPSHOTS transitions between good and bad quality 62 private ArrayList<Pair<CallQuality, Integer>> mUlSnapshots = new ArrayList<>(); 63 // mDlSnapshots holds snapshots from downlink call quality changes. We log take snapshots of 64 // the first MAX_SNAPSHOTS transitions between good and bad quality 65 private ArrayList<Pair<CallQuality, Integer>> mDlSnapshots = new ArrayList<>(); 66 67 // holds lightweight history of call quality and durations, used for calculating total time 68 // spent with bad and good quality for metrics and bugreports. This is separate from the 69 // snapshots because those are capped at MAX_SNAPSHOTS to avoid excessive memory use. 70 private ArrayList<TimestampedQualitySnapshot> mFullUplinkQuality = new ArrayList<>(); 71 private ArrayList<TimestampedQualitySnapshot> mFullDownlinkQuality = new ArrayList<>(); 72 73 // Current downlink call quality 74 private int mDlCallQualityState = GOOD_QUALITY; 75 76 // Current uplink call quality 77 private int mUlCallQualityState = GOOD_QUALITY; 78 79 // The last logged CallQuality 80 private CallQuality mLastCallQuality; 81 82 /** Snapshots taken at best and worst SignalStrengths */ 83 private Pair<CallQuality, Integer> mWorstSsWithGoodDlQuality; 84 private Pair<CallQuality, Integer> mBestSsWithGoodDlQuality; 85 private Pair<CallQuality, Integer> mWorstSsWithBadDlQuality; 86 private Pair<CallQuality, Integer> mBestSsWithBadDlQuality; 87 private Pair<CallQuality, Integer> mWorstSsWithGoodUlQuality; 88 private Pair<CallQuality, Integer> mBestSsWithGoodUlQuality; 89 private Pair<CallQuality, Integer> mWorstSsWithBadUlQuality; 90 private Pair<CallQuality, Integer> mBestSsWithBadUlQuality; 91 92 /** 93 * Construct a CallQualityMetrics object to be used to keep track of call quality for a single 94 * call session. 95 */ CallQualityMetrics(Phone phone)96 public CallQualityMetrics(Phone phone) { 97 mPhone = phone; 98 mLastCallQuality = new CallQuality(); 99 } 100 101 /** 102 * Called when call quality changes. 103 */ saveCallQuality(CallQuality cq)104 public void saveCallQuality(CallQuality cq) { 105 if (cq.getUplinkCallQualityLevel() == CallQuality.CALL_QUALITY_NOT_AVAILABLE 106 || cq.getDownlinkCallQualityLevel() == CallQuality.CALL_QUALITY_NOT_AVAILABLE) { 107 return; 108 } 109 110 // uplink and downlink call quality are tracked separately 111 int newUlCallQualityState = BAD_QUALITY; 112 int newDlCallQualityState = BAD_QUALITY; 113 if (isGoodQuality(cq.getUplinkCallQualityLevel())) { 114 newUlCallQualityState = GOOD_QUALITY; 115 } 116 if (isGoodQuality(cq.getDownlinkCallQualityLevel())) { 117 newDlCallQualityState = GOOD_QUALITY; 118 } 119 120 if (IS_DEBUGGABLE) { 121 if (newUlCallQualityState != mUlCallQualityState) { 122 addSnapshot(cq, mUlSnapshots); 123 } 124 if (newDlCallQualityState != mDlCallQualityState) { 125 addSnapshot(cq, mDlSnapshots); 126 } 127 } 128 129 updateTotalDurations(cq); 130 131 updateMinAndMaxSignalStrengthSnapshots(newDlCallQualityState, newUlCallQualityState, cq); 132 133 mUlCallQualityState = newUlCallQualityState; 134 mDlCallQualityState = newDlCallQualityState; 135 // call duration updates sometimes come out of order 136 if (cq.getCallDuration() > mLastCallQuality.getCallDuration()) { 137 mLastCallQuality = cq; 138 } 139 } 140 updateTotalDurations(CallQuality cq)141 private void updateTotalDurations(CallQuality cq) { 142 mFullDownlinkQuality.add(new TimestampedQualitySnapshot(cq.getCallDuration(), 143 cq.getDownlinkCallQualityLevel())); 144 mFullUplinkQuality.add(new TimestampedQualitySnapshot(cq.getCallDuration(), 145 cq.getUplinkCallQualityLevel())); 146 } 147 isGoodQuality(int callQualityLevel)148 private static boolean isGoodQuality(int callQualityLevel) { 149 return callQualityLevel < CallQuality.CALL_QUALITY_BAD; 150 } 151 152 /** 153 * Save a snapshot of the call quality and signal strength. This can be called with uplink or 154 * downlink call quality level. 155 */ addSnapshot(CallQuality cq, ArrayList<Pair<CallQuality, Integer>> snapshots)156 private void addSnapshot(CallQuality cq, ArrayList<Pair<CallQuality, Integer>> snapshots) { 157 if (snapshots.size() < MAX_SNAPSHOTS) { 158 Integer ss = getLteSnr(); 159 snapshots.add(Pair.create(cq, ss)); 160 } 161 } 162 163 /** 164 * Updates the snapshots saved when signal strength is highest and lowest while the call quality 165 * is good and bad for both uplink and downlink call quality. 166 * <p> 167 * At the end of the call we should have: 168 * - for both UL and DL: 169 * - snapshot of the best signal strength with bad call quality 170 * - snapshot of the worst signal strength with bad call quality 171 * - snapshot of the best signal strength with good call quality 172 * - snapshot of the worst signal strength with good call quality 173 */ updateMinAndMaxSignalStrengthSnapshots(int newDlCallQualityState, int newUlCallQualityState, CallQuality cq)174 private void updateMinAndMaxSignalStrengthSnapshots(int newDlCallQualityState, 175 int newUlCallQualityState, CallQuality cq) { 176 Integer ss = getLteSnr(); 177 if (ss.equals(CellInfo.UNAVAILABLE)) { 178 return; 179 } 180 181 // downlink 182 if (newDlCallQualityState == GOOD_QUALITY) { 183 if (mWorstSsWithGoodDlQuality == null || ss < mWorstSsWithGoodDlQuality.second) { 184 mWorstSsWithGoodDlQuality = Pair.create(cq, ss); 185 } 186 if (mBestSsWithGoodDlQuality == null || ss > mBestSsWithGoodDlQuality.second) { 187 mBestSsWithGoodDlQuality = Pair.create(cq, ss); 188 } 189 } else { 190 if (mWorstSsWithBadDlQuality == null || ss < mWorstSsWithBadDlQuality.second) { 191 mWorstSsWithBadDlQuality = Pair.create(cq, ss); 192 } 193 if (mBestSsWithBadDlQuality == null || ss > mBestSsWithBadDlQuality.second) { 194 mBestSsWithBadDlQuality = Pair.create(cq, ss); 195 } 196 } 197 198 // uplink 199 if (newUlCallQualityState == GOOD_QUALITY) { 200 if (mWorstSsWithGoodUlQuality == null || ss < mWorstSsWithGoodUlQuality.second) { 201 mWorstSsWithGoodUlQuality = Pair.create(cq, ss); 202 } 203 if (mBestSsWithGoodUlQuality == null || ss > mBestSsWithGoodUlQuality.second) { 204 mBestSsWithGoodUlQuality = Pair.create(cq, ss); 205 } 206 } else { 207 if (mWorstSsWithBadUlQuality == null || ss < mWorstSsWithBadUlQuality.second) { 208 mWorstSsWithBadUlQuality = Pair.create(cq, ss); 209 } 210 if (mBestSsWithBadUlQuality == null || ss > mBestSsWithBadUlQuality.second) { 211 mBestSsWithBadUlQuality = Pair.create(cq, ss); 212 } 213 } 214 } 215 216 // Returns the LTE signal to noise ratio, or 0 if unavailable getLteSnr()217 private Integer getLteSnr() { 218 ServiceStateTracker sst = mPhone.getDefaultPhone().getServiceStateTracker(); 219 if (sst == null) { 220 Rlog.e(TAG, "getLteSnr: unable to get SST for phone " + mPhone.getPhoneId()); 221 return CellInfo.UNAVAILABLE; 222 } 223 224 SignalStrength ss = sst.getSignalStrength(); 225 if (ss == null) { 226 Rlog.e(TAG, "getLteSnr: unable to get SignalStrength for phone " + mPhone.getPhoneId()); 227 return CellInfo.UNAVAILABLE; 228 } 229 230 // There may be multiple CellSignalStrengthLte, so try to use one with available SNR 231 for (CellSignalStrengthLte lteSs : ss.getCellSignalStrengths(CellSignalStrengthLte.class)) { 232 int snr = lteSs.getRssnr(); 233 if (snr != CellInfo.UNAVAILABLE) { 234 return snr; 235 } 236 } 237 238 return CellInfo.UNAVAILABLE; 239 } 240 toProto(int ss)241 private static TelephonyCallSession.Event.SignalStrength toProto(int ss) { 242 TelephonyCallSession.Event.SignalStrength ret = 243 new TelephonyCallSession.Event.SignalStrength(); 244 ret.lteSnr = ss; 245 return ret; 246 } 247 248 /** 249 * Return the full downlink CallQualitySummary using the saved CallQuality records. 250 */ getCallQualitySummaryDl()251 public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryDl() { 252 TelephonyCallSession.Event.CallQualitySummary summary = 253 new TelephonyCallSession.Event.CallQualitySummary(); 254 Pair<Integer, Integer> totalGoodAndBadDurations = getTotalGoodAndBadQualityTimeMs( 255 mFullDownlinkQuality); 256 summary.totalGoodQualityDurationInSeconds = totalGoodAndBadDurations.first / 1000; 257 summary.totalBadQualityDurationInSeconds = totalGoodAndBadDurations.second / 1000; 258 // This value could be different from mLastCallQuality.getCallDuration if we support 259 // handover from IMS->CS->IMS, but this is currently not possible 260 // TODO(b/130302396) this also may be possible when we put a call on hold and continue with 261 // another call 262 summary.totalDurationWithQualityInformationInSeconds = 263 mLastCallQuality.getCallDuration() / 1000; 264 if (mWorstSsWithGoodDlQuality != null) { 265 summary.snapshotOfWorstSsWithGoodQuality = 266 toCallQualityProto(mWorstSsWithGoodDlQuality.first); 267 summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodDlQuality.second); 268 } 269 if (mBestSsWithGoodDlQuality != null) { 270 summary.snapshotOfBestSsWithGoodQuality = 271 toCallQualityProto(mBestSsWithGoodDlQuality.first); 272 summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodDlQuality.second); 273 } 274 if (mWorstSsWithBadDlQuality != null) { 275 summary.snapshotOfWorstSsWithBadQuality = 276 toCallQualityProto(mWorstSsWithBadDlQuality.first); 277 summary.worstSsWithBadQuality = toProto(mWorstSsWithBadDlQuality.second); 278 } 279 if (mBestSsWithBadDlQuality != null) { 280 summary.snapshotOfBestSsWithBadQuality = 281 toCallQualityProto(mBestSsWithBadDlQuality.first); 282 summary.bestSsWithBadQuality = toProto(mBestSsWithBadDlQuality.second); 283 } 284 summary.snapshotOfEnd = toCallQualityProto(mLastCallQuality); 285 return summary; 286 } 287 288 /** 289 * Return the full uplink CallQualitySummary using the saved CallQuality records. 290 */ getCallQualitySummaryUl()291 public TelephonyCallSession.Event.CallQualitySummary getCallQualitySummaryUl() { 292 TelephonyCallSession.Event.CallQualitySummary summary = 293 new TelephonyCallSession.Event.CallQualitySummary(); 294 Pair<Integer, Integer> totalGoodAndBadDurations = getTotalGoodAndBadQualityTimeMs( 295 mFullUplinkQuality); 296 summary.totalGoodQualityDurationInSeconds = totalGoodAndBadDurations.first / 1000; 297 summary.totalBadQualityDurationInSeconds = totalGoodAndBadDurations.second / 1000; 298 // This value could be different from mLastCallQuality.getCallDuration if we support 299 // handover from IMS->CS->IMS, but this is currently not possible 300 // TODO(b/130302396) this also may be possible when we put a call on hold and continue with 301 // another call 302 summary.totalDurationWithQualityInformationInSeconds = 303 mLastCallQuality.getCallDuration() / 1000; 304 if (mWorstSsWithGoodUlQuality != null) { 305 summary.snapshotOfWorstSsWithGoodQuality = 306 toCallQualityProto(mWorstSsWithGoodUlQuality.first); 307 summary.worstSsWithGoodQuality = toProto(mWorstSsWithGoodUlQuality.second); 308 } 309 if (mBestSsWithGoodUlQuality != null) { 310 summary.snapshotOfBestSsWithGoodQuality = 311 toCallQualityProto(mBestSsWithGoodUlQuality.first); 312 summary.bestSsWithGoodQuality = toProto(mBestSsWithGoodUlQuality.second); 313 } 314 if (mWorstSsWithBadUlQuality != null) { 315 summary.snapshotOfWorstSsWithBadQuality = 316 toCallQualityProto(mWorstSsWithBadUlQuality.first); 317 summary.worstSsWithBadQuality = toProto(mWorstSsWithBadUlQuality.second); 318 } 319 if (mBestSsWithBadUlQuality != null) { 320 summary.snapshotOfBestSsWithBadQuality = 321 toCallQualityProto(mBestSsWithBadUlQuality.first); 322 summary.bestSsWithBadQuality = toProto(mBestSsWithBadUlQuality.second); 323 } 324 summary.snapshotOfEnd = toCallQualityProto(mLastCallQuality); 325 return summary; 326 } 327 328 329 /** 330 * Container class for call quality level and signal strength at the time of snapshot. This 331 * class implements compareTo so that it can be sorted by timestamp 332 */ 333 private class TimestampedQualitySnapshot implements Comparable<TimestampedQualitySnapshot> { 334 int mTimestampMs; 335 int mCallQualityLevel; 336 TimestampedQualitySnapshot(int timestamp, int cq)337 TimestampedQualitySnapshot(int timestamp, int cq) { 338 mTimestampMs = timestamp; 339 mCallQualityLevel = cq; 340 } 341 342 @Override compareTo(TimestampedQualitySnapshot o)343 public int compareTo(TimestampedQualitySnapshot o) { 344 return this.mTimestampMs - o.mTimestampMs; 345 } 346 347 @Override toString()348 public String toString() { 349 return "mTimestampMs=" + mTimestampMs + " mCallQualityLevel=" + mCallQualityLevel; 350 } 351 } 352 353 /** 354 * Use a list of snapshots to calculate and return the total time spent in a call with good 355 * quality and bad quality. 356 * This is slightly expensive since it involves sorting the snapshots by timestamp. 357 * 358 * @param snapshots a list of uplink or downlink snapshots 359 * @return a pair where the first element is the total good quality time and the second element 360 * is the total bad quality time 361 */ getTotalGoodAndBadQualityTimeMs( ArrayList<TimestampedQualitySnapshot> snapshots)362 private Pair<Integer, Integer> getTotalGoodAndBadQualityTimeMs( 363 ArrayList<TimestampedQualitySnapshot> snapshots) { 364 int totalGoodQualityTime = 0; 365 int totalBadQualityTime = 0; 366 int lastTimestamp = 0; 367 // sort by timestamp using TimestampedQualitySnapshot.compareTo 368 Collections.sort(snapshots); 369 for (TimestampedQualitySnapshot snapshot : snapshots) { 370 int timeSinceLastSnapshot = snapshot.mTimestampMs - lastTimestamp; 371 if (isGoodQuality(snapshot.mCallQualityLevel)) { 372 totalGoodQualityTime += timeSinceLastSnapshot; 373 } else { 374 totalBadQualityTime += timeSinceLastSnapshot; 375 } 376 lastTimestamp = snapshot.mTimestampMs; 377 } 378 379 return Pair.create(totalGoodQualityTime, totalBadQualityTime); 380 } 381 382 @Override toString()383 public String toString() { 384 StringBuilder sb = new StringBuilder(); 385 sb.append("[CallQualityMetrics phone "); 386 sb.append(mPhone.getPhoneId()); 387 sb.append(" mUlSnapshots: {"); 388 for (Pair<CallQuality, Integer> snapshot : mUlSnapshots) { 389 sb.append(" {cq="); 390 sb.append(snapshot.first); 391 sb.append(" ss="); 392 sb.append(snapshot.second); 393 sb.append("}"); 394 } 395 sb.append("}"); 396 sb.append(" mDlSnapshots:{"); 397 for (Pair<CallQuality, Integer> snapshot : mDlSnapshots) { 398 sb.append(" {cq="); 399 sb.append(snapshot.first); 400 sb.append(" ss="); 401 sb.append(snapshot.second); 402 sb.append("}"); 403 } 404 sb.append("}"); 405 sb.append(" "); 406 Pair<Integer, Integer> dlTotals = getTotalGoodAndBadQualityTimeMs(mFullDownlinkQuality); 407 Pair<Integer, Integer> ulTotals = getTotalGoodAndBadQualityTimeMs(mFullUplinkQuality); 408 sb.append(" TotalDlGoodQualityTimeMs: "); 409 sb.append(dlTotals.first); 410 sb.append(" TotalDlBadQualityTimeMs: "); 411 sb.append(dlTotals.second); 412 sb.append(" TotalUlGoodQualityTimeMs: "); 413 sb.append(ulTotals.first); 414 sb.append(" TotalUlBadQualityTimeMs: "); 415 sb.append(ulTotals.second); 416 sb.append("]"); 417 return sb.toString(); 418 } 419 } 420