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
17package com.android.server.telecom;
18
19import android.os.Parcelable;
20import android.telecom.ParcelableCallAnalytics;
21import android.telecom.DisconnectCause;
22
23import com.android.internal.annotations.VisibleForTesting;
24import com.android.internal.telephony.CallerInfo;
25import com.android.internal.util.IndentingPrintWriter;
26
27import java.util.HashMap;
28import 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 */
34public class Analytics {
35   public static class CallInfo {
36        void setCallStartTime(long startTime) {
37        }
38
39        void setCallEndTime(long endTime) {
40        }
41
42        void setCallIsAdditional(boolean isAdditional) {
43        }
44
45        void setCallIsInterrupted(boolean isInterrupted) {
46        }
47
48        void setCallDisconnectCause(DisconnectCause disconnectCause) {
49        }
50
51        void addCallTechnology(int callTechnology) {
52        }
53
54        void setCreatedFromExistingConnection(boolean createdFromExistingConnection) {
55        }
56
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
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
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
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
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
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
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
144        public void addCallTechnology(int callTechnology) {
145            Log.d(TAG, "adding callTechnology for call " + callId + ": " + callTechnology);
146            this.callTechnologies |= callTechnology;
147        }
148
149        @Override
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
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
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
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
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
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
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
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
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
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
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
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     */
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