| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.server.telecom; |
| |
| import android.content.Context; |
| import android.os.SystemProperties; |
| |
| import android.telecom.Connection; |
| import android.telecom.DisconnectCause; |
| import android.telecom.Logging.EventManager; |
| import android.telecom.ParcelableCallAnalytics; |
| import android.telecom.TelecomAnalytics; |
| import android.telecom.TelecomManager; |
| import android.telephony.SubscriptionInfo; |
| import android.telephony.SubscriptionManager; |
| import android.util.Base64; |
| import android.telecom.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.telecom.nano.TelecomLogClass; |
| |
| import java.io.PrintWriter; |
| import java.time.Instant; |
| import java.time.ZoneOffset; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.LinkedBlockingDeque; |
| import java.util.stream.Collectors; |
| |
| import static android.telecom.ParcelableCallAnalytics.AnalyticsEvent; |
| import static android.telecom.TelecomAnalytics.SessionTiming; |
| |
| /** |
| * A class that collects and stores data on how calls are being made, in order to |
| * aggregate these into useful statistics. |
| */ |
| public class Analytics { |
| public static final String ANALYTICS_DUMPSYS_ARG = "analytics"; |
| private static final String CLEAR_ANALYTICS_ARG = "clear"; |
| |
| public static final Map<String, Integer> sLogEventToAnalyticsEvent = |
| new HashMap<String, Integer>() {{ |
| put(LogUtils.Events.SET_SELECT_PHONE_ACCOUNT, |
| AnalyticsEvent.SET_SELECT_PHONE_ACCOUNT); |
| put(LogUtils.Events.REQUEST_HOLD, AnalyticsEvent.REQUEST_HOLD); |
| put(LogUtils.Events.REQUEST_UNHOLD, AnalyticsEvent.REQUEST_UNHOLD); |
| put(LogUtils.Events.SWAP, AnalyticsEvent.SWAP); |
| put(LogUtils.Events.SKIP_RINGING, AnalyticsEvent.SKIP_RINGING); |
| put(LogUtils.Events.CONFERENCE_WITH, AnalyticsEvent.CONFERENCE_WITH); |
| put(LogUtils.Events.SPLIT_FROM_CONFERENCE, AnalyticsEvent.SPLIT_CONFERENCE); |
| put(LogUtils.Events.SET_PARENT, AnalyticsEvent.SET_PARENT); |
| put(LogUtils.Events.MUTE, AnalyticsEvent.MUTE); |
| put(LogUtils.Events.UNMUTE, AnalyticsEvent.UNMUTE); |
| put(LogUtils.Events.AUDIO_ROUTE_BT, AnalyticsEvent.AUDIO_ROUTE_BT); |
| put(LogUtils.Events.AUDIO_ROUTE_EARPIECE, AnalyticsEvent.AUDIO_ROUTE_EARPIECE); |
| put(LogUtils.Events.AUDIO_ROUTE_HEADSET, AnalyticsEvent.AUDIO_ROUTE_HEADSET); |
| put(LogUtils.Events.AUDIO_ROUTE_SPEAKER, AnalyticsEvent.AUDIO_ROUTE_SPEAKER); |
| put(LogUtils.Events.SILENCE, AnalyticsEvent.SILENCE); |
| put(LogUtils.Events.SCREENING_COMPLETED, AnalyticsEvent.SCREENING_COMPLETED); |
| put(LogUtils.Events.BLOCK_CHECK_FINISHED, AnalyticsEvent.BLOCK_CHECK_FINISHED); |
| put(LogUtils.Events.DIRECT_TO_VM_FINISHED, AnalyticsEvent.DIRECT_TO_VM_FINISHED); |
| put(LogUtils.Events.REMOTELY_HELD, AnalyticsEvent.REMOTELY_HELD); |
| put(LogUtils.Events.REMOTELY_UNHELD, AnalyticsEvent.REMOTELY_UNHELD); |
| put(LogUtils.Events.REQUEST_PULL, AnalyticsEvent.REQUEST_PULL); |
| put(LogUtils.Events.REQUEST_ACCEPT, AnalyticsEvent.REQUEST_ACCEPT); |
| put(LogUtils.Events.REQUEST_REJECT, AnalyticsEvent.REQUEST_REJECT); |
| put(LogUtils.Events.SET_ACTIVE, AnalyticsEvent.SET_ACTIVE); |
| put(LogUtils.Events.SET_DISCONNECTED, AnalyticsEvent.SET_DISCONNECTED); |
| put(LogUtils.Events.SET_HOLD, AnalyticsEvent.SET_HOLD); |
| put(LogUtils.Events.SET_DIALING, AnalyticsEvent.SET_DIALING); |
| put(LogUtils.Events.START_CONNECTION, AnalyticsEvent.START_CONNECTION); |
| put(LogUtils.Events.BIND_CS, AnalyticsEvent.BIND_CS); |
| put(LogUtils.Events.CS_BOUND, AnalyticsEvent.CS_BOUND); |
| put(LogUtils.Events.SCREENING_SENT, AnalyticsEvent.SCREENING_SENT); |
| put(LogUtils.Events.DIRECT_TO_VM_INITIATED, AnalyticsEvent.DIRECT_TO_VM_INITIATED); |
| put(LogUtils.Events.BLOCK_CHECK_INITIATED, AnalyticsEvent.BLOCK_CHECK_INITIATED); |
| put(LogUtils.Events.FILTERING_INITIATED, AnalyticsEvent.FILTERING_INITIATED); |
| put(LogUtils.Events.FILTERING_COMPLETED, AnalyticsEvent.FILTERING_COMPLETED); |
| put(LogUtils.Events.FILTERING_TIMED_OUT, AnalyticsEvent.FILTERING_TIMED_OUT); |
| }}; |
| |
| public static final Map<String, Integer> sLogSessionToSessionId = |
| new HashMap<String, Integer> () {{ |
| put(LogUtils.Sessions.ICA_ANSWER_CALL, SessionTiming.ICA_ANSWER_CALL); |
| put(LogUtils.Sessions.ICA_REJECT_CALL, SessionTiming.ICA_REJECT_CALL); |
| put(LogUtils.Sessions.ICA_DISCONNECT_CALL, SessionTiming.ICA_DISCONNECT_CALL); |
| put(LogUtils.Sessions.ICA_HOLD_CALL, SessionTiming.ICA_HOLD_CALL); |
| put(LogUtils.Sessions.ICA_UNHOLD_CALL, SessionTiming.ICA_UNHOLD_CALL); |
| put(LogUtils.Sessions.ICA_MUTE, SessionTiming.ICA_MUTE); |
| put(LogUtils.Sessions.ICA_SET_AUDIO_ROUTE, SessionTiming.ICA_SET_AUDIO_ROUTE); |
| put(LogUtils.Sessions.ICA_CONFERENCE, SessionTiming.ICA_CONFERENCE); |
| put(LogUtils.Sessions.CSW_HANDLE_CREATE_CONNECTION_COMPLETE, |
| SessionTiming.CSW_HANDLE_CREATE_CONNECTION_COMPLETE); |
| put(LogUtils.Sessions.CSW_SET_ACTIVE, SessionTiming.CSW_SET_ACTIVE); |
| put(LogUtils.Sessions.CSW_SET_RINGING, SessionTiming.CSW_SET_RINGING); |
| put(LogUtils.Sessions.CSW_SET_DIALING, SessionTiming.CSW_SET_DIALING); |
| put(LogUtils.Sessions.CSW_SET_DISCONNECTED, SessionTiming.CSW_SET_DISCONNECTED); |
| put(LogUtils.Sessions.CSW_SET_ON_HOLD, SessionTiming.CSW_SET_ON_HOLD); |
| put(LogUtils.Sessions.CSW_REMOVE_CALL, SessionTiming.CSW_REMOVE_CALL); |
| put(LogUtils.Sessions.CSW_SET_IS_CONFERENCED, SessionTiming.CSW_SET_IS_CONFERENCED); |
| put(LogUtils.Sessions.CSW_ADD_CONFERENCE_CALL, |
| SessionTiming.CSW_ADD_CONFERENCE_CALL); |
| |
| }}; |
| |
| public static final Map<String, Integer> sLogEventTimingToAnalyticsEventTiming = |
| new HashMap<String, Integer>() {{ |
| put(LogUtils.Events.Timings.ACCEPT_TIMING, |
| ParcelableCallAnalytics.EventTiming.ACCEPT_TIMING); |
| put(LogUtils.Events.Timings.REJECT_TIMING, |
| ParcelableCallAnalytics.EventTiming.REJECT_TIMING); |
| put(LogUtils.Events.Timings.DISCONNECT_TIMING, |
| ParcelableCallAnalytics.EventTiming.DISCONNECT_TIMING); |
| put(LogUtils.Events.Timings.HOLD_TIMING, |
| ParcelableCallAnalytics.EventTiming.HOLD_TIMING); |
| put(LogUtils.Events.Timings.UNHOLD_TIMING, |
| ParcelableCallAnalytics.EventTiming.UNHOLD_TIMING); |
| put(LogUtils.Events.Timings.OUTGOING_TIME_TO_DIALING_TIMING, |
| ParcelableCallAnalytics.EventTiming.OUTGOING_TIME_TO_DIALING_TIMING); |
| put(LogUtils.Events.Timings.BIND_CS_TIMING, |
| ParcelableCallAnalytics.EventTiming.BIND_CS_TIMING); |
| put(LogUtils.Events.Timings.SCREENING_COMPLETED_TIMING, |
| ParcelableCallAnalytics.EventTiming.SCREENING_COMPLETED_TIMING); |
| put(LogUtils.Events.Timings.DIRECT_TO_VM_FINISHED_TIMING, |
| ParcelableCallAnalytics.EventTiming.DIRECT_TO_VM_FINISHED_TIMING); |
| put(LogUtils.Events.Timings.BLOCK_CHECK_FINISHED_TIMING, |
| ParcelableCallAnalytics.EventTiming.BLOCK_CHECK_FINISHED_TIMING); |
| put(LogUtils.Events.Timings.FILTERING_COMPLETED_TIMING, |
| ParcelableCallAnalytics.EventTiming.FILTERING_COMPLETED_TIMING); |
| put(LogUtils.Events.Timings.FILTERING_TIMED_OUT_TIMING, |
| ParcelableCallAnalytics.EventTiming.FILTERING_TIMED_OUT_TIMING); |
| put(LogUtils.Events.Timings.START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING, |
| ParcelableCallAnalytics.EventTiming. |
| START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING); |
| }}; |
| |
| public static final Map<Integer, String> sSessionIdToLogSession = new HashMap<>(); |
| static { |
| for (Map.Entry<String, Integer> e : sLogSessionToSessionId.entrySet()) { |
| sSessionIdToLogSession.put(e.getValue(), e.getKey()); |
| } |
| } |
| |
| public static class CallInfo { |
| public void setCallStartTime(long startTime) { |
| } |
| |
| public void setCallEndTime(long endTime) { |
| } |
| |
| public void setCallIsAdditional(boolean isAdditional) { |
| } |
| |
| public void setCallIsEmergency(boolean isEmergency) { |
| } |
| |
| public void setCallIsInterrupted(boolean isInterrupted) { |
| } |
| |
| public void setCallDisconnectCause(DisconnectCause disconnectCause) { |
| } |
| |
| public void addCallTechnology(int callTechnology) { |
| } |
| |
| public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) { |
| } |
| |
| public void setCallConnectionService(String connectionServiceName) { |
| } |
| |
| public void setCallEvents(EventManager.EventRecord records) { |
| } |
| |
| public void setCallIsVideo(boolean isVideo) { |
| } |
| |
| public void addVideoEvent(int eventId, int videoState) { |
| } |
| |
| public void addInCallService(String serviceName, int type, long boundDuration, |
| boolean isNullBinding) { |
| } |
| |
| public void addCallProperties(int properties) { |
| } |
| |
| public void setCallSource(int callSource) { |
| } |
| |
| public void setMissedReason(long missedReason) { |
| } |
| } |
| |
| /** |
| * A class that holds data associated with a call. |
| */ |
| @VisibleForTesting |
| public static class CallInfoImpl extends CallInfo { |
| public String callId; |
| public long startTime; // start time in milliseconds since the epoch. 0 if not yet set. |
| public long endTime; // end time in milliseconds since the epoch. 0 if not yet set. |
| public int callDirection; // one of UNKNOWN_DIRECTION, INCOMING_DIRECTION, |
| // or OUTGOING_DIRECTION. |
| public boolean isAdditionalCall = false; // true if the call came in while another call was |
| // in progress or if the user dialed this call |
| // while in the middle of another call. |
| public boolean isInterrupted = false; // true if the call was interrupted by an incoming |
| // or outgoing call. |
| public int callTechnologies; // bitmask denoting which technologies a call used. |
| |
| // true if the Telecom Call object was created from an existing connection via |
| // CallsManager#createCallForExistingConnection, for example, by ImsConference. |
| public boolean createdFromExistingConnection = false; |
| |
| public DisconnectCause callTerminationReason; |
| public String connectionService; |
| public boolean isEmergency = false; |
| |
| public EventManager.EventRecord callEvents; |
| |
| public boolean isVideo = false; |
| public List<TelecomLogClass.VideoEvent> videoEvents; |
| public List<TelecomLogClass.InCallServiceInfo> inCallServiceInfos; |
| public int callProperties = 0; |
| public int callSource = CALL_SOURCE_UNSPECIFIED; |
| public long missedReason; |
| |
| private long mTimeOfLastVideoEvent = -1; |
| |
| CallInfoImpl(String callId, int callDirection) { |
| this.callId = callId; |
| startTime = 0; |
| endTime = 0; |
| this.callDirection = callDirection; |
| callTechnologies = 0; |
| connectionService = ""; |
| videoEvents = new LinkedList<>(); |
| inCallServiceInfos = new LinkedList<>(); |
| missedReason = 0; |
| } |
| |
| CallInfoImpl(CallInfoImpl other) { |
| this.callId = other.callId; |
| this.startTime = other.startTime; |
| this.endTime = other.endTime; |
| this.callDirection = other.callDirection; |
| this.isAdditionalCall = other.isAdditionalCall; |
| this.isInterrupted = other.isInterrupted; |
| this.callTechnologies = other.callTechnologies; |
| this.createdFromExistingConnection = other.createdFromExistingConnection; |
| this.connectionService = other.connectionService; |
| this.isEmergency = other.isEmergency; |
| this.callEvents = other.callEvents; |
| this.isVideo = other.isVideo; |
| this.videoEvents = other.videoEvents; |
| this.callProperties = other.callProperties; |
| this.callSource = other.callSource; |
| this.missedReason = other.missedReason; |
| |
| if (other.callTerminationReason != null) { |
| this.callTerminationReason = new DisconnectCause( |
| other.callTerminationReason.getCode(), |
| other.callTerminationReason.getLabel(), |
| other.callTerminationReason.getDescription(), |
| other.callTerminationReason.getReason(), |
| other.callTerminationReason.getTone()); |
| } else { |
| this.callTerminationReason = null; |
| } |
| } |
| |
| @Override |
| public void setCallStartTime(long startTime) { |
| Log.d(TAG, "setting startTime for call " + callId + " to " + startTime); |
| this.startTime = startTime; |
| } |
| |
| @Override |
| public void setCallEndTime(long endTime) { |
| Log.d(TAG, "setting endTime for call " + callId + " to " + endTime); |
| this.endTime = endTime; |
| } |
| |
| @Override |
| public void setCallIsAdditional(boolean isAdditional) { |
| Log.d(TAG, "setting isAdditional for call " + callId + " to " + isAdditional); |
| this.isAdditionalCall = isAdditional; |
| } |
| |
| @Override |
| public void setCallIsInterrupted(boolean isInterrupted) { |
| Log.d(TAG, "setting isInterrupted for call " + callId + " to " + isInterrupted); |
| this.isInterrupted = isInterrupted; |
| } |
| |
| @Override |
| public void addCallTechnology(int callTechnology) { |
| Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology); |
| this.callTechnologies |= callTechnology; |
| } |
| |
| @Override |
| public void setCallIsEmergency(boolean isEmergency) { |
| Log.d(TAG, "setting call as emergency: " + isEmergency); |
| this.isEmergency = isEmergency; |
| } |
| |
| @Override |
| public void setCallDisconnectCause(DisconnectCause disconnectCause) { |
| Log.d(TAG, "setting disconnectCause for call " + callId + " to " + disconnectCause); |
| this.callTerminationReason = disconnectCause; |
| } |
| |
| @Override |
| public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) { |
| Log.d(TAG, "setting createdFromExistingConnection for call " + callId + " to " |
| + createdFromExistingConnection); |
| this.createdFromExistingConnection = createdFromExistingConnection; |
| } |
| |
| @Override |
| public void setCallConnectionService(String connectionServiceName) { |
| Log.d(TAG, "setting connection service for call " + callId + ": " |
| + connectionServiceName); |
| this.connectionService = connectionServiceName; |
| } |
| |
| @Override |
| public void setMissedReason(long missedReason) { |
| Log.d(TAG, "setting missedReason for call " + callId + ": " |
| + missedReason); |
| this.missedReason = missedReason; |
| } |
| |
| @Override |
| public void setCallEvents(EventManager.EventRecord records) { |
| this.callEvents = records; |
| } |
| |
| @Override |
| public void setCallIsVideo(boolean isVideo) { |
| this.isVideo = isVideo; |
| } |
| |
| @Override |
| public void addVideoEvent(int eventId, int videoState) { |
| long timeSinceLastEvent; |
| long currentTime = System.currentTimeMillis(); |
| if (mTimeOfLastVideoEvent < 0) { |
| timeSinceLastEvent = -1; |
| } else { |
| timeSinceLastEvent = roundToOneSigFig(currentTime - mTimeOfLastVideoEvent); |
| } |
| mTimeOfLastVideoEvent = currentTime; |
| |
| videoEvents.add(new TelecomLogClass.VideoEvent() |
| .setEventName(eventId) |
| .setTimeSinceLastEventMillis(timeSinceLastEvent) |
| .setVideoState(videoState)); |
| } |
| |
| @Override |
| public void addInCallService(String serviceName, int type, long boundDuration, |
| boolean isNullBinding) { |
| inCallServiceInfos.add(new TelecomLogClass.InCallServiceInfo() |
| .setInCallServiceName(serviceName) |
| .setInCallServiceType(type) |
| .setBoundDurationMillis(boundDuration) |
| .setIsNullBinding(isNullBinding)); |
| } |
| |
| @Override |
| public void addCallProperties(int properties) { |
| this.callProperties |= properties; |
| } |
| |
| @Override |
| public void setCallSource(int callSource) { |
| this.callSource = callSource; |
| } |
| |
| @Override |
| public String toString() { |
| return "{\n" |
| + " startTime: " + startTime + '\n' |
| + " endTime: " + endTime + '\n' |
| + " direction: " + getCallDirectionString() + '\n' |
| + " isAdditionalCall: " + isAdditionalCall + '\n' |
| + " isInterrupted: " + isInterrupted + '\n' |
| + " isEmergency: " + isEmergency + '\n' |
| + " callTechnologies: " + getCallTechnologiesAsString() + '\n' |
| + " callTerminationReason: " + getCallDisconnectReasonString() + '\n' |
| + " missedReason: " + getMissedReasonString() + '\n' |
| + " connectionService: " + connectionService + '\n' |
| + " isVideoCall: " + isVideo + '\n' |
| + " inCallServices: " + getInCallServicesString() + '\n' |
| + " callProperties: " + Connection.propertiesToStringShort(callProperties) |
| + '\n' |
| + " callSource: " + getCallSourceString() + '\n' |
| + "}\n"; |
| } |
| |
| public ParcelableCallAnalytics toParcelableAnalytics() { |
| TelecomLogClass.CallLog analyticsProto = toProto(); |
| List<ParcelableCallAnalytics.AnalyticsEvent> events = |
| Arrays.stream(analyticsProto.callEvents) |
| .map(callEventProto -> new ParcelableCallAnalytics.AnalyticsEvent( |
| callEventProto.getEventName(), |
| callEventProto.getTimeSinceLastEventMillis()) |
| ).collect(Collectors.toList()); |
| |
| List<ParcelableCallAnalytics.EventTiming> timings = |
| Arrays.stream(analyticsProto.callTimings) |
| .map(callTimingProto -> new ParcelableCallAnalytics.EventTiming( |
| callTimingProto.getTimingName(), |
| callTimingProto.getTimeMillis()) |
| ).collect(Collectors.toList()); |
| |
| ParcelableCallAnalytics result = new ParcelableCallAnalytics( |
| // rounds down to nearest 5 minute mark |
| analyticsProto.getStartTime5Min(), |
| analyticsProto.getCallDurationMillis(), |
| analyticsProto.getType(), |
| analyticsProto.getIsAdditionalCall(), |
| analyticsProto.getIsInterrupted(), |
| analyticsProto.getCallTechnologies(), |
| analyticsProto.getCallTerminationCode(), |
| analyticsProto.getIsEmergencyCall(), |
| analyticsProto.connectionService[0], |
| analyticsProto.getIsCreatedFromExistingConnection(), |
| events, |
| timings); |
| |
| result.setIsVideoCall(analyticsProto.getIsVideoCall()); |
| result.setVideoEvents(Arrays.stream(analyticsProto.videoEvents) |
| .map(videoEventProto -> new ParcelableCallAnalytics.VideoEvent( |
| videoEventProto.getEventName(), |
| videoEventProto.getTimeSinceLastEventMillis(), |
| videoEventProto.getVideoState()) |
| ).collect(Collectors.toList())); |
| |
| result.setCallSource(analyticsProto.getCallSource()); |
| |
| return result; |
| } |
| |
| public TelecomLogClass.CallLog toProto() { |
| TelecomLogClass.CallLog result = new TelecomLogClass.CallLog(); |
| result.setStartTime5Min( |
| startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES); |
| |
| // Rounds up to the nearest second. |
| long callDuration = (endTime == 0 || startTime == 0) ? 0 : endTime - startTime; |
| callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ? |
| 0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND); |
| result.setCallDurationMillis(callDuration); |
| |
| result.setType(callDirection) |
| .setIsAdditionalCall(isAdditionalCall) |
| .setIsInterrupted(isInterrupted) |
| .setCallTechnologies(callTechnologies) |
| .setCallTerminationCode( |
| callTerminationReason == null ? |
| ParcelableCallAnalytics.STILL_CONNECTED : |
| callTerminationReason.getCode()) |
| .setIsEmergencyCall(isEmergency) |
| .setIsCreatedFromExistingConnection(createdFromExistingConnection) |
| .setIsEmergencyCall(isEmergency) |
| .setIsVideoCall(isVideo) |
| .setConnectionProperties(callProperties) |
| .setCallSource(callSource); |
| |
| result.connectionService = new String[] {connectionService}; |
| if (callEvents != null) { |
| result.callEvents = convertLogEventsToProtoEvents(callEvents.getEvents()); |
| result.callTimings = callEvents.extractEventTimings().stream() |
| .map(Analytics::logEventTimingToProtoEventTiming) |
| .toArray(TelecomLogClass.EventTimingEntry[]::new); |
| } |
| result.videoEvents = |
| videoEvents.toArray(new TelecomLogClass.VideoEvent[videoEvents.size()]); |
| result.inCallServices = inCallServiceInfos.toArray( |
| new TelecomLogClass.InCallServiceInfo[inCallServiceInfos.size()]); |
| |
| return result; |
| } |
| |
| private String getCallDirectionString() { |
| switch (callDirection) { |
| case UNKNOWN_DIRECTION: |
| return "UNKNOWN"; |
| case INCOMING_DIRECTION: |
| return "INCOMING"; |
| case OUTGOING_DIRECTION: |
| return "OUTGOING"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| private String getCallTechnologiesAsString() { |
| StringBuilder s = new StringBuilder(); |
| s.append('['); |
| if ((callTechnologies & CDMA_PHONE) != 0) s.append("CDMA "); |
| if ((callTechnologies & GSM_PHONE) != 0) s.append("GSM "); |
| if ((callTechnologies & SIP_PHONE) != 0) s.append("SIP "); |
| if ((callTechnologies & IMS_PHONE) != 0) s.append("IMS "); |
| if ((callTechnologies & THIRD_PARTY_PHONE) != 0) s.append("THIRD_PARTY "); |
| s.append(']'); |
| return s.toString(); |
| } |
| |
| private String getCallDisconnectReasonString() { |
| if (callTerminationReason != null) { |
| return callTerminationReason.toString(); |
| } else { |
| return "NOT SET"; |
| } |
| } |
| |
| private String getMissedReasonString() { |
| //TODO: Implement this |
| return null; |
| } |
| |
| private String getInCallServicesString() { |
| StringBuilder s = new StringBuilder(); |
| s.append("[\n"); |
| if (inCallServiceInfos != null) { |
| for (TelecomLogClass.InCallServiceInfo service : inCallServiceInfos) { |
| s.append(" "); |
| s.append("name: "); |
| s.append(service.getInCallServiceName()); |
| s.append(" type: "); |
| s.append(service.getInCallServiceType()); |
| s.append(" is crashed: "); |
| s.append(service.getIsNullBinding()); |
| s.append(" service last time in ms: "); |
| s.append(service.getBoundDurationMillis()); |
| s.append("\n"); |
| } |
| } |
| s.append("]"); |
| return s.toString(); |
| } |
| |
| private String getCallSourceString() { |
| switch (callSource) { |
| case CALL_SOURCE_UNSPECIFIED: |
| return "UNSPECIFIED"; |
| case CALL_SOURCE_EMERGENCY_DIALPAD: |
| return "EMERGENCY_DIALPAD"; |
| case CALL_SOURCE_EMERGENCY_SHORTCUT: |
| return "EMERGENCY_SHORTCUT"; |
| default: |
| return "UNSPECIFIED"; |
| } |
| } |
| } |
| public static final String TAG = "TelecomAnalytics"; |
| |
| // Constants for call direction |
| public static final int UNKNOWN_DIRECTION = ParcelableCallAnalytics.CALLTYPE_UNKNOWN; |
| public static final int INCOMING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_INCOMING; |
| public static final int OUTGOING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_OUTGOING; |
| |
| // Constants for call technology |
| public static final int CDMA_PHONE = ParcelableCallAnalytics.CDMA_PHONE; |
| public static final int GSM_PHONE = ParcelableCallAnalytics.GSM_PHONE; |
| public static final int IMS_PHONE = ParcelableCallAnalytics.IMS_PHONE; |
| public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE; |
| public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE; |
| |
| // Constants for call source |
| public static final int CALL_SOURCE_UNSPECIFIED = |
| TelecomManager.CALL_SOURCE_UNSPECIFIED; |
| public static final int CALL_SOURCE_EMERGENCY_DIALPAD = |
| TelecomManager.CALL_SOURCE_EMERGENCY_DIALPAD; |
| public static final int CALL_SOURCE_EMERGENCY_SHORTCUT = |
| TelecomManager.CALL_SOURCE_EMERGENCY_SHORTCUT; |
| |
| // Constants for video events |
| public static final int SEND_LOCAL_SESSION_MODIFY_REQUEST = |
| ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_REQUEST; |
| public static final int SEND_LOCAL_SESSION_MODIFY_RESPONSE = |
| ParcelableCallAnalytics.VideoEvent.SEND_LOCAL_SESSION_MODIFY_RESPONSE; |
| public static final int RECEIVE_REMOTE_SESSION_MODIFY_REQUEST = |
| ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST; |
| public static final int RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE = |
| ParcelableCallAnalytics.VideoEvent.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE; |
| |
| public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND; |
| |
| public static final int MAX_NUM_CALLS_TO_STORE = 100; |
| public static final int MAX_NUM_DUMP_TIMES_TO_STORE = 100; |
| |
| private static final Object sLock = new Object(); // Coarse lock for all of analytics |
| private static final LinkedBlockingDeque<Long> sDumpTimes = |
| new LinkedBlockingDeque<>(MAX_NUM_DUMP_TIMES_TO_STORE); |
| private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>(); |
| private static final LinkedList<String> sActiveCallIds = new LinkedList<>(); |
| private static final List<SessionTiming> sSessionTimings = new LinkedList<>(); |
| |
| public static void addSessionTiming(String sessionName, long time) { |
| if (sLogSessionToSessionId.containsKey(sessionName)) { |
| synchronized (sLock) { |
| sSessionTimings.add(new SessionTiming(sLogSessionToSessionId.get(sessionName), |
| time)); |
| } |
| } |
| } |
| |
| public static CallInfo initiateCallAnalytics(String callId, int direction) { |
| Log.i(TAG, "Starting analytics for call " + callId); |
| CallInfoImpl callInfo = new CallInfoImpl(callId, direction); |
| synchronized (sLock) { |
| while (sActiveCallIds.size() >= MAX_NUM_CALLS_TO_STORE) { |
| String callToRemove = sActiveCallIds.remove(); |
| sCallIdToInfo.remove(callToRemove); |
| } |
| sCallIdToInfo.put(callId, callInfo); |
| sActiveCallIds.add(callId); |
| } |
| return callInfo; |
| } |
| |
| public static TelecomAnalytics dumpToParcelableAnalytics() { |
| List<ParcelableCallAnalytics> calls = new LinkedList<>(); |
| List<SessionTiming> sessionTimings = new LinkedList<>(); |
| synchronized (sLock) { |
| calls.addAll(sCallIdToInfo.values().stream() |
| .map(CallInfoImpl::toParcelableAnalytics) |
| .collect(Collectors.toList())); |
| sessionTimings.addAll(sSessionTimings); |
| sCallIdToInfo.clear(); |
| sSessionTimings.clear(); |
| } |
| return new TelecomAnalytics(sessionTimings, calls); |
| } |
| |
| public static void dumpToEncodedProto(Context context, PrintWriter pw, String[] args) { |
| TelecomLogClass.TelecomLog result = new TelecomLogClass.TelecomLog(); |
| |
| synchronized (sLock) { |
| noteDumpTime(); |
| result.callLogs = sCallIdToInfo.values().stream() |
| .map(CallInfoImpl::toProto) |
| .toArray(TelecomLogClass.CallLog[]::new); |
| result.sessionTimings = sSessionTimings.stream() |
| .map(timing -> new TelecomLogClass.LogSessionTiming() |
| .setSessionEntryPoint(timing.getKey()) |
| .setTimeMillis(timing.getTime())) |
| .toArray(TelecomLogClass.LogSessionTiming[]::new); |
| result.setHardwareRevision(SystemProperties.get("ro.boot.revision", "")); |
| result.setCarrierId(getCarrierId(context)); |
| if (args.length > 1 && CLEAR_ANALYTICS_ARG.equals(args[1])) { |
| sCallIdToInfo.clear(); |
| sSessionTimings.clear(); |
| } |
| } |
| String encodedProto = Base64.encodeToString( |
| TelecomLogClass.TelecomLog.toByteArray(result), Base64.DEFAULT); |
| pw.write(encodedProto); |
| } |
| |
| private static int getCarrierId(Context context) { |
| SubscriptionManager subscriptionManager = |
| context.getSystemService(SubscriptionManager.class); |
| List<SubscriptionInfo> subInfos = subscriptionManager.getActiveSubscriptionInfoList(); |
| if (subInfos == null) { |
| return -1; |
| } |
| return subInfos.stream() |
| .max(Comparator.comparing(Analytics::scoreSubscriptionInfo)) |
| .map(SubscriptionInfo::getCarrierId).orElse(-1); |
| } |
| |
| // Copied over from Telephony's server-side logic for consistency |
| private static int scoreSubscriptionInfo(SubscriptionInfo subInfo) { |
| final int scoreCarrierId = 0b100; |
| final int scoreNotOpportunistic = 0b010; |
| final int scoreSlot0 = 0b001; |
| |
| return ((subInfo.getCarrierId() >= 0) ? scoreCarrierId : 0) |
| + (subInfo.isOpportunistic() ? 0 : scoreNotOpportunistic) |
| + ((subInfo.getSimSlotIndex() == 0) ? scoreSlot0 : 0); |
| } |
| |
| public static void dump(IndentingPrintWriter writer) { |
| synchronized (sLock) { |
| int prefixLength = CallsManager.TELECOM_CALL_ID_PREFIX.length(); |
| List<String> callIds = new ArrayList<>(sCallIdToInfo.keySet()); |
| // Sort the analytics in increasing order of call IDs |
| try { |
| Collections.sort(callIds, (id1, id2) -> { |
| int i1, i2; |
| try { |
| i1 = Integer.valueOf(id1.substring(prefixLength)); |
| } catch (NumberFormatException e) { |
| i1 = Integer.MAX_VALUE; |
| } |
| |
| try { |
| i2 = Integer.valueOf(id2.substring(prefixLength)); |
| } catch (NumberFormatException e) { |
| i2 = Integer.MAX_VALUE; |
| } |
| return i1 - i2; |
| }); |
| } catch (IllegalArgumentException e) { |
| // do nothing, leave the list in a partially sorted state. |
| } |
| |
| for (String callId : callIds) { |
| writer.printf("Call %s: ", callId); |
| writer.println(sCallIdToInfo.get(callId).toString()); |
| } |
| |
| Map<Integer, Double> averageTimings = SessionTiming.averageTimings(sSessionTimings); |
| averageTimings.entrySet().stream() |
| .filter(e -> sSessionIdToLogSession.containsKey(e.getKey())) |
| .forEach(e -> writer.printf("%s: %.2f\n", |
| sSessionIdToLogSession.get(e.getKey()), e.getValue())); |
| writer.println("Hardware Version: " + SystemProperties.get("ro.boot.revision", "")); |
| writer.println("Past analytics dumps: "); |
| writer.increaseIndent(); |
| for (long time : sDumpTimes) { |
| writer.println(Instant.ofEpochMilli(time).atZone(ZoneOffset.UTC)); |
| } |
| writer.decreaseIndent(); |
| } |
| } |
| |
| public static void reset() { |
| synchronized (sLock) { |
| sCallIdToInfo.clear(); |
| } |
| } |
| |
| public static void noteDumpTime() { |
| if (sDumpTimes.remainingCapacity() == 0) { |
| sDumpTimes.removeLast(); |
| } |
| try { |
| sDumpTimes.addFirst(System.currentTimeMillis()); |
| } catch (IllegalStateException e) { |
| Log.w(TAG, "Failed to note dump time -- full"); |
| } |
| } |
| |
| /** |
| * Returns a copy of callIdToInfo. Use only for testing. |
| */ |
| @VisibleForTesting |
| public static Map<String, CallInfoImpl> cloneData() { |
| synchronized (sLock) { |
| Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size()); |
| for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) { |
| result.put(entry.getKey(), new CallInfoImpl(entry.getValue())); |
| } |
| return result; |
| } |
| } |
| |
| private static TelecomLogClass.Event[] convertLogEventsToProtoEvents( |
| List<EventManager.Event> logEvents) { |
| long timeOfLastEvent = -1; |
| ArrayList<TelecomLogClass.Event> events = new ArrayList<>(logEvents.size()); |
| for (EventManager.Event logEvent : logEvents) { |
| if (sLogEventToAnalyticsEvent.containsKey(logEvent.eventId)) { |
| TelecomLogClass.Event event = new TelecomLogClass.Event(); |
| event.setEventName(sLogEventToAnalyticsEvent.get(logEvent.eventId)); |
| event.setTimeSinceLastEventMillis(roundToOneSigFig( |
| timeOfLastEvent < 0 ? -1 : logEvent.time - timeOfLastEvent)); |
| events.add(event); |
| timeOfLastEvent = logEvent.time; |
| } |
| } |
| return events.toArray(new TelecomLogClass.Event[events.size()]); |
| } |
| |
| private static TelecomLogClass.EventTimingEntry logEventTimingToProtoEventTiming( |
| EventManager.EventRecord.EventTiming logEventTiming) { |
| int analyticsEventTimingName = |
| sLogEventTimingToAnalyticsEventTiming.containsKey(logEventTiming.name) ? |
| sLogEventTimingToAnalyticsEventTiming.get(logEventTiming.name) : |
| ParcelableCallAnalytics.EventTiming.INVALID; |
| return new TelecomLogClass.EventTimingEntry() |
| .setTimingName(analyticsEventTimingName) |
| .setTimeMillis(logEventTiming.time); |
| } |
| |
| @VisibleForTesting |
| public static long roundToOneSigFig(long val) { |
| if (val == 0) { |
| return val; |
| } |
| int logVal = (int) Math.floor(Math.log10(val < 0 ? -val : val)); |
| double s = Math.pow(10, logVal); |
| double dec = val / s; |
| return (long) (Math.round(dec) * s); |
| } |
| } |