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.os.Parcelable;
20 import android.telecom.ParcelableCallAnalytics;
21 import android.telecom.DisconnectCause;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 import com.android.internal.telephony.CallerInfo;
25 import com.android.internal.util.IndentingPrintWriter;
26 
27 import java.util.HashMap;
28 import java.util.Map;
29 
30 /**
31  * A class that collects and stores data on how calls are being made, in order to
32  * aggregate these into useful statistics.
33  */
34 public class Analytics {
35    public static class CallInfo {
setCallStartTime(long startTime)36         void setCallStartTime(long startTime) {
37         }
38 
setCallEndTime(long endTime)39         void setCallEndTime(long endTime) {
40         }
41 
setCallIsAdditional(boolean isAdditional)42         void setCallIsAdditional(boolean isAdditional) {
43         }
44 
setCallIsInterrupted(boolean isInterrupted)45         void setCallIsInterrupted(boolean isInterrupted) {
46         }
47 
setCallDisconnectCause(DisconnectCause disconnectCause)48         void setCallDisconnectCause(DisconnectCause disconnectCause) {
49         }
50 
addCallTechnology(int callTechnology)51         void addCallTechnology(int callTechnology) {
52         }
53 
setCreatedFromExistingConnection(boolean createdFromExistingConnection)54         void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
55         }
56 
setCallConnectionService(String connectionServiceName)57         void setCallConnectionService(String connectionServiceName) {
58         }
59     }
60 
61     /**
62      * A class that holds data associated with a call.
63      */
64     @VisibleForTesting
65     public static class CallInfoImpl extends CallInfo {
66         public String callId;
67         public long startTime;  // start time in milliseconds since the epoch. 0 if not yet set.
68         public long endTime;  // end time in milliseconds since the epoch. 0 if not yet set.
69         public int callDirection;  // one of UNKNOWN_DIRECTION, INCOMING_DIRECTION,
70                                    // or OUTGOING_DIRECTION.
71         public boolean isAdditionalCall = false;  // true if the call came in while another call was
72                                                   // in progress or if the user dialed this call
73                                                   // while in the middle of another call.
74         public boolean isInterrupted = false;  // true if the call was interrupted by an incoming
75                                                // or outgoing call.
76         public int callTechnologies;  // bitmask denoting which technologies a call used.
77 
78         // true if the Telecom Call object was created from an existing connection via
79         // CallsManager#createCallForExistingConnection, for example, by ImsConference.
80         public boolean createdFromExistingConnection = false;
81 
82         public DisconnectCause callTerminationReason;
83         public String connectionService;
84         public boolean isEmergency = false;
85 
CallInfoImpl(String callId, int callDirection)86         CallInfoImpl(String callId, int callDirection) {
87             this.callId = callId;
88             startTime = 0;
89             endTime = 0;
90             this.callDirection = callDirection;
91             callTechnologies = 0;
92             connectionService = "";
93         }
94 
CallInfoImpl(CallInfoImpl other)95         CallInfoImpl(CallInfoImpl other) {
96             this.callId = other.callId;
97             this.startTime = other.startTime;
98             this.endTime = other.endTime;
99             this.callDirection = other.callDirection;
100             this.isAdditionalCall = other.isAdditionalCall;
101             this.isInterrupted = other.isInterrupted;
102             this.callTechnologies = other.callTechnologies;
103             this.createdFromExistingConnection = other.createdFromExistingConnection;
104             this.connectionService = other.connectionService;
105             this.isEmergency = other.isEmergency;
106 
107             if (other.callTerminationReason != null) {
108                 this.callTerminationReason = new DisconnectCause(
109                         other.callTerminationReason.getCode(),
110                         other.callTerminationReason.getLabel(),
111                         other.callTerminationReason.getDescription(),
112                         other.callTerminationReason.getReason(),
113                         other.callTerminationReason.getTone());
114             } else {
115                 this.callTerminationReason = null;
116             }
117         }
118 
119         @Override
setCallStartTime(long startTime)120         public void setCallStartTime(long startTime) {
121             Log.d(TAG, "setting startTime for call " + callId + " to " + startTime);
122             this.startTime = startTime;
123         }
124 
125         @Override
setCallEndTime(long endTime)126         public void setCallEndTime(long endTime) {
127             Log.d(TAG, "setting endTime for call " + callId + " to " + endTime);
128             this.endTime = endTime;
129         }
130 
131         @Override
setCallIsAdditional(boolean isAdditional)132         public void setCallIsAdditional(boolean isAdditional) {
133             Log.d(TAG, "setting isAdditional for call " + callId + " to " + isAdditional);
134             this.isAdditionalCall = isAdditional;
135         }
136 
137         @Override
setCallIsInterrupted(boolean isInterrupted)138         public void setCallIsInterrupted(boolean isInterrupted) {
139             Log.d(TAG, "setting isInterrupted for call " + callId + " to " + isInterrupted);
140             this.isInterrupted = isInterrupted;
141         }
142 
143         @Override
addCallTechnology(int callTechnology)144         public void addCallTechnology(int callTechnology) {
145             Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology);
146             this.callTechnologies |= callTechnology;
147         }
148 
149         @Override
setCallDisconnectCause(DisconnectCause disconnectCause)150         public void setCallDisconnectCause(DisconnectCause disconnectCause) {
151             Log.d(TAG, "setting disconnectCause for call " + callId + " to " + disconnectCause);
152             this.callTerminationReason = disconnectCause;
153         }
154 
155         @Override
setCreatedFromExistingConnection(boolean createdFromExistingConnection)156         public void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
157             Log.d(TAG, "setting createdFromExistingConnection for call " + callId + " to "
158                     + createdFromExistingConnection);
159             this.createdFromExistingConnection = createdFromExistingConnection;
160         }
161 
162         @Override
setCallConnectionService(String connectionServiceName)163         public void setCallConnectionService(String connectionServiceName) {
164             Log.d(TAG, "setting connection service for call " + callId + ": "
165                     + connectionServiceName);
166             this.connectionService = connectionServiceName;
167         }
168 
169         @Override
toString()170         public String toString() {
171             return "{\n"
172                     + "    startTime: " + startTime + '\n'
173                     + "    endTime: " + endTime + '\n'
174                     + "    direction: " + getCallDirectionString() + '\n'
175                     + "    isAdditionalCall: " + isAdditionalCall + '\n'
176                     + "    isInterrupted: " + isInterrupted + '\n'
177                     + "    callTechnologies: " + getCallTechnologiesAsString() + '\n'
178                     + "    callTerminationReason: " + getCallDisconnectReasonString() + '\n'
179                     + "    connectionService: " + connectionService + '\n'
180                     + "}\n";
181         }
182 
toParcelableAnalytics()183         public ParcelableCallAnalytics toParcelableAnalytics() {
184             // Rounds up to the nearest second.
185             long callDuration = (endTime == 0 || startTime == 0) ? 0 : endTime - startTime;
186             callDuration += (callDuration % MILLIS_IN_1_SECOND == 0) ?
187                     0 : (MILLIS_IN_1_SECOND - callDuration % MILLIS_IN_1_SECOND);
188             return new ParcelableCallAnalytics(
189                     // rounds down to nearest 5 minute mark
190                     startTime - startTime % ParcelableCallAnalytics.MILLIS_IN_5_MINUTES,
191                     callDuration,
192                     callDirection,
193                     isAdditionalCall,
194                     isInterrupted,
195                     callTechnologies,
196                     callTerminationReason == null ?
197                             ParcelableCallAnalytics.STILL_CONNECTED :
198                             callTerminationReason.getCode(),
199                     isEmergency,
200                     connectionService,
201                     createdFromExistingConnection);
202         }
203 
getCallDirectionString()204         private String getCallDirectionString() {
205             switch (callDirection) {
206                 case UNKNOWN_DIRECTION:
207                     return "UNKNOWN";
208                 case INCOMING_DIRECTION:
209                     return "INCOMING";
210                 case OUTGOING_DIRECTION:
211                     return "OUTGOING";
212                 default:
213                     return "UNKNOWN";
214             }
215         }
216 
getCallTechnologiesAsString()217         private String getCallTechnologiesAsString() {
218             StringBuilder s = new StringBuilder();
219             s.append('[');
220             if ((callTechnologies & CDMA_PHONE) != 0) s.append("CDMA ");
221             if ((callTechnologies & GSM_PHONE) != 0) s.append("GSM ");
222             if ((callTechnologies & SIP_PHONE) != 0) s.append("SIP ");
223             if ((callTechnologies & IMS_PHONE) != 0) s.append("IMS ");
224             if ((callTechnologies & THIRD_PARTY_PHONE) != 0) s.append("THIRD_PARTY ");
225             s.append(']');
226             return s.toString();
227         }
228 
getCallDisconnectReasonString()229         private String getCallDisconnectReasonString() {
230             if (callTerminationReason != null) {
231                 return callTerminationReason.toString();
232             } else {
233                 return "NOT SET";
234             }
235         }
236     }
237     public static final String TAG = "TelecomAnalytics";
238 
239     // Constants for call direction
240     public static final int UNKNOWN_DIRECTION = ParcelableCallAnalytics.CALLTYPE_UNKNOWN;
241     public static final int INCOMING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_INCOMING;
242     public static final int OUTGOING_DIRECTION = ParcelableCallAnalytics.CALLTYPE_OUTGOING;
243 
244     // Constants for call technology
245     public static final int CDMA_PHONE = ParcelableCallAnalytics.CDMA_PHONE;
246     public static final int GSM_PHONE = ParcelableCallAnalytics.GSM_PHONE;
247     public static final int IMS_PHONE = ParcelableCallAnalytics.IMS_PHONE;
248     public static final int SIP_PHONE = ParcelableCallAnalytics.SIP_PHONE;
249     public static final int THIRD_PARTY_PHONE = ParcelableCallAnalytics.THIRD_PARTY_PHONE;
250 
251     public static final long MILLIS_IN_1_SECOND = ParcelableCallAnalytics.MILLIS_IN_1_SECOND;
252 
253     private static final Object sLock = new Object(); // Coarse lock for all of analytics
254     private static final Map<String, CallInfoImpl> sCallIdToInfo = new HashMap<>();
255 
initiateCallAnalytics(String callId, int direction)256     public static CallInfo initiateCallAnalytics(String callId, int direction) {
257         Log.d(TAG, "Starting analytics for call " + callId);
258         CallInfoImpl callInfo = new CallInfoImpl(callId, direction);
259         synchronized (sLock) {
260             sCallIdToInfo.put(callId, callInfo);
261         }
262         return callInfo;
263     }
264 
dumpToParcelableAnalytics()265     public static ParcelableCallAnalytics[] dumpToParcelableAnalytics() {
266         ParcelableCallAnalytics[] result;
267         synchronized (sLock) {
268             result = new ParcelableCallAnalytics[sCallIdToInfo.size()];
269             int idx = 0;
270             for (CallInfoImpl entry : sCallIdToInfo.values()) {
271                 result[idx] = entry.toParcelableAnalytics();
272                 idx++;
273             }
274             sCallIdToInfo.clear();
275         }
276         return result;
277     }
278 
dump(IndentingPrintWriter writer)279     public static void dump(IndentingPrintWriter writer) {
280         synchronized (sLock) {
281             for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
282                 writer.printf("Call %s: ", entry.getKey());
283                 writer.println(entry.getValue().toString());
284             }
285         }
286     }
287 
reset()288     public static void reset() {
289         synchronized (sLock) {
290             sCallIdToInfo.clear();
291         }
292     }
293 
294     /**
295      * Returns a deep copy of callIdToInfo that's safe to read/write without synchronization
296      */
cloneData()297     public static Map<String, CallInfoImpl> cloneData() {
298         synchronized (sLock) {
299             Map<String, CallInfoImpl> result = new HashMap<>(sCallIdToInfo.size());
300             for (Map.Entry<String, CallInfoImpl> entry : sCallIdToInfo.entrySet()) {
301                 result.put(entry.getKey(), new CallInfoImpl(entry.getValue()));
302             }
303             return result;
304         }
305     }
306 }
307