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.AudioRoute.TYPE_BLUETOOTH_HA; 20 import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_LE; 21 import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO; 22 import static com.android.server.telecom.AudioRoute.TYPE_DOCK; 23 import static com.android.server.telecom.AudioRoute.TYPE_EARPIECE; 24 import static com.android.server.telecom.AudioRoute.TYPE_SPEAKER; 25 import static com.android.server.telecom.AudioRoute.TYPE_STREAMING; 26 import static com.android.server.telecom.AudioRoute.TYPE_WIRED; 27 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS; 28 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH; 29 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH_LE; 30 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_EARPIECE; 31 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_HEARING_AID; 32 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_PHONE_SPEAKER; 33 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_UNSPECIFIED; 34 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WATCH_SPEAKER; 35 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WIRED_HEADSET; 36 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH; 37 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE; 38 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE; 39 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_HEARING_AID; 40 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_PHONE_SPEAKER; 41 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_UNSPECIFIED; 42 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WATCH_SPEAKER; 43 import static com.android.server.telecom.TelecomStatsLog.CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WIRED_HEADSET; 44 45 import android.annotation.NonNull; 46 import android.app.StatsManager; 47 import android.content.Context; 48 import android.os.Looper; 49 import android.os.Message; 50 import android.os.SystemClock; 51 import android.telecom.Log; 52 import android.util.Pair; 53 import android.util.StatsEvent; 54 55 import androidx.annotation.VisibleForTesting; 56 57 import com.android.server.telecom.AudioRoute; 58 import com.android.server.telecom.PendingAudioRoute; 59 import com.android.server.telecom.TelecomStatsLog; 60 import com.android.server.telecom.nano.PulledAtomsClass; 61 62 import java.util.Arrays; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Objects; 67 68 public class AudioRouteStats extends TelecomPulledAtom { 69 @VisibleForTesting 70 public static final long THRESHOLD_REVERT_MS = 5000; 71 @VisibleForTesting 72 public static final int EVENT_REVERT_THRESHOLD_EXPIRED = EVENT_SUB_BASE + 1; 73 private static final String TAG = AudioRouteStats.class.getSimpleName(); 74 private static final String FILE_NAME = "audio_route_stats"; 75 private Map<AudioRouteStatsKey, AudioRouteStatsData> mAudioRouteStatsMap; 76 private Pair<AudioRouteStatsKey, long[]> mCur; 77 private boolean mIsOngoing; 78 AudioRouteStats(@onNull Context context, @NonNull Looper looper, boolean isTestMode)79 public AudioRouteStats(@NonNull Context context, @NonNull Looper looper, boolean isTestMode) { 80 super(context, looper, isTestMode); 81 } 82 83 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 84 @Override getTag()85 public int getTag() { 86 return CALL_AUDIO_ROUTE_STATS; 87 } 88 89 @Override getFileName()90 protected String getFileName() { 91 return FILE_NAME; 92 } 93 94 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 95 @Override onPull(final List<StatsEvent> data)96 public synchronized int onPull(final List<StatsEvent> data) { 97 if (mPulledAtoms.callAudioRouteStats.length != 0) { 98 Arrays.stream(mPulledAtoms.callAudioRouteStats).forEach(v -> data.add( 99 TelecomStatsLog.buildStatsEvent(getTag(), 100 v.getCallAudioRouteSource(), v.getCallAudioRouteDest(), 101 v.getSuccess(), v.getRevert(), v.getCount(), v.getAverageLatencyMs()))); 102 mAudioRouteStatsMap.clear(); 103 onAggregate(); 104 return StatsManager.PULL_SUCCESS; 105 } else { 106 return StatsManager.PULL_SKIP; 107 } 108 } 109 110 @Override onLoad()111 protected synchronized void onLoad() { 112 if (mPulledAtoms.callAudioRouteStats != null) { 113 mAudioRouteStatsMap = new HashMap<>(); 114 for (PulledAtomsClass.CallAudioRouteStats v : mPulledAtoms.callAudioRouteStats) { 115 mAudioRouteStatsMap.put(new AudioRouteStatsKey(v.getCallAudioRouteSource(), 116 v.getCallAudioRouteDest(), v.getSuccess(), v.getRevert()), 117 new AudioRouteStatsData(v.getCount(), v.getAverageLatencyMs())); 118 } 119 mLastPulledTimestamps = mPulledAtoms.getCallAudioRouteStatsPullTimestampMillis(); 120 } 121 } 122 123 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 124 @Override onAggregate()125 public synchronized void onAggregate() { 126 Log.d(TAG, "onAggregate: %s", mAudioRouteStatsMap); 127 clearAtoms(); 128 if (mAudioRouteStatsMap.isEmpty()) { 129 return; 130 } 131 mPulledAtoms.setCallAudioRouteStatsPullTimestampMillis(mLastPulledTimestamps); 132 mPulledAtoms.callAudioRouteStats = 133 new PulledAtomsClass.CallAudioRouteStats[mAudioRouteStatsMap.size()]; 134 int[] index = new int[1]; 135 mAudioRouteStatsMap.forEach((k, v) -> { 136 mPulledAtoms.callAudioRouteStats[index[0]] = new PulledAtomsClass.CallAudioRouteStats(); 137 mPulledAtoms.callAudioRouteStats[index[0]].setCallAudioRouteSource(k.mSource); 138 mPulledAtoms.callAudioRouteStats[index[0]].setCallAudioRouteDest(k.mDest); 139 mPulledAtoms.callAudioRouteStats[index[0]].setSuccess(k.mIsSuccess); 140 mPulledAtoms.callAudioRouteStats[index[0]].setRevert(k.mIsRevert); 141 mPulledAtoms.callAudioRouteStats[index[0]].setCount(v.mCount); 142 mPulledAtoms.callAudioRouteStats[index[0]].setAverageLatencyMs(v.mAverageLatency); 143 index[0]++; 144 }); 145 save(DELAY_FOR_PERSISTENT_MILLIS); 146 } 147 148 @VisibleForTesting log(int source, int target, boolean isSuccess, boolean isRevert, int latency)149 public void log(int source, int target, boolean isSuccess, boolean isRevert, int latency) { 150 post(() -> onLog(new AudioRouteStatsKey(source, target, isSuccess, isRevert), latency)); 151 } 152 onRouteEnter(PendingAudioRoute pendingRoute)153 public void onRouteEnter(PendingAudioRoute pendingRoute) { 154 int sourceType = convertAudioType(pendingRoute.getOrigRoute(), true); 155 int destType = convertAudioType(pendingRoute.getDestRoute(), false); 156 long curTime = SystemClock.elapsedRealtime(); 157 158 post(() -> { 159 // Ignore the transition route 160 if (!mIsOngoing) { 161 mIsOngoing = true; 162 // Check if the previous route is reverted as the revert time has not been expired. 163 if (mCur != null) { 164 if (destType == mCur.first.getSource() && curTime - mCur.second[0] 165 < THRESHOLD_REVERT_MS) { 166 mCur.first.setRevert(true); 167 } 168 if (mCur.second[1] < 0) { 169 mCur.second[1] = curTime; 170 } 171 onLog(); 172 } 173 mCur = new Pair<>(new AudioRouteStatsKey(sourceType, destType), new long[]{curTime, 174 -1}); 175 if (hasMessages(EVENT_REVERT_THRESHOLD_EXPIRED)) { 176 // Only keep the latest event 177 removeMessages(EVENT_REVERT_THRESHOLD_EXPIRED); 178 } 179 sendMessageDelayed( 180 obtainMessage(EVENT_REVERT_THRESHOLD_EXPIRED), THRESHOLD_REVERT_MS); 181 } 182 }); 183 } 184 onRouteExit(PendingAudioRoute pendingRoute, boolean isSuccess)185 public void onRouteExit(PendingAudioRoute pendingRoute, boolean isSuccess) { 186 // Check the dest type on the route exiting as it may be different as the enter 187 int destType = convertAudioType(pendingRoute.getDestRoute(), false); 188 long curTime = SystemClock.elapsedRealtime(); 189 post(() -> { 190 if (mIsOngoing) { 191 mIsOngoing = false; 192 // Should not be null unless the route is not done before the revert timer expired. 193 if (mCur != null) { 194 mCur.first.setDestType(destType); 195 mCur.first.setSuccess(isSuccess); 196 mCur.second[1] = curTime; 197 } 198 } 199 }); 200 } 201 onLog()202 private void onLog() { 203 if (mCur != null) { 204 // Ignore the case if the source and dest types are same 205 if (mCur.first.mSource != mCur.first.mDest) { 206 // The route should have been done before the revert timer expires. Otherwise, it 207 // would be logged as the failed case 208 if (mCur.second[1] < 0) { 209 mCur.second[1] = SystemClock.elapsedRealtime(); 210 } 211 onLog(mCur.first, (int) (mCur.second[1] - mCur.second[0])); 212 } 213 mCur = null; 214 } 215 } 216 onLog(AudioRouteStatsKey key, int latency)217 private void onLog(AudioRouteStatsKey key, int latency) { 218 AudioRouteStatsData data = mAudioRouteStatsMap.computeIfAbsent(key, 219 k -> new AudioRouteStatsData(0, 0)); 220 data.add(latency); 221 onAggregate(); 222 } 223 convertAudioType(AudioRoute route, boolean isSource)224 private int convertAudioType(AudioRoute route, boolean isSource) { 225 if (route != null) { 226 switch (route.getType()) { 227 case TYPE_EARPIECE: 228 return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_EARPIECE 229 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_EARPIECE; 230 case TYPE_WIRED: 231 return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WIRED_HEADSET 232 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WIRED_HEADSET; 233 case TYPE_SPEAKER: 234 return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_PHONE_SPEAKER 235 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_PHONE_SPEAKER; 236 case TYPE_BLUETOOTH_LE: 237 return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH_LE 238 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH_LE; 239 case TYPE_BLUETOOTH_SCO: 240 if (isSource) { 241 return route.isWatch() 242 ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_WATCH_SPEAKER 243 : CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_BLUETOOTH; 244 } else { 245 return route.isWatch() 246 ? CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_WATCH_SPEAKER 247 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_BLUETOOTH; 248 } 249 case TYPE_BLUETOOTH_HA: 250 return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_HEARING_AID 251 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_HEARING_AID; 252 case TYPE_DOCK: 253 // Reserved for the future 254 case TYPE_STREAMING: 255 // Reserved for the future 256 default: 257 break; 258 } 259 } 260 261 return isSource ? CALL_AUDIO_ROUTE_STATS__ROUTE_SOURCE__CALL_AUDIO_UNSPECIFIED 262 : CALL_AUDIO_ROUTE_STATS__ROUTE_DEST__CALL_AUDIO_UNSPECIFIED; 263 } 264 265 @Override handleMessage(Message msg)266 public void handleMessage(Message msg) { 267 switch (msg.what) { 268 case EVENT_REVERT_THRESHOLD_EXPIRED: 269 onLog(); 270 break; 271 default: 272 super.handleMessage(msg); 273 } 274 } 275 276 static class AudioRouteStatsKey { 277 278 final int mSource; 279 int mDest; 280 boolean mIsSuccess; 281 boolean mIsRevert; 282 AudioRouteStatsKey(int source, int dest)283 AudioRouteStatsKey(int source, int dest) { 284 mSource = source; 285 mDest = dest; 286 } 287 AudioRouteStatsKey(int source, int dest, boolean isSuccess, boolean isRevert)288 AudioRouteStatsKey(int source, int dest, boolean isSuccess, boolean isRevert) { 289 mSource = source; 290 mDest = dest; 291 mIsSuccess = isSuccess; 292 mIsRevert = isRevert; 293 } 294 setDestType(int dest)295 void setDestType(int dest) { 296 mDest = dest; 297 } 298 setSuccess(boolean isSuccess)299 void setSuccess(boolean isSuccess) { 300 mIsSuccess = isSuccess; 301 } 302 setRevert(boolean isRevert)303 void setRevert(boolean isRevert) { 304 mIsRevert = isRevert; 305 } 306 getSource()307 int getSource() { 308 return mSource; 309 } 310 311 @Override equals(Object other)312 public boolean equals(Object other) { 313 if (this == other) { 314 return true; 315 } 316 if (!(other instanceof AudioRouteStatsKey obj)) { 317 return false; 318 } 319 return this.mSource == obj.mSource && this.mDest == obj.mDest 320 && this.mIsSuccess == obj.mIsSuccess && this.mIsRevert == obj.mIsRevert; 321 } 322 323 @Override hashCode()324 public int hashCode() { 325 return Objects.hash(mSource, mDest, mIsSuccess, mIsRevert); 326 } 327 328 @Override toString()329 public String toString() { 330 return "[AudioRouteStatsKey: mSource=" + mSource + ", mDest=" + mDest 331 + ", mIsSuccess=" + mIsSuccess + ", mIsRevert=" + mIsRevert + "]"; 332 } 333 } 334 335 static class AudioRouteStatsData { 336 337 int mCount; 338 int mAverageLatency; 339 AudioRouteStatsData(int count, int averageLatency)340 AudioRouteStatsData(int count, int averageLatency) { 341 mCount = count; 342 mAverageLatency = averageLatency; 343 } 344 add(int latency)345 void add(int latency) { 346 mCount++; 347 mAverageLatency += (latency - mAverageLatency) / mCount; 348 } 349 350 @Override toString()351 public String toString() { 352 return "[AudioRouteStatsData: mCount=" + mCount + ", mAverageLatency:" 353 + mAverageLatency + "]"; 354 } 355 } 356 } 357