1 /*
2  * Copyright (C) 2015 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.server.telecom;
18 
19 import static java.util.Map.entry;
20 
21 import android.content.Context;
22 import android.os.SystemProperties;
23 
24 import android.telecom.Connection;
25 import android.telecom.DisconnectCause;
26 import android.telecom.Logging.EventManager;
27 import android.telecom.ParcelableCallAnalytics;
28 import android.telecom.TelecomAnalytics;
29 import android.telecom.TelecomManager;
30 import android.telephony.SubscriptionInfo;
31 import android.telephony.SubscriptionManager;
32 import android.util.Base64;
33 import android.telecom.Log;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.IndentingPrintWriter;
37 import com.android.server.telecom.nano.TelecomLogClass;
38 
39 import java.io.PrintWriter;
40 import java.time.Instant;
41 import java.time.ZoneOffset;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collections;
45 import java.util.Comparator;
46 import java.util.HashMap;
47 import java.util.LinkedList;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.concurrent.LinkedBlockingDeque;
51 import java.util.stream.Collectors;
52 
53 import static android.provider.CallLog.Calls.AUTO_MISSED_EMERGENCY_CALL;
54 import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_DIALING;
55 import static android.provider.CallLog.Calls.AUTO_MISSED_MAXIMUM_RINGING;
56 import static android.provider.CallLog.Calls.USER_MISSED_CALL_FILTERS_TIMEOUT;
57 import static android.provider.CallLog.Calls.USER_MISSED_CALL_SCREENING_SERVICE_SILENCED;
58 import static android.provider.CallLog.Calls.USER_MISSED_DND_MODE;
59 import static android.provider.CallLog.Calls.USER_MISSED_LOW_RING_VOLUME;
60 import static android.provider.CallLog.Calls.USER_MISSED_NEVER_RANG;
61 import static android.provider.CallLog.Calls.USER_MISSED_NO_VIBRATE;
62 import static android.provider.CallLog.Calls.USER_MISSED_SHORT_RING;
63 import static android.telecom.ParcelableCallAnalytics.AnalyticsEvent;
64 import static android.telecom.TelecomAnalytics.SessionTiming;
65 
66 /**
67  * A class that collects and stores data on how calls are being made, in order to
68  * aggregate these into useful statistics.
69  */
70 public class Analytics {
71     public static final String ANALYTICS_DUMPSYS_ARG = "analytics";
72     private static final String CLEAR_ANALYTICS_ARG = "clear";
73 
74     public static final Map<String, Integer> sLogEventToAnalyticsEvent = Map.ofEntries(
75             entry(LogUtils.Events.SET_SELECT_PHONE_ACCOUNT,
76                     AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT),
77             entry(LogUtils.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD),
78             entry(LogUtils.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD),
79             entry(LogUtils.Events.SWAP, AnalyticsEvent.SWAP),
80             entry(LogUtils.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING),
81             entry(LogUtils.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH),
82             entry(LogUtils.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE),
83             entry(LogUtils.Events.SET_PARENT, AnalyticsEvent.SET_PARENT),
84             entry(LogUtils.Events.MUTE, AnalyticsEvent.MUTE),
85             entry(LogUtils.Events.UNMUTE, AnalyticsEvent.UNMUTE),
86             entry(LogUtils.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT),
87             entry(LogUtils.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE),
88             entry(LogUtils.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET),
89             entry(LogUtils.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER),
90             entry(LogUtils.Events.SILENCE, AnalyticsEvent.SILENCE),
91             entry(LogUtils.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED),
92             entry(LogUtils.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED),
93             entry(LogUtils.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED),
94             entry(LogUtils.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD),
95             entry(LogUtils.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD),
96             entry(LogUtils.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL),
97             entry(LogUtils.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT),
98             entry(LogUtils.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT),
99             entry(LogUtils.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE),
100             entry(LogUtils.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED),
101             entry(LogUtils.Events.SET_HOLD, AnalyticsEvent.SET_HOLD),
102             entry(LogUtils.Events.SET_DIALING, AnalyticsEvent.SET_DIALING),
103             entry(LogUtils.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION),
104             entry(LogUtils.Events.BIND_CS, AnalyticsEvent.BIND_CS),
105             entry(LogUtils.Events.CS_BOUND, AnalyticsEvent.CS_BOUND),
106             entry(LogUtils.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT),
107             entry(LogUtils.Events.DIRECT_TO_VM_INITIATED,
108                     AnalyticsEvent.DIRECT_TO_VM_INITIATED),
109             entry(LogUtils.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED),
110             entry(LogUtils.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED),
111             entry(LogUtils.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED),
112             entry(LogUtils.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT),
113             entry(LogUtils.Events.DND_PRE_CHECK_INITIATED, AnalyticsEvent.DND_CHECK_INITIATED),
114             entry(LogUtils.Events.DND_PRE_CHECK_COMPLETED, AnalyticsEvent.DND_CHECK_COMPLETED));
115 
116     public static final Map<String, Integer> sLogSessionToSessionId = Map.ofEntries(
117             entry(LogUtils.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL),
118             entry(LogUtils.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL),
119             entry(LogUtils.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL),
120             entry(LogUtils.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL),
121             entry(LogUtils.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL),
122             entry(LogUtils.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE),
123             entry(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE),
124             entry(LogUtils.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE),
125             entry(LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
126                     SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE),
127             entry(LogUtils.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE),
128             entry(LogUtils.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING),
129             entry(LogUtils.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING),
130             entry(LogUtils.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED),
131             entry(LogUtils.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD),
132             entry(LogUtils.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL),
133             entry(LogUtils.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED),
134             entry(LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
135                     SessionTiming.CSW_ADD_CONFERENCE_CALL));
136 
137     public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming = Map.ofEntries(
138             entry(LogUtils.Events.Timings.ACCEPT_TIMING,
139                     ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING),
140             entry(LogUtils.Events.Timings.REJECT_TIMING,
141                     ParcelableCallAnalytics.EventTiming.REJECT_TIMING),
142             entry(LogUtils.Events.Timings.DISCONNECT_TIMING,
143                     ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING),
144             entry(LogUtils.Events.Timings.HOLD_TIMING,
145                     ParcelableCallAnalytics.EventTiming.HOLD_TIMING),
146             entry(LogUtils.Events.Timings.UNHOLD_TIMING,
147                     ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING),
148             entry(LogUtils.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING,
149                     ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING),
150             entry(LogUtils.Events.Timings.BIND_CS_TIMING,
151                     ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING),
152             entry(LogUtils.Events.Timings.SCREENING_COMPLETED_TIMING,
153                     ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING),
154             entry(LogUtils.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING,
155                     ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING),
156             entry(LogUtils.Events.Timings.BLOCK_CHECK_FINISHED_TIMING,
157                     ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING),
158             entry(LogUtils.Events.Timings.FILTERING_COMPLETED_TIMING,
159                     ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING),
160             entry(LogUtils.Events.Timings.FILTERING_TIMED_OUT_TIMING,
161                     ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING),
162             entry(LogUtils.Events.Timings.START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING,
163                     ParcelableCallAnalytics.EventTiming.
164                             START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING),
165             entry(LogUtils.Events.Timings.DND_PRE_CHECK_COMPLETED_TIMING,
166                     ParcelableCallAnalytics.EventTiming.DND_PRE_CALL_PRE_CHECK_TIMING));
167 
168     public static final Map<Integer, String> sSessionIdToLogSession = new HashMap<>();
169 
170     static {
171         for (Map.Entry<String, Integer> e : sLogSessionToSessionId.entrySet()) {
e.getValue()172             sSessionIdToLogSession.put(e.getValue(), e.getKey());
173         }
174     }
175 
176     public static class CallInfo {
setCallStartTime(long startTime)177         public void setCallStartTime(long startTime) {
178         }
179 
setCallEndTime(long endTime)180         public void setCallEndTime(long endTime) {
181         }
182 
setCallIsAdditional(boolean isAdditional)183         public void setCallIsAdditional(boolean isAdditional) {
184         }
185 
setCallIsEmergency(boolean isEmergency)186         public void setCallIsEmergency(boolean isEmergency) {
187         }
188 
setCallIsInterrupted(boolean isInterrupted)189         public void setCallIsInterrupted(boolean isInterrupted) {
190         }
191 
setCallDisconnectCause(DisconnectCause disconnectCause)192         public void setCallDisconnectCause(DisconnectCause disconnectCause) {
193         }
194 
addCallTechnology(int callTechnology)195         public void addCallTechnology(int callTechnology) {
196         }
197 
setCreatedFromExistingConnection(boolean createdFromExistingConnection)198         public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
199         }
200 
setCallConnectionService(String connectionServiceName)201         public void setCallConnectionService(String connectionServiceName) {
202         }
203 
setCallEvents(EventManager.EventRecord records)204         public void setCallEvents(EventManager.EventRecord records) {
205         }
206 
setCallIsVideo(boolean isVideo)207         public void setCallIsVideo(boolean isVideo) {
208         }
209 
addVideoEvent(int eventId, int videoState)210         public void addVideoEvent(int eventId, int videoState) {
211         }
212 
addInCallService(String serviceName, int type, long boundDuration, boolean isNullBinding)213         public void addInCallService(String serviceName, int type, long boundDuration,
214                 boolean isNullBinding) {
215         }
216 
addCallProperties(int properties)217         public void addCallProperties(int properties) {
218         }
219 
setCallSource(int callSource)220         public void setCallSource(int callSource) {
221         }
222 
setMissedReason(long missedReason)223         public void setMissedReason(long missedReason) {
224         }
225     }
226 
227     /**
228      * A class that holds data associated with a call.
229      */
230     @VisibleForTesting
231     public static class CallInfoImpl extends CallInfo {
232         public String callId;
233         public long startTime;  // start time in milliseconds since the epoch. 0 if not yet set.
234         public long endTime;  // end time in milliseconds since the epoch. 0 if not yet set.
235         public int callDirection;  // one of UNKNOWN_DIRECTION, INCOMING_DIRECTION,
236         // or OUTGOING_DIRECTION.
237         public boolean isAdditionalCall = false;  // true if the call came in while another call was
238         // in progress or if the user dialed this call
239         // while in the middle of another call.
240         public boolean isInterrupted = false;  // true if the call was interrupted by an incoming
241         // or outgoing call.
242         public int callTechnologies;  // bitmask denoting which technologies a call used.
243 
244         // true if the Telecom Call object was created from an existing connection via
245         // CallsManager#createCallForExistingConnection, for example, by ImsConference.
246         public boolean createdFromExistingConnection = false;
247 
248         public DisconnectCause callTerminationReason;
249         public String connectionService;
250         public boolean isEmergency = false;
251 
252         public EventManager.EventRecord callEvents;
253 
254         public boolean isVideo = false;
255         public List<TelecomLogClass.VideoEvent> videoEvents;
256         public List<TelecomLogClass.InCallServiceInfo> inCallServiceInfos;
257         public int callProperties = 0;
258         public int callSource = CALL_SOURCE_UNSPECIFIED;
259         public long missedReason;
260 
261         private long mTimeOfLastVideoEvent = -1;
262 
CallInfoImpl(String callId, int callDirection)263         CallInfoImpl(String callId, int callDirection) {
264             this.callId = callId;
265             startTime = 0;
266             endTime = 0;
267             this.callDirection = callDirection;
268             callTechnologies = 0;
269             connectionService = "";
270             videoEvents = new LinkedList<>();
271             inCallServiceInfos = new LinkedList<>();
272             missedReason = 0;
273         }
274 
CallInfoImpl(CallInfoImpl other)275         CallInfoImpl(CallInfoImpl other) {
276             this.callId = other.callId;
277             this.startTime = other.startTime;
278             this.endTime = other.endTime;
279             this.callDirection = other.callDirection;
280             this.isAdditionalCall = other.isAdditionalCall;
281             this.isInterrupted = other.isInterrupted;
282             this.callTechnologies = other.callTechnologies;
283             this.createdFromExistingConnection = other.createdFromExistingConnection;
284             this.connectionService = other.connectionService;
285             this.isEmergency = other.isEmergency;
286             this.callEvents = other.callEvents;
287             this.isVideo = other.isVideo;
288             this.videoEvents = other.videoEvents;
289             this.callProperties = other.callProperties;
290             this.callSource = other.callSource;
291             this.missedReason = other.missedReason;
292 
293             if (other.callTerminationReason != null) {
294                 this.callTerminationReason = new DisconnectCause(
295                         other.callTerminationReason.getCode(),
296                         other.callTerminationReason.getLabel(),
297                         other.callTerminationReason.getDescription(),
298                         other.callTerminationReason.getReason(),
299                         other.callTerminationReason.getTone());
300             } else {
301                 this.callTerminationReason = null;
302             }
303         }
304 
305         @Override
setCallStartTime(long startTime)306         public void setCallStartTime(long startTime) {
307             Log.d(TAG, "setting startTime for call " + callId + " to " + startTime);
308             this.startTime = startTime;
309         }
310 
311         @Override
setCallEndTime(long endTime)312         public void setCallEndTime(long endTime) {
313             Log.d(TAG, "setting endTime for call " + callId + " to " + endTime);
314             this.endTime = endTime;
315         }
316 
317         @Override
setCallIsAdditional(boolean isAdditional)318         public void setCallIsAdditional(boolean isAdditional) {
319             Log.d(TAG, "setting isAdditional for call " + callId + " to " + isAdditional);
320             this.isAdditionalCall = isAdditional;
321         }
322 
323         @Override
setCallIsInterrupted(boolean isInterrupted)324         public void setCallIsInterrupted(boolean isInterrupted) {
325             Log.d(TAG, "setting isInterrupted for call " + callId + " to " + isInterrupted);
326             this.isInterrupted = isInterrupted;
327         }
328 
329         @Override
addCallTechnology(int callTechnology)330         public void addCallTechnology(int callTechnology) {
331             Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology);
332             this.callTechnologies |= callTechnology;
333         }
334 
335         @Override
setCallIsEmergency(boolean isEmergency)336         public void setCallIsEmergency(boolean isEmergency) {
337             Log.d(TAG, "setting call as emergency: " + isEmergency);
338             this.isEmergency = isEmergency;
339         }
340 
341         @Override
setCallDisconnectCause(DisconnectCause disconnectCause)342         public void setCallDisconnectCause(DisconnectCause disconnectCause) {
343             Log.d(TAG, "setting disconnectCause for call " + callId + " to " + disconnectCause);
344             this.callTerminationReason = disconnectCause;
345         }
346 
347         @Override
setCreatedFromExistingConnection(boolean createdFromExistingConnection)348         public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
349             Log.d(TAG, "setting createdFromExistingConnection for call " + callId + " to "
350                     + createdFromExistingConnection);
351             this.createdFromExistingConnection = createdFromExistingConnection;
352         }
353 
354         @Override
setCallConnectionService(String connectionServiceName)355         public void setCallConnectionService(String connectionServiceName) {
356             Log.d(TAG, "setting connection service for call " + callId + ": "
357                     + connectionServiceName);
358             this.connectionService = connectionServiceName;
359         }
360 
361         @Override
setMissedReason(long missedReason)362         public void setMissedReason(long missedReason) {
363             Log.d(TAG, "setting missedReason for call " + callId + ": "
364                     + missedReason);
365             this.missedReason = missedReason;
366         }
367 
368         @Override
setCallEvents(EventManager.EventRecord records)369         public void setCallEvents(EventManager.EventRecord records) {
370             this.callEvents = records;
371         }
372 
373         @Override
setCallIsVideo(boolean isVideo)374         public void setCallIsVideo(boolean isVideo) {
375             this.isVideo = isVideo;
376         }
377 
378         @Override
addVideoEvent(int eventId, int videoState)379         public void addVideoEvent(int eventId, int videoState) {
380             long timeSinceLastEvent;
381             long currentTime = System.currentTimeMillis();
382             if (mTimeOfLastVideoEvent < 0) {
383                 timeSinceLastEvent = -1;
384             } else {
385                 timeSinceLastEvent = roundToOneSigFig(currentTime - mTimeOfLastVideoEvent);
386             }
387             mTimeOfLastVideoEvent = currentTime;
388 
389             videoEvents.add(new TelecomLogClass.VideoEvent()
390                     .setEventName(eventId)
391                     .setTimeSinceLastEventMillis(timeSinceLastEvent)
392                     .setVideoState(videoState));
393         }
394 
395         @Override
addInCallService(String serviceName, int type, long boundDuration, boolean isNullBinding)396         public void addInCallService(String serviceName, int type, long boundDuration,
397                 boolean isNullBinding) {
398             inCallServiceInfos.add(new TelecomLogClass.InCallServiceInfo()
399                     .setInCallServiceName(serviceName)
400                     .setInCallServiceType(type)
401                     .setBoundDurationMillis(boundDuration)
402                     .setIsNullBinding(isNullBinding));
403         }
404 
405         @Override
addCallProperties(int properties)406         public void addCallProperties(int properties) {
407             this.callProperties |= properties;
408         }
409 
410         @Override
setCallSource(int callSource)411         public void setCallSource(int callSource) {
412             this.callSource = callSource;
413         }
414 
415         @Override
toString()416         public String toString() {
417             return "{\n"
418                     + "    startTime: " + startTime + '\n'
419                     + "    endTime: " + endTime + '\n'
420                     + "    direction: " + getCallDirectionString() + '\n'
421                     + "    isAdditionalCall: " + isAdditionalCall + '\n'
422                     + "    isInterrupted: " + isInterrupted + '\n'
423                     + "    isEmergency: " + isEmergency + '\n'
424                     + "    callTechnologies: " + getCallTechnologiesAsString() + '\n'
425                     + "    callTerminationReason: " + getCallDisconnectReasonString() + '\n'
426                     + "    missedReason: " + getMissedReasonString() + '\n'
427                     + "    connectionService: " + connectionService + '\n'
428                     + "    isVideoCall: " + isVideo + '\n'
429                     + "    inCallServices: " + getInCallServicesString() + '\n'
430                     + "    callProperties: " + Connection.propertiesToStringShort(callProperties)
431                     + '\n'
432                     + "    callSource: " + getCallSourceString() + '\n'
433                     + "}\n";
434         }
435 
toParcelableAnalytics()436         public ParcelableCallAnalytics toParcelableAnalytics() {
437             TelecomLogClass.CallLog analyticsProto = toProto();
438             List<ParcelableCallAnalytics.AnalyticsEvent> events =
439                     Arrays.stream(analyticsProto.callEvents)
440                             .map(callEventProto -> new ParcelableCallAnalytics.AnalyticsEvent(
441                                     callEventProto.getEventName(),
442                                     callEventProto.getTimeSinceLastEventMillis())
443                             ).collect(Collectors.toList());
444 
445             List<ParcelableCallAnalytics.EventTiming> timings =
446                     Arrays.stream(analyticsProto.callTimings)
447                             .map(callTimingProto -> new ParcelableCallAnalytics.EventTiming(
448                                     callTimingProto.getTimingName(),
449                                     callTimingProto.getTimeMillis())
450                             ).collect(Collectors.toList());
451 
452             ParcelableCallAnalytics result = new ParcelableCallAnalytics(
453                     // rounds down to nearest 5 minute mark
454                     analyticsProto.getStartTime5Min(),
455                     analyticsProto.getCallDurationMillis(),
456                     analyticsProto.getType(),
457                     analyticsProto.getIsAdditionalCall(),
458                     analyticsProto.getIsInterrupted(),
459                     analyticsProto.getCallTechnologies(),
460                     analyticsProto.getCallTerminationCode(),
461                     analyticsProto.getIsEmergencyCall(),
462                     analyticsProto.connectionService[0],
463                     analyticsProto.getIsCreatedFromExistingConnection(),
464                     events,
465                     timings);
466 
467             result.setIsVideoCall(analyticsProto.getIsVideoCall());
468             result.setVideoEvents(Arrays.stream(analyticsProto.videoEvents)
469                     .map(videoEventProto -> new ParcelableCallAnalytics.VideoEvent(
470                             videoEventProto.getEventName(),
471                             videoEventProto.getTimeSinceLastEventMillis(),
472                             videoEventProto.getVideoState())
473                     ).collect(Collectors.toList()));
474 
475             result.setCallSource(analyticsProto.getCallSource());
476 
477             return result;
478         }
479 
toProto()480         public TelecomLogClass.CallLog toProto() {
481             TelecomLogClass.CallLog result = new TelecomLogClass.CallLog();
482             result.setStartTime5Min(
483                     startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
484 
485             // Rounds up to the nearest second.
486             long callDuration = (endTime == 0 || startTime == 0) ? 0 : endTime - startTime;
487             callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ?
488                     0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND);
489             result.setCallDurationMillis(callDuration);
490 
491             result.setType(callDirection)
492                     .setIsAdditionalCall(isAdditionalCall)
493                     .setIsInterrupted(isInterrupted)
494                     .setCallTechnologies(callTechnologies)
495                     .setCallTerminationCode(
496                             callTerminationReason == null ?
497                                     ParcelableCallAnalytics.STILL_CONNECTED :
498                                     callTerminationReason.getCode())
499                     .setIsEmergencyCall(isEmergency)
500                     .setIsCreatedFromExistingConnection(createdFromExistingConnection)
501                     .setIsEmergencyCall(isEmergency)
502                     .setIsVideoCall(isVideo)
503                     .setConnectionProperties(callProperties)
504                     .setCallSource(callSource);
505 
506             result.connectionService = new String[]{connectionService};
507             if (callEvents != null) {
508                 result.callEvents = convertLogEventsToProtoEvents(callEvents.getEvents());
509                 result.callTimings = callEvents.extractEventTimings().stream()
510                         .map(Analytics::logEventTimingToProtoEventTiming)
511                         .toArray(TelecomLogClass.EventTimingEntry[]::new);
512             }
513             result.videoEvents =
514                     videoEvents.toArray(new TelecomLogClass.VideoEvent[videoEvents.size()]);
515             result.inCallServices = inCallServiceInfos.toArray(
516                     new TelecomLogClass.InCallServiceInfo[inCallServiceInfos.size()]);
517 
518             return result;
519         }
520 
getCallDirectionString()521         private String getCallDirectionString() {
522             switch (callDirection) {
523                 case UNKNOWN_DIRECTION:
524                     return "UNKNOWN";
525                 case INCOMING_DIRECTION:
526                     return "INCOMING";
527                 case OUTGOING_DIRECTION:
528                     return "OUTGOING";
529                 default:
530                     return "UNKNOWN";
531             }
532         }
533 
getCallTechnologiesAsString()534         private String getCallTechnologiesAsString() {
535             StringBuilder s = new StringBuilder();
536             s.append('[');
537             if ((callTechnologies & CDMA_PHONE) != 0) s.append("CDMA ");
538             if ((callTechnologies & GSM_PHONE) != 0) s.append("GSM ");
539             if ((callTechnologies & SIP_PHONE) != 0) s.append("SIP ");
540             if ((callTechnologies & IMS_PHONE) != 0) s.append("IMS ");
541             if ((callTechnologies & THIRD_PARTY_PHONE) != 0) s.append("THIRD_PARTY ");
542             s.append(']');
543             return s.toString();
544         }
545 
getCallDisconnectReasonString()546         private String getCallDisconnectReasonString() {
547             if (callTerminationReason != null) {
548                 return callTerminationReason.toString();
549             } else {
550                 return "NOT SET";
551             }
552         }
553 
getMissedReasonString()554         private String getMissedReasonString() {
555             StringBuilder s =  new StringBuilder();
556             s.append('[');
557             if ((missedReason & AUTO_MISSED_EMERGENCY_CALL) != 0) {
558                 s.append("emergency]");
559                 return s.toString();
560             } else if ((missedReason & AUTO_MISSED_MAXIMUM_DIALING) != 0) {
561                 s.append("max_dialing]");
562                 return s.toString();
563             } else if ((missedReason & AUTO_MISSED_MAXIMUM_RINGING) != 0) {
564                 s.append("max_ringing]");
565                 return s.toString();
566             }
567 
568             // user missed
569             if ((missedReason & USER_MISSED_SHORT_RING) != 0) s.append("short_ring ");
570             if ((missedReason & USER_MISSED_DND_MODE) != 0) s.append("dnd ");
571             if ((missedReason & USER_MISSED_LOW_RING_VOLUME) != 0) s.append("low_volume ");
572             if ((missedReason & USER_MISSED_NO_VIBRATE) != 0) s.append("no_vibrate ");
573             if ((missedReason & USER_MISSED_CALL_SCREENING_SERVICE_SILENCED) != 0)
574                 s.append("css_silenced ");
575             if ((missedReason & USER_MISSED_CALL_FILTERS_TIMEOUT) != 0) s.append("filter_timeout ");
576             if ((missedReason & USER_MISSED_NEVER_RANG) != 0) s.append("no_ring ");
577             s.append("]");
578             return s.toString();
579         }
580 
getInCallServicesString()581         private String getInCallServicesString() {
582             StringBuilder s = new StringBuilder();
583             s.append("[\n");
584             if (inCallServiceInfos != null) {
585                 for (TelecomLogClass.InCallServiceInfo service : inCallServiceInfos) {
586                     s.append("    ");
587                     s.append("name: ");
588                     s.append(service.getInCallServiceName());
589                     s.append(" type: ");
590                     s.append(service.getInCallServiceType());
591                     s.append(" is crashed: ");
592                     s.append(service.getIsNullBinding());
593                     s.append(" service last time in ms: ");
594                     s.append(service.getBoundDurationMillis());
595                     s.append("\n");
596                 }
597             }
598             s.append("]");
599             return s.toString();
600         }
601 
getCallSourceString()602         private String getCallSourceString() {
603             switch (callSource) {
604                 case CALL_SOURCE_UNSPECIFIED:
605                     return "UNSPECIFIED";
606                 case CALL_SOURCE_EMERGENCY_DIALPAD:
607                     return "EMERGENCY_DIALPAD";
608                 case CALL_SOURCE_EMERGENCY_SHORTCUT:
609                     return "EMERGENCY_SHORTCUT";
610                 default:
611                     return "UNSPECIFIED";
612             }
613         }
614     }
615 
616     public static final String TAG = "TelecomAnalytics";
617 
618     // Constants for call direction
619     public static final int UNKNOWN_DIRECTION = ParcelableCallAnalytics.CALLTYPE_UNKNOWN;
620     public static final int INCOMING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_INCOMING;
621     public static final int OUTGOING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_OUTGOING;
622 
623     // Constants for call technology
624     public static final int CDMA_PHONE = ParcelableCallAnalytics.CDMA_PHONE;
625     public static final int GSM_PHONE = ParcelableCallAnalytics.GSM_PHONE;
626     public static final int IMS_PHONE = ParcelableCallAnalytics.IMS_PHONE;
627     public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE;
628     public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE;
629 
630     // Constants for call source
631     public static final int CALL_SOURCE_UNSPECIFIED =
632             TelecomManager.CALL_SOURCE_UNSPECIFIED;
633     public static final int CALL_SOURCE_EMERGENCY_DIALPAD =
634             TelecomManager.CALL_SOURCE_EMERGENCY_DIALPAD;
635     public static final int CALL_SOURCE_EMERGENCY_SHORTCUT =
636             TelecomManager.CALL_SOURCE_EMERGENCY_SHORTCUT;
637 
638     // Constants for video events
639     public static final int SEND_LOCAL_SESSION_MODIFY_REQUEST =
640             ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_REQUEST;
641     public static final int SEND_LOCAL_SESSION_MODIFY_RESPONSE =
642             ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_RESPONSE;
643     public static final int RECEIVE_REMOTE_SESSION_MODIFY_REQUEST =
644             ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST;
645     public static final int RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE =
646             ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE;
647 
648     public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND;
649 
650     public static final int MAX_NUM_CALLS_TO_STORE = 100;
651     public static final int MAX_NUM_DUMP_TIMES_TO_STORE = 100;
652 
653     private static final Object sLock = new Object(); // Coarse lock for all of analytics
654     private static final LinkedBlockingDeque<Long> sDumpTimes =
655             new LinkedBlockingDeque<>(MAX_NUM_DUMP_TIMES_TO_STORE);
656     private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>();
657     private static final LinkedList<String> sActiveCallIds = new LinkedList<>();
658     private static final List<SessionTiming> sSessionTimings = new LinkedList<>();
659 
addSessionTiming(String sessionName, long time)660     public static void addSessionTiming(String sessionName, long time) {
661         if (sLogSessionToSessionId.containsKey(sessionName)) {
662             synchronized (sLock) {
663                 sSessionTimings.add(new SessionTiming(sLogSessionToSessionId.get(sessionName),
664                         time));
665             }
666         }
667     }
668 
initiateCallAnalytics(String callId, int direction)669     public static CallInfo initiateCallAnalytics(String callId, int direction) {
670         Log.i(TAG, "Starting analytics for call " + callId);
671         CallInfoImpl callInfo = new CallInfoImpl(callId, direction);
672         synchronized (sLock) {
673             while (sActiveCallIds.size() >= MAX_NUM_CALLS_TO_STORE) {
674                 String callToRemove = sActiveCallIds.remove();
675                 sCallIdToInfo.remove(callToRemove);
676             }
677             sCallIdToInfo.put(callId, callInfo);
678             sActiveCallIds.add(callId);
679         }
680         return callInfo;
681     }
682 
dumpToParcelableAnalytics()683     public static TelecomAnalytics dumpToParcelableAnalytics() {
684         List<ParcelableCallAnalytics> calls = new LinkedList<>();
685         List<SessionTiming> sessionTimings = new LinkedList<>();
686         synchronized (sLock) {
687             calls.addAll(sCallIdToInfo.values().stream()
688                     .map(CallInfoImpl::toParcelableAnalytics)
689                     .collect(Collectors.toList()));
690             sessionTimings.addAll(sSessionTimings);
691             sCallIdToInfo.clear();
692             sSessionTimings.clear();
693         }
694         return new TelecomAnalytics(sessionTimings, calls);
695     }
696 
dumpToEncodedProto(Context context, PrintWriter pw, String[] args)697     public static void dumpToEncodedProto(Context context, PrintWriter pw, String[] args) {
698         TelecomLogClass.TelecomLog result = new TelecomLogClass.TelecomLog();
699 
700         synchronized (sLock) {
701             noteDumpTime();
702             result.callLogs = sCallIdToInfo.values().stream()
703                     .map(CallInfoImpl::toProto)
704                     .toArray(TelecomLogClass.CallLog[]::new);
705             result.sessionTimings = sSessionTimings.stream()
706                     .map(timing -> new TelecomLogClass.LogSessionTiming()
707                             .setSessionEntryPoint(timing.getKey())
708                             .setTimeMillis(timing.getTime()))
709                     .toArray(TelecomLogClass.LogSessionTiming[]::new);
710             result.setHardwareRevision(SystemProperties.get("ro.boot.revision", ""));
711             result.setCarrierId(getCarrierId(context));
712             if (args.length > 1 && CLEAR_ANALYTICS_ARG.equals(args[1])) {
713                 sCallIdToInfo.clear();
714                 sSessionTimings.clear();
715             }
716         }
717         String encodedProto = Base64.encodeToString(
718                 TelecomLogClass.TelecomLog.toByteArray(result), Base64.DEFAULT);
719         pw.write(encodedProto);
720     }
721 
getCarrierId(Context context)722     private static int getCarrierId(Context context) {
723         try {
724             SubscriptionManager subscriptionManager =
725                     context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
726             List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList();
727             if (subInfos == null) {
728                 return -1;
729             }
730             return subInfos.stream()
731                     .max(Comparator.comparing(Analytics::scoreSubscriptionInfo))
732                     .map(SubscriptionInfo::getCarrierId).orElse(-1);
733         } catch (UnsupportedOperationException ignored) {
734             return -1;
735         }
736     }
737 
738     // Copied over from Telephony's server-side logic for consistency
scoreSubscriptionInfo(SubscriptionInfo subInfo)739     private static int scoreSubscriptionInfo(SubscriptionInfo subInfo) {
740         final int scoreCarrierId = 0b100;
741         final int scoreNotOpportunistic = 0b010;
742         final int scoreSlot0 = 0b001;
743 
744         return ((subInfo.getCarrierId() >= 0) ? scoreCarrierId : 0)
745                 + (subInfo.isOpportunistic() ? 0 : scoreNotOpportunistic)
746                 + ((subInfo.getSimSlotIndex() == 0) ? scoreSlot0 : 0);
747     }
748 
dump(IndentingPrintWriter writer)749     public static void dump(IndentingPrintWriter writer) {
750         synchronized (sLock) {
751             int prefixLength = CallsManager.TELECOM_CALL_ID_PREFIX.length();
752             List<String> callIds = new ArrayList<>(sCallIdToInfo.keySet());
753             // Sort the analytics in increasing order of call IDs
754             try {
755                 Collections.sort(callIds, (id1, id2) -> {
756                     int i1, i2;
757                     try {
758                         i1 = Integer.valueOf(id1.substring(prefixLength));
759                     } catch (NumberFormatException e) {
760                         i1 = Integer.MAX_VALUE;
761                     }
762 
763                     try {
764                         i2 = Integer.valueOf(id2.substring(prefixLength));
765                     } catch (NumberFormatException e) {
766                         i2 = Integer.MAX_VALUE;
767                     }
768                     return i1 - i2;
769                 });
770             } catch (IllegalArgumentException e) {
771                 // do nothing, leave the list in a partially sorted state.
772             }
773 
774             for (String callId : callIds) {
775                 writer.printf("Call %s: ", callId);
776                 writer.println(sCallIdToInfo.get(callId).toString());
777             }
778 
779             Map<Integer, Double> averageTimings = SessionTiming.averageTimings(sSessionTimings);
780             averageTimings.entrySet().stream()
781                     .filter(e -> sSessionIdToLogSession.containsKey(e.getKey()))
782                     .forEach(e -> writer.printf("%s: %.2f\n",
783                             sSessionIdToLogSession.get(e.getKey()), e.getValue()));
784             writer.println("Hardware Version: " + SystemProperties.get("ro.boot.revision", ""));
785             writer.println("Past analytics dumps: ");
786             writer.increaseIndent();
787             for (long time : sDumpTimes) {
788                 writer.println(Instant.ofEpochMilli(time).atZone(ZoneOffset.UTC));
789             }
790             writer.decreaseIndent();
791         }
792     }
793 
reset()794     public static void reset() {
795         synchronized (sLock) {
796             sCallIdToInfo.clear();
797         }
798     }
799 
noteDumpTime()800     public static void noteDumpTime() {
801         if (sDumpTimes.remainingCapacity() == 0) {
802             sDumpTimes.removeLast();
803         }
804         try {
805             sDumpTimes.addFirst(System.currentTimeMillis());
806         } catch (IllegalStateException e) {
807             Log.w(TAG, "Failed to note dump time -- full");
808         }
809     }
810 
811     /**
812      * Returns a copy of callIdToInfo. Use only for testing.
813      */
814     @VisibleForTesting
cloneData()815     public static Map<String, CallInfoImpl> cloneData() {
816         synchronized (sLock) {
817             Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size());
818             for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
819                 result.put(entry.getKey(), new CallInfoImpl(entry.getValue()));
820             }
821             return result;
822         }
823     }
824 
convertLogEventsToProtoEvents( List<EventManager.Event> logEvents)825     private static TelecomLogClass.Event[] convertLogEventsToProtoEvents(
826             List<EventManager.Event> logEvents) {
827         long timeOfLastEvent = -1;
828         ArrayList<TelecomLogClass.Event> events = new ArrayList<>(logEvents.size());
829         for (EventManager.Event logEvent : logEvents) {
830             if (sLogEventToAnalyticsEvent.containsKey(logEvent.eventId)) {
831                 TelecomLogClass.Event event = new TelecomLogClass.Event();
832                 event.setEventName(sLogEventToAnalyticsEvent.get(logEvent.eventId));
833                 event.setTimeSinceLastEventMillis(roundToOneSigFig(
834                         timeOfLastEvent < 0 ? -1 : logEvent.time - timeOfLastEvent));
835                 events.add(event);
836                 timeOfLastEvent = logEvent.time;
837             }
838         }
839         return events.toArray(new TelecomLogClass.Event[events.size()]);
840     }
841 
842     private static TelecomLogClass.EventTimingEntry logEventTimingToProtoEventTiming(
843             EventManager.EventRecord.EventTiming logEventTiming) {
844         int analyticsEventTimingName =
845                 sLogEventTimingToAnalyticsEventTiming.containsKey(logEventTiming.name) ?
846                         sLogEventTimingToAnalyticsEventTiming.get(logEventTiming.name) :
847                         ParcelableCallAnalytics.EventTiming.INVALID;
848         return new TelecomLogClass.EventTimingEntry()
849                 .setTimingName(analyticsEventTimingName)
850                 .setTimeMillis(logEventTiming.time);
851     }
852 
853     @VisibleForTesting
854     public static long roundToOneSigFig(long val) {
855         if (val == 0) {
856             return val;
857         }
858         int logVal = (int) Math.floor(Math.log10(val < 0 ? -val : val));
859         double s = Math.pow(10, logVal);
860         double dec = val / s;
861         return (long) (Math.round(dec) * s);
862     }
863 }
864