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 android.telecom.Connection;
20 import android.telecom.DisconnectCause;
21 import android.telecom.Logging.EventManager;
22 import android.telecom.ParcelableCallAnalytics;
23 import android.telecom.TelecomAnalytics;
24 import android.util.Base64;
25 import android.telecom.Log;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.util.IndentingPrintWriter;
29 import com.android.server.telecom.nano.TelecomLogClass;
30 
31 import java.io.PrintWriter;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.LinkedList;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.PriorityQueue;
40 import java.util.stream.Collectors;
41 
42 import static android.telecom.ParcelableCallAnalytics.AnalyticsEvent;
43 import static android.telecom.TelecomAnalytics.SessionTiming;
44 
45 /**
46  * A class that collects and stores data on how calls are being made, in order to
47  * aggregate these into useful statistics.
48  */
49 public class Analytics {
50     public static final String ANALYTICS_DUMPSYS_ARG = "analytics";
51     private static final String CLEAR_ANALYTICS_ARG = "clear";
52 
53     public static final Map<String, Integer> sLogEventToAnalyticsEvent =
54             new HashMap<String, Integer>() {{
55                 put(LogUtils.Events.SET_SELECT_PHONE_ACCOUNT,
56                         AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT);
57                 put(LogUtils.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD);
58                 put(LogUtils.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD);
59                 put(LogUtils.Events.SWAP, AnalyticsEvent.SWAP);
60                 put(LogUtils.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING);
61                 put(LogUtils.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH);
62                 put(LogUtils.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE);
63                 put(LogUtils.Events.SET_PARENT, AnalyticsEvent.SET_PARENT);
64                 put(LogUtils.Events.MUTE, AnalyticsEvent.MUTE);
65                 put(LogUtils.Events.UNMUTE, AnalyticsEvent.UNMUTE);
66                 put(LogUtils.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT);
67                 put(LogUtils.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE);
68                 put(LogUtils.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET);
69                 put(LogUtils.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER);
70                 put(LogUtils.Events.SILENCE, AnalyticsEvent.SILENCE);
71                 put(LogUtils.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED);
72                 put(LogUtils.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED);
73                 put(LogUtils.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED);
74                 put(LogUtils.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD);
75                 put(LogUtils.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD);
76                 put(LogUtils.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL);
77                 put(LogUtils.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT);
78                 put(LogUtils.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT);
79                 put(LogUtils.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE);
80                 put(LogUtils.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED);
81                 put(LogUtils.Events.SET_HOLD, AnalyticsEvent.SET_HOLD);
82                 put(LogUtils.Events.SET_DIALING, AnalyticsEvent.SET_DIALING);
83                 put(LogUtils.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION);
84                 put(LogUtils.Events.BIND_CS, AnalyticsEvent.BIND_CS);
85                 put(LogUtils.Events.CS_BOUND, AnalyticsEvent.CS_BOUND);
86                 put(LogUtils.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT);
87                 put(LogUtils.Events.DIRECT_TO_VM_INITIATED, AnalyticsEvent.DIRECT_TO_VM_INITIATED);
88                 put(LogUtils.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED);
89                 put(LogUtils.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED);
90                 put(LogUtils.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED);
91                 put(LogUtils.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT);
92             }};
93 
94     public static final Map<String, Integer> sLogSessionToSessionId =
95             new HashMap<String, Integer> () {{
96                 put(LogUtils.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL);
97                 put(LogUtils.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL);
98                 put(LogUtils.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL);
99                 put(LogUtils.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL);
100                 put(LogUtils.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL);
101                 put(LogUtils.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE);
102                 put(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE);
103                 put(LogUtils.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE);
104                 put(LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE,
105                         SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE);
106                 put(LogUtils.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE);
107                 put(LogUtils.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING);
108                 put(LogUtils.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING);
109                 put(LogUtils.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED);
110                 put(LogUtils.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD);
111                 put(LogUtils.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL);
112                 put(LogUtils.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED);
113                 put(LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL,
114                         SessionTiming.CSW_ADD_CONFERENCE_CALL);
115 
116             }};
117 
118     public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming =
119             new HashMap<String, Integer>() {{
120                 put(LogUtils.Events.Timings.ACCEPT_TIMING,
121                         ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING);
122                 put(LogUtils.Events.Timings.REJECT_TIMING,
123                         ParcelableCallAnalytics.EventTiming.REJECT_TIMING);
124                 put(LogUtils.Events.Timings.DISCONNECT_TIMING,
125                         ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING);
126                 put(LogUtils.Events.Timings.HOLD_TIMING,
127                         ParcelableCallAnalytics.EventTiming.HOLD_TIMING);
128                 put(LogUtils.Events.Timings.UNHOLD_TIMING,
129                         ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING);
130                 put(LogUtils.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING,
131                         ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING);
132                 put(LogUtils.Events.Timings.BIND_CS_TIMING,
133                         ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING);
134                 put(LogUtils.Events.Timings.SCREENING_COMPLETED_TIMING,
135                         ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING);
136                 put(LogUtils.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING,
137                         ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING);
138                 put(LogUtils.Events.Timings.BLOCK_CHECK_FINISHED_TIMING,
139                         ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING);
140                 put(LogUtils.Events.Timings.FILTERING_COMPLETED_TIMING,
141                         ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING);
142                 put(LogUtils.Events.Timings.FILTERING_TIMED_OUT_TIMING,
143                         ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING);
144             }};
145 
146     public static final Map<Integer, String> sSessionIdToLogSession = new HashMap<>();
147     static {
148         for (Map.Entry<String, Integer> e : sLogSessionToSessionId.entrySet()) {
e.getValue()149             sSessionIdToLogSession.put(e.getValue(), e.getKey());
150         }
151     }
152 
153     public static class CallInfo {
setCallStartTime(long startTime)154         public void setCallStartTime(long startTime) {
155         }
156 
setCallEndTime(long endTime)157         public void setCallEndTime(long endTime) {
158         }
159 
setCallIsAdditional(boolean isAdditional)160         public void setCallIsAdditional(boolean isAdditional) {
161         }
162 
setCallIsInterrupted(boolean isInterrupted)163         public void setCallIsInterrupted(boolean isInterrupted) {
164         }
165 
setCallDisconnectCause(DisconnectCause disconnectCause)166         public void setCallDisconnectCause(DisconnectCause disconnectCause) {
167         }
168 
addCallTechnology(int callTechnology)169         public void addCallTechnology(int callTechnology) {
170         }
171 
setCreatedFromExistingConnection(boolean createdFromExistingConnection)172         public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
173         }
174 
setCallConnectionService(String connectionServiceName)175         public void setCallConnectionService(String connectionServiceName) {
176         }
177 
setCallEvents(EventManager.EventRecord records)178         public void setCallEvents(EventManager.EventRecord records) {
179         }
180 
setCallIsVideo(boolean isVideo)181         public void setCallIsVideo(boolean isVideo) {
182         }
183 
addVideoEvent(int eventId, int videoState)184         public void addVideoEvent(int eventId, int videoState) {
185         }
186 
addInCallService(String serviceName, int type)187         public void addInCallService(String serviceName, int type) {
188         }
189 
addCallProperties(int properties)190         public void addCallProperties(int properties) {
191         }
192     }
193 
194     /**
195      * A class that holds data associated with a call.
196      */
197     @VisibleForTesting
198     public static class CallInfoImpl extends CallInfo {
199         public String callId;
200         public long startTime;  // start time in milliseconds since the epoch. 0 if not yet set.
201         public long endTime;  // end time in milliseconds since the epoch. 0 if not yet set.
202         public int callDirection;  // one of UNKNOWN_DIRECTION, INCOMING_DIRECTION,
203                                    // or OUTGOING_DIRECTION.
204         public boolean isAdditionalCall = false;  // true if the call came in while another call was
205                                                   // in progress or if the user dialed this call
206                                                   // while in the middle of another call.
207         public boolean isInterrupted = false;  // true if the call was interrupted by an incoming
208                                                // or outgoing call.
209         public int callTechnologies;  // bitmask denoting which technologies a call used.
210 
211         // true if the Telecom Call object was created from an existing connection via
212         // CallsManager#createCallForExistingConnection, for example, by ImsConference.
213         public boolean createdFromExistingConnection = false;
214 
215         public DisconnectCause callTerminationReason;
216         public String connectionService;
217         public boolean isEmergency = false;
218 
219         public EventManager.EventRecord callEvents;
220 
221         public boolean isVideo = false;
222         public List<TelecomLogClass.VideoEvent> videoEvents;
223         public List<TelecomLogClass.InCallServiceInfo> inCallServiceInfos;
224         public int callProperties = 0;
225 
226         private long mTimeOfLastVideoEvent = -1;
227 
CallInfoImpl(String callId, int callDirection)228         CallInfoImpl(String callId, int callDirection) {
229             this.callId = callId;
230             startTime = 0;
231             endTime = 0;
232             this.callDirection = callDirection;
233             callTechnologies = 0;
234             connectionService = "";
235             videoEvents = new LinkedList<>();
236             inCallServiceInfos = new LinkedList<>();
237         }
238 
CallInfoImpl(CallInfoImpl other)239         CallInfoImpl(CallInfoImpl other) {
240             this.callId = other.callId;
241             this.startTime = other.startTime;
242             this.endTime = other.endTime;
243             this.callDirection = other.callDirection;
244             this.isAdditionalCall = other.isAdditionalCall;
245             this.isInterrupted = other.isInterrupted;
246             this.callTechnologies = other.callTechnologies;
247             this.createdFromExistingConnection = other.createdFromExistingConnection;
248             this.connectionService = other.connectionService;
249             this.isEmergency = other.isEmergency;
250             this.callEvents = other.callEvents;
251             this.isVideo = other.isVideo;
252             this.videoEvents = other.videoEvents;
253             this.callProperties = other.callProperties;
254 
255             if (other.callTerminationReason != null) {
256                 this.callTerminationReason = new DisconnectCause(
257                         other.callTerminationReason.getCode(),
258                         other.callTerminationReason.getLabel(),
259                         other.callTerminationReason.getDescription(),
260                         other.callTerminationReason.getReason(),
261                         other.callTerminationReason.getTone());
262             } else {
263                 this.callTerminationReason = null;
264             }
265         }
266 
267         @Override
setCallStartTime(long startTime)268         public void setCallStartTime(long startTime) {
269             Log.d(TAG, "setting startTime for call " + callId + " to " + startTime);
270             this.startTime = startTime;
271         }
272 
273         @Override
setCallEndTime(long endTime)274         public void setCallEndTime(long endTime) {
275             Log.d(TAG, "setting endTime for call " + callId + " to " + endTime);
276             this.endTime = endTime;
277         }
278 
279         @Override
setCallIsAdditional(boolean isAdditional)280         public void setCallIsAdditional(boolean isAdditional) {
281             Log.d(TAG, "setting isAdditional for call " + callId + " to " + isAdditional);
282             this.isAdditionalCall = isAdditional;
283         }
284 
285         @Override
setCallIsInterrupted(boolean isInterrupted)286         public void setCallIsInterrupted(boolean isInterrupted) {
287             Log.d(TAG, "setting isInterrupted for call " + callId + " to " + isInterrupted);
288             this.isInterrupted = isInterrupted;
289         }
290 
291         @Override
addCallTechnology(int callTechnology)292         public void addCallTechnology(int callTechnology) {
293             Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology);
294             this.callTechnologies |= callTechnology;
295         }
296 
297         @Override
setCallDisconnectCause(DisconnectCause disconnectCause)298         public void setCallDisconnectCause(DisconnectCause disconnectCause) {
299             Log.d(TAG, "setting disconnectCause for call " + callId + " to " + disconnectCause);
300             this.callTerminationReason = disconnectCause;
301         }
302 
303         @Override
setCreatedFromExistingConnection(boolean createdFromExistingConnection)304         public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
305             Log.d(TAG, "setting createdFromExistingConnection for call " + callId + " to "
306                     + createdFromExistingConnection);
307             this.createdFromExistingConnection = createdFromExistingConnection;
308         }
309 
310         @Override
setCallConnectionService(String connectionServiceName)311         public void setCallConnectionService(String connectionServiceName) {
312             Log.d(TAG, "setting connection service for call " + callId + ": "
313                     + connectionServiceName);
314             this.connectionService = connectionServiceName;
315         }
316 
317         @Override
setCallEvents(EventManager.EventRecord records)318         public void setCallEvents(EventManager.EventRecord records) {
319             this.callEvents = records;
320         }
321 
322         @Override
setCallIsVideo(boolean isVideo)323         public void setCallIsVideo(boolean isVideo) {
324             this.isVideo = isVideo;
325         }
326 
327         @Override
addVideoEvent(int eventId, int videoState)328         public void addVideoEvent(int eventId, int videoState) {
329             long timeSinceLastEvent;
330             long currentTime = System.currentTimeMillis();
331             if (mTimeOfLastVideoEvent < 0) {
332                 timeSinceLastEvent = -1;
333             } else {
334                 timeSinceLastEvent = roundToOneSigFig(currentTime - mTimeOfLastVideoEvent);
335             }
336             mTimeOfLastVideoEvent = currentTime;
337 
338             videoEvents.add(new TelecomLogClass.VideoEvent()
339                     .setEventName(eventId)
340                     .setTimeSinceLastEventMillis(timeSinceLastEvent)
341                     .setVideoState(videoState));
342         }
343 
344         @Override
addInCallService(String serviceName, int type)345         public void addInCallService(String serviceName, int type) {
346             inCallServiceInfos.add(new TelecomLogClass.InCallServiceInfo()
347                     .setInCallServiceName(serviceName)
348                     .setInCallServiceType(type));
349         }
350 
351         @Override
addCallProperties(int properties)352         public void addCallProperties(int properties) {
353             this.callProperties |= properties;
354         }
355 
356         @Override
toString()357         public String toString() {
358             return "{\n"
359                     + "    startTime: " + startTime + '\n'
360                     + "    endTime: " + endTime + '\n'
361                     + "    direction: " + getCallDirectionString() + '\n'
362                     + "    isAdditionalCall: " + isAdditionalCall + '\n'
363                     + "    isInterrupted: " + isInterrupted + '\n'
364                     + "    callTechnologies: " + getCallTechnologiesAsString() + '\n'
365                     + "    callTerminationReason: " + getCallDisconnectReasonString() + '\n'
366                     + "    connectionService: " + connectionService + '\n'
367                     + "    isVideoCall: " + isVideo + '\n'
368                     + "    inCallServices: " + getInCallServicesString() + '\n'
369                     + "    callProperties: " + Connection.propertiesToStringShort(callProperties)
370                     + '\n'
371                     + "}\n";
372         }
373 
toParcelableAnalytics()374         public ParcelableCallAnalytics toParcelableAnalytics() {
375             TelecomLogClass.CallLog analyticsProto = toProto();
376             List<ParcelableCallAnalytics.AnalyticsEvent> events =
377                     Arrays.stream(analyticsProto.callEvents)
378                     .map(callEventProto -> new ParcelableCallAnalytics.AnalyticsEvent(
379                                 callEventProto.getEventName(),
380                                 callEventProto.getTimeSinceLastEventMillis())
381                     ).collect(Collectors.toList());
382 
383             List<ParcelableCallAnalytics.EventTiming> timings =
384                     Arrays.stream(analyticsProto.callTimings)
385                     .map(callTimingProto -> new ParcelableCallAnalytics.EventTiming(
386                             callTimingProto.getTimingName(),
387                             callTimingProto.getTimeMillis())
388                     ).collect(Collectors.toList());
389 
390             ParcelableCallAnalytics result = new ParcelableCallAnalytics(
391                     // rounds down to nearest 5 minute mark
392                     analyticsProto.getStartTime5Min(),
393                     analyticsProto.getCallDurationMillis(),
394                     analyticsProto.getType(),
395                     analyticsProto.getIsAdditionalCall(),
396                     analyticsProto.getIsInterrupted(),
397                     analyticsProto.getCallTechnologies(),
398                     analyticsProto.getCallTerminationCode(),
399                     analyticsProto.getIsEmergencyCall(),
400                     analyticsProto.connectionService[0],
401                     analyticsProto.getIsCreatedFromExistingConnection(),
402                     events,
403                     timings);
404 
405             result.setIsVideoCall(analyticsProto.getIsVideoCall());
406             result.setVideoEvents(Arrays.stream(analyticsProto.videoEvents)
407                     .map(videoEventProto -> new ParcelableCallAnalytics.VideoEvent(
408                             videoEventProto.getEventName(),
409                             videoEventProto.getTimeSinceLastEventMillis(),
410                             videoEventProto.getVideoState())
411                     ).collect(Collectors.toList()));
412 
413             return result;
414         }
415 
toProto()416         public TelecomLogClass.CallLog toProto() {
417             TelecomLogClass.CallLog result = new TelecomLogClass.CallLog();
418             result.setStartTime5Min(
419                     startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES);
420 
421             // Rounds up to the nearest second.
422             long callDuration = (endTime == 0 || startTime == 0) ? 0 : endTime - startTime;
423             callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ?
424                     0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND);
425             result.setCallDurationMillis(callDuration);
426 
427             result.setType(callDirection)
428                     .setIsAdditionalCall(isAdditionalCall)
429                     .setIsInterrupted(isInterrupted)
430                     .setCallTechnologies(callTechnologies)
431                     .setCallTerminationCode(
432                             callTerminationReason == null ?
433                                     ParcelableCallAnalytics.STILL_CONNECTED :
434                                     callTerminationReason.getCode())
435                     .setIsEmergencyCall(isEmergency)
436                     .setIsCreatedFromExistingConnection(createdFromExistingConnection)
437                     .setIsEmergencyCall(isEmergency)
438                     .setIsVideoCall(isVideo)
439                     .setConnectionProperties(callProperties);
440 
441             result.connectionService = new String[] {connectionService};
442             if (callEvents != null) {
443                 result.callEvents = convertLogEventsToProtoEvents(callEvents.getEvents());
444                 result.callTimings = callEvents.extractEventTimings().stream()
445                         .map(Analytics::logEventTimingToProtoEventTiming)
446                         .toArray(TelecomLogClass.EventTimingEntry[]::new);
447             }
448             result.videoEvents =
449                     videoEvents.toArray(new TelecomLogClass.VideoEvent[videoEvents.size()]);
450             result.inCallServices = inCallServiceInfos.toArray(
451                     new TelecomLogClass.InCallServiceInfo[inCallServiceInfos.size()]);
452 
453             return result;
454         }
455 
getCallDirectionString()456         private String getCallDirectionString() {
457             switch (callDirection) {
458                 case UNKNOWN_DIRECTION:
459                     return "UNKNOWN";
460                 case INCOMING_DIRECTION:
461                     return "INCOMING";
462                 case OUTGOING_DIRECTION:
463                     return "OUTGOING";
464                 default:
465                     return "UNKNOWN";
466             }
467         }
468 
getCallTechnologiesAsString()469         private String getCallTechnologiesAsString() {
470             StringBuilder s = new StringBuilder();
471             s.append('[');
472             if ((callTechnologies & CDMA_PHONE) != 0) s.append("CDMA ");
473             if ((callTechnologies & GSM_PHONE) != 0) s.append("GSM ");
474             if ((callTechnologies & SIP_PHONE) != 0) s.append("SIP ");
475             if ((callTechnologies & IMS_PHONE) != 0) s.append("IMS ");
476             if ((callTechnologies & THIRD_PARTY_PHONE) != 0) s.append("THIRD_PARTY ");
477             s.append(']');
478             return s.toString();
479         }
480 
getCallDisconnectReasonString()481         private String getCallDisconnectReasonString() {
482             if (callTerminationReason != null) {
483                 return callTerminationReason.toString();
484             } else {
485                 return "NOT SET";
486             }
487         }
488 
getInCallServicesString()489         private String getInCallServicesString() {
490             StringBuilder s = new StringBuilder();
491             s.append("[\n");
492             for (TelecomLogClass.InCallServiceInfo service : inCallServiceInfos) {
493                 s.append("    ");
494                 s.append("name: ");
495                 s.append(service.getInCallServiceName());
496                 s.append(" type: ");
497                 s.append(service.getInCallServiceType());
498                 s.append("\n");
499             }
500             s.append("]");
501             return s.toString();
502         }
503     }
504     public static final String TAG = "TelecomAnalytics";
505 
506     // Constants for call direction
507     public static final int UNKNOWN_DIRECTION = ParcelableCallAnalytics.CALLTYPE_UNKNOWN;
508     public static final int INCOMING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_INCOMING;
509     public static final int OUTGOING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_OUTGOING;
510 
511     // Constants for call technology
512     public static final int CDMA_PHONE = ParcelableCallAnalytics.CDMA_PHONE;
513     public static final int GSM_PHONE = ParcelableCallAnalytics.GSM_PHONE;
514     public static final int IMS_PHONE = ParcelableCallAnalytics.IMS_PHONE;
515     public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE;
516     public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE;
517 
518     // Constants for video events
519     public static final int SEND_LOCAL_SESSION_MODIFY_REQUEST =
520             ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_REQUEST;
521     public static final int SEND_LOCAL_SESSION_MODIFY_RESPONSE =
522             ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_RESPONSE;
523     public static final int RECEIVE_REMOTE_SESSION_MODIFY_REQUEST =
524             ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST;
525     public static final int RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE =
526             ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE;
527 
528     public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND;
529 
530     public static final int MAX_NUM_CALLS_TO_STORE = 100;
531 
532     private static final Object sLock = new Object(); // Coarse lock for all of analytics
533     private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>();
534     private static final LinkedList<String> sActiveCallIds = new LinkedList<>();
535     private static final List<SessionTiming> sSessionTimings = new LinkedList<>();
536 
addSessionTiming(String sessionName, long time)537     public static void addSessionTiming(String sessionName, long time) {
538         if (sLogSessionToSessionId.containsKey(sessionName)) {
539             synchronized (sLock) {
540                 sSessionTimings.add(new SessionTiming(sLogSessionToSessionId.get(sessionName),
541                         time));
542             }
543         }
544     }
545 
initiateCallAnalytics(String callId, int direction)546     public static CallInfo initiateCallAnalytics(String callId, int direction) {
547         Log.d(TAG, "Starting analytics for call " + callId);
548         CallInfoImpl callInfo = new CallInfoImpl(callId, direction);
549         synchronized (sLock) {
550             while (sActiveCallIds.size() >= MAX_NUM_CALLS_TO_STORE) {
551                 String callToRemove = sActiveCallIds.remove();
552                 sCallIdToInfo.remove(callToRemove);
553             }
554             sCallIdToInfo.put(callId, callInfo);
555             sActiveCallIds.add(callId);
556         }
557         return callInfo;
558     }
559 
dumpToParcelableAnalytics()560     public static TelecomAnalytics dumpToParcelableAnalytics() {
561         List<ParcelableCallAnalytics> calls = new LinkedList<>();
562         List<SessionTiming> sessionTimings = new LinkedList<>();
563         synchronized (sLock) {
564             calls.addAll(sCallIdToInfo.values().stream()
565                     .map(CallInfoImpl::toParcelableAnalytics)
566                     .collect(Collectors.toList()));
567             sessionTimings.addAll(sSessionTimings);
568             sCallIdToInfo.clear();
569             sSessionTimings.clear();
570         }
571         return new TelecomAnalytics(sessionTimings, calls);
572     }
573 
dumpToEncodedProto(PrintWriter pw, String[] args)574     public static void dumpToEncodedProto(PrintWriter pw, String[] args) {
575         TelecomLogClass.TelecomLog result = new TelecomLogClass.TelecomLog();
576 
577         synchronized (sLock) {
578             result.callLogs = sCallIdToInfo.values().stream()
579                     .map(CallInfoImpl::toProto)
580                     .toArray(TelecomLogClass.CallLog[]::new);
581             result.sessionTimings = sSessionTimings.stream()
582                     .map(timing -> new TelecomLogClass.LogSessionTiming()
583                             .setSessionEntryPoint(timing.getKey())
584                             .setTimeMillis(timing.getTime()))
585                     .toArray(TelecomLogClass.LogSessionTiming[]::new);
586             if (args.length > 1 && CLEAR_ANALYTICS_ARG.equals(args[1])) {
587                 sCallIdToInfo.clear();
588                 sSessionTimings.clear();
589             }
590         }
591         String encodedProto = Base64.encodeToString(
592                 TelecomLogClass.TelecomLog.toByteArray(result), Base64.DEFAULT);
593         pw.write(encodedProto);
594     }
595 
dump(IndentingPrintWriter writer)596     public static void dump(IndentingPrintWriter writer) {
597         synchronized (sLock) {
598             int prefixLength = CallsManager.TELECOM_CALL_ID_PREFIX.length();
599             List<String> callIds = new ArrayList<>(sCallIdToInfo.keySet());
600             // Sort the analytics in increasing order of call IDs
601             try {
602                 Collections.sort(callIds, (id1, id2) -> {
603                     int i1, i2;
604                     try {
605                         i1 = Integer.valueOf(id1.substring(prefixLength));
606                     } catch (NumberFormatException e) {
607                         i1 = Integer.MAX_VALUE;
608                     }
609 
610                     try {
611                         i2 = Integer.valueOf(id2.substring(prefixLength));
612                     } catch (NumberFormatException e) {
613                         i2 = Integer.MAX_VALUE;
614                     }
615                     return i1 - i2;
616                 });
617             } catch (IllegalArgumentException e) {
618                 // do nothing, leave the list in a partially sorted state.
619             }
620 
621             for (String callId : callIds) {
622                 writer.printf("Call %s: ", callId);
623                 writer.println(sCallIdToInfo.get(callId).toString());
624             }
625 
626             Map<Integer, Double> averageTimings = SessionTiming.averageTimings(sSessionTimings);
627             averageTimings.entrySet().stream()
628                     .filter(e -> sSessionIdToLogSession.containsKey(e.getKey()))
629                     .forEach(e -> writer.printf("%s: %.2f\n",
630                             sSessionIdToLogSession.get(e.getKey()), e.getValue()));
631         }
632     }
633 
reset()634     public static void reset() {
635         synchronized (sLock) {
636             sCallIdToInfo.clear();
637         }
638     }
639 
640     /**
641      * Returns a copy of callIdToInfo. Use only for testing.
642      */
643     @VisibleForTesting
cloneData()644     public static Map<String, CallInfoImpl> cloneData() {
645         synchronized (sLock) {
646             Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size());
647             for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
648                 result.put(entry.getKey(), new CallInfoImpl(entry.getValue()));
649             }
650             return result;
651         }
652     }
653 
convertLogEventsToProtoEvents( List<EventManager.Event> logEvents)654     private static TelecomLogClass.Event[] convertLogEventsToProtoEvents(
655             List<EventManager.Event> logEvents) {
656         long timeOfLastEvent = -1;
657         ArrayList<TelecomLogClass.Event> events = new ArrayList<>(logEvents.size());
658         for (EventManager.Event logEvent : logEvents) {
659             if (sLogEventToAnalyticsEvent.containsKey(logEvent.eventId)) {
660                 TelecomLogClass.Event event = new TelecomLogClass.Event();
661                 event.setEventName(sLogEventToAnalyticsEvent.get(logEvent.eventId));
662                 event.setTimeSinceLastEventMillis(roundToOneSigFig(
663                         timeOfLastEvent < 0 ? -1 : logEvent.time - timeOfLastEvent));
664                 events.add(event);
665                 timeOfLastEvent = logEvent.time;
666             }
667         }
668         return events.toArray(new TelecomLogClass.Event[events.size()]);
669     }
670 
671     private static TelecomLogClass.EventTimingEntry logEventTimingToProtoEventTiming(
672             EventManager.EventRecord.EventTiming logEventTiming) {
673         int analyticsEventTimingName =
674                 sLogEventTimingToAnalyticsEventTiming.containsKey(logEventTiming.name) ?
675                         sLogEventTimingToAnalyticsEventTiming.get(logEventTiming.name) :
676                         ParcelableCallAnalytics.EventTiming.INVALID;
677         return new TelecomLogClass.EventTimingEntry()
678                 .setTimingName(analyticsEventTimingName)
679                 .setTimeMillis(logEventTiming.time);
680     }
681 
682     @VisibleForTesting
683     public static long roundToOneSigFig(long val)  {
684         if (val == 0) {
685             return val;
686         }
687         int logVal = (int) Math.floor(Math.log10(val < 0 ? -val : val));
688         double s = Math.pow(10, logVal);
689         double dec =  val / s;
690         return (long) (Math.round(dec) * s);
691     }
692 }
693