Home | History | Annotate | Download | only in telecom
      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()) {
    149             sSessionIdToLogSession.put(e.getValue(), e.getKey());
    150         }
    151     }
    152 
    153     public static class CallInfo {
    154         public void setCallStartTime(long startTime) {
    155         }
    156 
    157         public void setCallEndTime(long endTime) {
    158         }
    159 
    160         public void setCallIsAdditional(boolean isAdditional) {
    161         }
    162 
    163         public void setCallIsInterrupted(boolean isInterrupted) {
    164         }
    165 
    166         public void setCallDisconnectCause(DisconnectCause disconnectCause) {
    167         }
    168 
    169         public void addCallTechnology(int callTechnology) {
    170         }
    171 
    172         public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
    173         }
    174 
    175         public void setCallConnectionService(String connectionServiceName) {
    176         }
    177 
    178         public void setCallEvents(EventManager.EventRecord records) {
    179         }
    180 
    181         public void setCallIsVideo(boolean isVideo) {
    182         }
    183 
    184         public void addVideoEvent(int eventId, int videoState) {
    185         }
    186 
    187         public void addInCallService(String serviceName, int type) {
    188         }
    189 
    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 
    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 
    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
    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
    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
    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
    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
    292         public void addCallTechnology(int callTechnology) {
    293             Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology);
    294             this.callTechnologies |= callTechnology;
    295         }
    296 
    297         @Override
    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
    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
    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
    318         public void setCallEvents(EventManager.EventRecord records) {
    319             this.callEvents = records;
    320         }
    321 
    322         @Override
    323         public void setCallIsVideo(boolean isVideo) {
    324             this.isVideo = isVideo;
    325         }
    326 
    327         @Override
    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
    345         public void addInCallService(String serviceName, int type) {
    346             inCallServiceInfos.add(new TelecomLogClass.InCallServiceInfo()
    347                     .setInCallServiceName(serviceName)
    348                     .setInCallServiceType(type));
    349         }
    350 
    351         @Override
    352         public void addCallProperties(int properties) {
    353             this.callProperties |= properties;
    354         }
    355 
    356         @Override
    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 
    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 
    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 
    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 
    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 
    481         private String getCallDisconnectReasonString() {
    482             if (callTerminationReason != null) {
    483                 return callTerminationReason.toString();
    484             } else {
    485                 return "NOT SET";
    486             }
    487         }
    488 
    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 
    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 
    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 
    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 
    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 
    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 
    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
    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 
    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