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