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