• 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.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