• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.metrics;
18 
19 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS;
20 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_MANAGED;
21 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SELFMANAGED;
22 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM;
23 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
24 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API;
25 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP;
26 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP_WITH_TELECOM_SUPPORT;
27 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_INCOMING;
28 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_OUTGOING;
29 import static com.android.server.telecom.TelecomStatsLog.CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN;
30 
31 import android.annotation.NonNull;
32 import android.app.StatsManager;
33 import android.content.Context;
34 import android.os.Looper;
35 import android.telecom.Log;
36 import android.telecom.PhoneAccount;
37 import android.util.StatsEvent;
38 
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.server.telecom.Call;
42 import com.android.server.telecom.TelecomStatsLog;
43 import com.android.server.telecom.nano.PulledAtomsClass;
44 
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.Set;
53 
54 public class CallStats extends TelecomPulledAtom {
55     private static final String TAG = CallStats.class.getSimpleName();
56 
57     private static final String FILE_NAME = "call_stats";
58     private final Set<String> mOngoingCallsWithoutMultipleAudioDevices = new HashSet<>();
59     private final Set<String> mOngoingCallsWithMultipleAudioDevices = new HashSet<>();
60     private Map<CallStatsKey, CallStatsData> mCallStatsMap;
61     private boolean mHasMultipleAudioDevices;
62 
CallStats(@onNull Context context, @NonNull Looper looper, boolean isTestMode)63     public CallStats(@NonNull Context context, @NonNull Looper looper, boolean isTestMode) {
64         super(context, looper, isTestMode);
65     }
66 
67     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
68     @Override
getTag()69     public int getTag() {
70         return CALL_STATS;
71     }
72 
73     @Override
getFileName()74     protected String getFileName() {
75         return FILE_NAME;
76     }
77 
78     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
79     @Override
onPull(final List<StatsEvent> data)80     public synchronized int onPull(final List<StatsEvent> data) {
81         if (mPulledAtoms.callStats.length != 0) {
82             Arrays.stream(mPulledAtoms.callStats).forEach(v -> data.add(
83                     TelecomStatsLog.buildStatsEvent(getTag(),
84                             v.getCallDirection(), v.getExternalCall(), v.getEmergencyCall(),
85                             v.getMultipleAudioAvailable(), v.getAccountType(), v.getUid(),
86                             v.getCount(), v.getAverageDurationMs(), v.getDisconnectCause(),
87                             v.getSimultaneousType(), v.getVideoCall())));
88             mCallStatsMap.clear();
89             onAggregate();
90             return StatsManager.PULL_SUCCESS;
91         } else {
92             return StatsManager.PULL_SKIP;
93         }
94     }
95 
96     @Override
onLoad()97     protected synchronized void onLoad() {
98         if (mPulledAtoms.callStats != null) {
99             mCallStatsMap = new HashMap<>();
100             for (PulledAtomsClass.CallStats v : mPulledAtoms.callStats) {
101                 mCallStatsMap.put(new CallStatsKey(v.getCallDirection(),
102                         v.getExternalCall(), v.getEmergencyCall(),
103                         v.getMultipleAudioAvailable(), v.getAccountType(),
104                         v.getUid(), v.getDisconnectCause(), v.getSimultaneousType(),
105                         v.getVideoCall()),
106                         new CallStatsData(
107                                 v.getCount(), v.getAverageDurationMs()));
108             }
109             mLastPulledTimestamps = mPulledAtoms.getCallStatsPullTimestampMillis();
110         }
111     }
112 
113     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
114     @Override
onAggregate()115     public synchronized void onAggregate() {
116         Log.d(TAG, "onAggregate: %s", mCallStatsMap);
117         clearAtoms();
118         if (mCallStatsMap.isEmpty()) {
119             return;
120         }
121         mPulledAtoms.setCallStatsPullTimestampMillis(mLastPulledTimestamps);
122         mPulledAtoms.callStats = new PulledAtomsClass.CallStats[mCallStatsMap.size()];
123         int[] index = new int[1];
124         mCallStatsMap.forEach((k, v) -> {
125             mPulledAtoms.callStats[index[0]] = new PulledAtomsClass.CallStats();
126             mPulledAtoms.callStats[index[0]].setCallDirection(k.mDirection);
127             mPulledAtoms.callStats[index[0]].setExternalCall(k.mIsExternal);
128             mPulledAtoms.callStats[index[0]].setEmergencyCall(k.mIsEmergency);
129             mPulledAtoms.callStats[index[0]].setMultipleAudioAvailable(k.mIsMultipleAudioAvailable);
130             mPulledAtoms.callStats[index[0]].setAccountType(k.mAccountType);
131             mPulledAtoms.callStats[index[0]].setUid(k.mUid);
132             mPulledAtoms.callStats[index[0]].setDisconnectCause(k.mCause);
133             mPulledAtoms.callStats[index[0]].setSimultaneousType(k.mSimultaneousType);
134             mPulledAtoms.callStats[index[0]].setVideoCall(k.mHasVideoCall);
135             mPulledAtoms.callStats[index[0]].setCount(v.mCount);
136             mPulledAtoms.callStats[index[0]].setAverageDurationMs(v.mAverageDuration);
137             index[0]++;
138         });
139         save(DELAY_FOR_PERSISTENT_MILLIS);
140     }
141 
log(int direction, boolean isExternal, boolean isEmergency, boolean isMultipleAudioAvailable, int accountType, int uid, int duration)142     public void log(int direction, boolean isExternal, boolean isEmergency,
143         boolean isMultipleAudioAvailable, int accountType, int uid, int duration) {
144         log(direction, isExternal, isEmergency, isMultipleAudioAvailable, accountType, uid,
145                 0, 0, false, duration);
146     }
147 
log(int direction, boolean isExternal, boolean isEmergency, boolean isMultipleAudioAvailable, int accountType, int uid, int disconnectCause, int simultaneousType, boolean hasVideoCall, int duration)148     public void log(int direction, boolean isExternal, boolean isEmergency,
149             boolean isMultipleAudioAvailable, int accountType, int uid,
150             int disconnectCause, int simultaneousType, boolean hasVideoCall, int duration) {
151         post(() -> {
152             CallStatsKey key = new CallStatsKey(direction, isExternal, isEmergency,
153                     isMultipleAudioAvailable, accountType, uid, disconnectCause, simultaneousType,
154                     hasVideoCall);
155             CallStatsData data = mCallStatsMap.computeIfAbsent(key, k -> new CallStatsData(0, 0));
156             data.add(duration);
157             onAggregate();
158         });
159     }
160 
onCallStart(Call call)161     public void onCallStart(Call call) {
162         post(() -> {
163             if (mHasMultipleAudioDevices) {
164                 mOngoingCallsWithMultipleAudioDevices.add(call.getId());
165             } else {
166                 mOngoingCallsWithoutMultipleAudioDevices.add(call.getId());
167             }
168         });
169     }
170 
onCallEnd(Call call)171     public void onCallEnd(Call call) {
172         final int duration = (int) (call.getAgeMillis());
173         post(() -> {
174             final boolean hasMultipleAudioDevices = mOngoingCallsWithMultipleAudioDevices.remove(
175                     call.getId());
176             final int direction = call.isIncoming() ? CALL_STATS__CALL_DIRECTION__DIR_INCOMING
177                     : (call.isOutgoing() ? CALL_STATS__CALL_DIRECTION__DIR_OUTGOING
178                     : CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN);
179             final int accountType = getAccountType(call.getPhoneAccountFromHandle());
180             int uid = call.getCallingPackageIdentity().mCallingPackageUid;
181             try {
182                 uid = mContext.getPackageManager().getApplicationInfo(
183                         call.getTargetPhoneAccount().getComponentName().getPackageName(), 0).uid;
184             } catch (Exception e) {
185                 Log.i(TAG, "failed to get the uid for " + e);
186             }
187 
188             log(direction, call.isExternalCall(), call.isEmergencyCall(), hasMultipleAudioDevices,
189                     accountType, uid, call.getDisconnectCause().getCode(),
190                     call.getSimultaneousType(), call.hasVideoCall(), duration);
191         });
192     }
193 
194     /**
195      * Used for logging non-telecom calls that have no associated {@link Call}.  This is inferred
196      * from the {@link com.android.server.telecom.CallAudioWatchdog}.
197      *
198      * @param hasTelecomSupport {@code true} if the app making the non-telecom call has Telecom
199      *                                      support (i.e. has a phone account};
200      *                                      {@code false} otherwise.
201      * @param uid The uid of the app making the call.
202      * @param durationMillis The duration of the call, in millis.
203      */
onNonTelecomCallEnd(final boolean hasTelecomSupport, final int uid, final long durationMillis)204     public void onNonTelecomCallEnd(final boolean hasTelecomSupport, final int uid,
205             final long durationMillis) {
206         post(() -> log(CALL_STATS__CALL_DIRECTION__DIR_UNKNOWN,
207                 false /* isExternalCall */,
208                 false /* isEmergencyCall */,
209                 false /* hasMultipleAudioDevices  */,
210                 hasTelecomSupport ?
211                         CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP_WITH_TELECOM_SUPPORT :
212                         CALL_STATS__ACCOUNT_TYPE__ACCOUNT_NON_TELECOM_VOIP,
213                 uid, (int) durationMillis));
214     }
215 
getAccountType(PhoneAccount account)216     private int getAccountType(PhoneAccount account) {
217         if (account == null) {
218             return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
219         }
220         if (account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
221             return account.hasCapabilities(
222                     PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
223                     ? CALL_STATS__ACCOUNT_TYPE__ACCOUNT_VOIP_API
224                     : CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SELFMANAGED;
225         }
226         if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
227             return account.hasCapabilities(
228                     PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
229                     ? CALL_STATS__ACCOUNT_TYPE__ACCOUNT_SIM
230                     : CALL_STATS__ACCOUNT_TYPE__ACCOUNT_MANAGED;
231         }
232         return CALL_STATS__ACCOUNT_TYPE__ACCOUNT_UNKNOWN;
233     }
234 
onAudioDevicesChange(boolean hasMultipleAudioDevices)235     public void onAudioDevicesChange(boolean hasMultipleAudioDevices) {
236         post(() -> {
237             if (mHasMultipleAudioDevices != hasMultipleAudioDevices) {
238                 mHasMultipleAudioDevices = hasMultipleAudioDevices;
239                 if (mHasMultipleAudioDevices) {
240                     mOngoingCallsWithMultipleAudioDevices.addAll(
241                             mOngoingCallsWithoutMultipleAudioDevices);
242                     mOngoingCallsWithoutMultipleAudioDevices.clear();
243                 }
244             }
245         });
246     }
247 
248     static class CallStatsKey {
249         final int mDirection;
250         final boolean mIsExternal;
251         final boolean mIsEmergency;
252         final boolean mIsMultipleAudioAvailable;
253         final int mAccountType;
254         final int mUid;
255         final int mCause;
256         final int mSimultaneousType;
257         final boolean mHasVideoCall;
258 
CallStatsKey(int direction, boolean isExternal, boolean isEmergency, boolean isMultipleAudioAvailable, int accountType, int uid)259         CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
260             boolean isMultipleAudioAvailable, int accountType, int uid) {
261             this(direction, isExternal, isEmergency, isMultipleAudioAvailable, accountType, uid,
262                     0, 0, false);
263         }
264 
CallStatsKey(int direction, boolean isExternal, boolean isEmergency, boolean isMultipleAudioAvailable, int accountType, int uid, int cause, int simultaneousType, boolean hasVideoCall)265         CallStatsKey(int direction, boolean isExternal, boolean isEmergency,
266                 boolean isMultipleAudioAvailable, int accountType, int uid,
267                 int cause, int simultaneousType, boolean hasVideoCall) {
268             mDirection = direction;
269             mIsExternal = isExternal;
270             mIsEmergency = isEmergency;
271             mIsMultipleAudioAvailable = isMultipleAudioAvailable;
272             mAccountType = accountType;
273             mUid = uid;
274             mCause = cause;
275             mSimultaneousType = simultaneousType;
276             mHasVideoCall = hasVideoCall;
277         }
278 
279         @Override
equals(Object other)280         public boolean equals(Object other) {
281             if (this == other) {
282                 return true;
283             }
284             if (!(other instanceof CallStatsKey obj)) {
285                 return false;
286             }
287             return this.mDirection == obj.mDirection && this.mIsExternal == obj.mIsExternal
288                     && this.mIsEmergency == obj.mIsEmergency
289                     && this.mIsMultipleAudioAvailable == obj.mIsMultipleAudioAvailable
290                     && this.mAccountType == obj.mAccountType && this.mUid == obj.mUid
291                     && this.mCause == obj.mCause && this.mSimultaneousType == obj.mSimultaneousType
292                     && this.mHasVideoCall == obj.mHasVideoCall;
293         }
294 
295         @Override
hashCode()296         public int hashCode() {
297             return Objects.hash(mDirection, mIsExternal, mIsEmergency, mIsMultipleAudioAvailable,
298                     mAccountType, mUid, mCause, mSimultaneousType, mHasVideoCall);
299         }
300 
301         @Override
toString()302         public String toString() {
303             return "[CallStatsKey: mDirection=" + mDirection + ", mIsExternal=" + mIsExternal
304                     + ", mIsEmergency=" + mIsEmergency + ", mIsMultipleAudioAvailable="
305                     + mIsMultipleAudioAvailable + ", mAccountType=" + mAccountType + ", mUid="
306                     + mUid + ", mCause=" + mCause + ", mScType=" + mSimultaneousType
307                     + ", mHasVideoCall =" + mHasVideoCall + "]";
308         }
309     }
310 
311     static class CallStatsData {
312 
313         int mCount;
314         int mAverageDuration;
315 
CallStatsData(int count, int averageDuration)316         CallStatsData(int count, int averageDuration) {
317             mCount = count;
318             mAverageDuration = averageDuration;
319         }
320 
add(int duration)321         void add(int duration) {
322             mCount++;
323             mAverageDuration += (duration - mAverageDuration) / mCount;
324         }
325 
326         @Override
toString()327         public String toString() {
328             return "[CallStatsData: mCount=" + mCount + ", mAverageDuration:" + mAverageDuration
329                     + "]";
330         }
331     }
332 }
333