1 /*
2  * Copyright 2023 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 #include "le_audio_health_status.h"
18 
19 #include <bluetooth/log.h>
20 
21 #include <vector>
22 
23 #include "bta/include/bta_groups.h"
24 #include "common/strings.h"
25 #include "main/shim/metrics_api.h"
26 #include "os/log.h"
27 #include "osi/include/properties.h"
28 
29 using bluetooth::common::ToString;
30 using bluetooth::groups::kGroupUnknown;
31 using bluetooth::le_audio::LeAudioDevice;
32 using bluetooth::le_audio::LeAudioHealthStatus;
33 using bluetooth::le_audio::LeAudioRecommendationActionCb;
34 
35 namespace bluetooth::le_audio {
36 class LeAudioHealthStatusImpl;
37 LeAudioHealthStatusImpl* instance;
38 
39 class LeAudioHealthStatusImpl : public LeAudioHealthStatus {
40  public:
LeAudioHealthStatusImpl(void)41   LeAudioHealthStatusImpl(void) { log::debug("Initiated"); }
42 
~LeAudioHealthStatusImpl(void)43   ~LeAudioHealthStatusImpl(void) { clear_module(); }
44 
RegisterCallback(LeAudioRecommendationActionCb cb)45   void RegisterCallback(LeAudioRecommendationActionCb cb) override {
46     register_callback(std::move(cb));
47   }
48 
RemoveStatistics(const RawAddress & address,int group_id)49   void RemoveStatistics(const RawAddress& address, int group_id) override {
50     log::debug("{}, group_id: {}", address, group_id);
51     remove_device(address);
52     remove_group(group_id);
53   }
54 
AddStatisticForDevice(const LeAudioDevice * device,LeAudioHealthDeviceStatType type)55   void AddStatisticForDevice(const LeAudioDevice* device,
56                              LeAudioHealthDeviceStatType type) override {
57     if (device == nullptr) {
58       log::error("device is null");
59       return;
60     }
61 
62     const RawAddress& address = device->address_;
63     log::debug("{}, {}", address, ToString(type));
64 
65     auto dev = find_device(address);
66     if (dev == nullptr) {
67       add_device(address);
68       dev = find_device(address);
69       if (dev == nullptr) {
70         log::error("Could not add device {}", address);
71         return;
72       }
73     }
74     // log counter metrics
75     log_counter_metrics_for_device(type, device->allowlist_flag_);
76 
77     LeAudioHealthBasedAction action;
78     switch (type) {
79       case LeAudioHealthDeviceStatType::VALID_DB:
80         dev->is_valid_service_ = true;
81         action = LeAudioHealthBasedAction::NONE;
82         break;
83       case LeAudioHealthDeviceStatType::INVALID_DB:
84         dev->is_valid_service_ = false;
85         action = LeAudioHealthBasedAction::DISABLE;
86         break;
87       case LeAudioHealthDeviceStatType::INVALID_CSIS:
88         dev->is_valid_group_member_ = false;
89         action = LeAudioHealthBasedAction::DISABLE;
90         break;
91       case LeAudioHealthDeviceStatType::VALID_CSIS:
92         dev->is_valid_group_member_ = true;
93         action = LeAudioHealthBasedAction::NONE;
94         break;
95     }
96 
97     if (dev->latest_recommendation_ != action) {
98       dev->latest_recommendation_ = action;
99       send_recommendation_for_device(address, action);
100       return;
101     }
102   }
103 
AddStatisticForGroup(const LeAudioDeviceGroup * device_group,LeAudioHealthGroupStatType type)104   void AddStatisticForGroup(const LeAudioDeviceGroup* device_group,
105                             LeAudioHealthGroupStatType type) override {
106     if (device_group == nullptr) {
107       log::error("device_group is null");
108       return;
109     }
110 
111     int group_id = device_group->group_id_;
112     log::debug("group_id: {}, {}", group_id, ToString(type));
113 
114     auto group = find_group(group_id);
115     if (group == nullptr) {
116       add_group(group_id);
117       group = find_group(group_id);
118       if (group == nullptr) {
119         log::error("Could not add group {}", group_id);
120         return;
121       }
122     }
123 
124     LeAudioDevice* device = device_group->GetFirstDevice();
125     if (device == nullptr) {
126       log::error("Front device is null. Number of devices: {}",
127                  device_group->Size());
128       return;
129     }
130     // log counter metrics
131     log_counter_metrics_for_group(type, device->allowlist_flag_);
132 
133     switch (type) {
134       case LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS:
135         group->stream_success_cnt_++;
136         if (group->latest_recommendation_ == LeAudioHealthBasedAction::NONE) {
137           return;
138         }
139         break;
140       case LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED:
141         group->stream_cis_failures_cnt_++;
142         group->stream_failures_cnt_++;
143         break;
144       case LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED:
145         group->stream_signaling_failures_cnt_++;
146         group->stream_failures_cnt_++;
147         break;
148       case LeAudioHealthGroupStatType::STREAM_CONTEXT_NOT_AVAILABLE:
149         group->stream_context_not_avail_cnt_++;
150         break;
151     }
152 
153     LeAudioHealthBasedAction action = LeAudioHealthBasedAction::NONE;
154     if (group->stream_success_cnt_ == 0) {
155       /* Never succeed in stream creation */
156       if ((group->stream_failures_cnt_ >=
157            MAX_ALLOWED_FAILURES_IN_A_ROW_WITHOUT_SUCCESS)) {
158         action = LeAudioHealthBasedAction::DISABLE;
159       } else if (group->stream_context_not_avail_cnt_ >=
160                  MAX_ALLOWED_FAILURES_IN_A_ROW_WITHOUT_SUCCESS) {
161         action = LeAudioHealthBasedAction::INACTIVATE_GROUP;
162         group->stream_context_not_avail_cnt_ = 0;
163       }
164     } else {
165       /* Had some success before */
166       if ((100 * group->stream_failures_cnt_ / group->stream_success_cnt_) >=
167           THRESHOLD_FOR_DISABLE_CONSIDERATION) {
168         action = LeAudioHealthBasedAction::CONSIDER_DISABLING;
169       } else if (group->stream_context_not_avail_cnt_ >=
170                  MAX_ALLOWED_FAILURES_IN_A_ROW_WITHOUT_SUCCESS) {
171         action = LeAudioHealthBasedAction::INACTIVATE_GROUP;
172         group->stream_context_not_avail_cnt_ = 0;
173       }
174     }
175 
176     if (group->latest_recommendation_ != action) {
177       group->latest_recommendation_ = action;
178       send_recommendation_for_group(group_id, action);
179     }
180   }
181 
Dump(int fd)182   void Dump(int fd) {
183     dprintf(fd, "  LeAudioHealthStats: \n    groups:");
184     for (const auto& g : group_stats_) {
185       dumpsys_group(fd, g);
186     }
187     dprintf(fd, "\n    devices: ");
188     for (const auto& dev : devices_stats_) {
189       dumpsys_dev(fd, dev);
190     }
191     dprintf(fd, "\n");
192   }
193 
194  private:
195   static constexpr int MAX_ALLOWED_FAILURES_IN_A_ROW_WITHOUT_SUCCESS = 3;
196   static constexpr int THRESHOLD_FOR_DISABLE_CONSIDERATION = 70;
197 
198   std::vector<LeAudioRecommendationActionCb> callbacks_;
199   std::vector<device_stats> devices_stats_;
200   std::vector<group_stats> group_stats_;
201 
dumpsys_group(int fd,const group_stats & group)202   void dumpsys_group(int fd, const group_stats& group) {
203     std::stringstream stream;
204 
205     stream << "\n group_id: " << group.group_id_ << ": "
206            << group.latest_recommendation_
207            << ", success: " << group.stream_success_cnt_
208            << ", fail total: " << group.stream_failures_cnt_
209            << ", fail cis: " << group.stream_cis_failures_cnt_
210            << ", fail signaling: " << group.stream_signaling_failures_cnt_
211            << ", context not avail: " << group.stream_context_not_avail_cnt_;
212 
213     dprintf(fd, "%s", stream.str().c_str());
214   }
215 
dumpsys_dev(int fd,const device_stats & dev)216   void dumpsys_dev(int fd, const device_stats& dev) {
217     std::stringstream stream;
218 
219     stream << "\n " << ADDRESS_TO_LOGGABLE_STR(dev.address_) << ": "
220            << dev.latest_recommendation_
221            << (dev.is_valid_service_ ? " service: OK" : " service : NOK")
222            << (dev.is_valid_group_member_ ? " csis: OK" : " csis : NOK");
223 
224     dprintf(fd, "%s", stream.str().c_str());
225   }
226 
clear_module(void)227   void clear_module(void) {
228     devices_stats_.clear();
229     group_stats_.clear();
230     callbacks_.clear();
231   }
232 
send_recommendation_for_device(const RawAddress & address,LeAudioHealthBasedAction recommendation)233   void send_recommendation_for_device(const RawAddress& address,
234                                       LeAudioHealthBasedAction recommendation) {
235     log::debug("{}, {}", address, ToString(recommendation));
236     /* Notify new user about known groups */
237     for (auto& cb : callbacks_) {
238       cb.Run(address, kGroupUnknown, recommendation);
239     }
240   }
241 
send_recommendation_for_group(int group_id,const LeAudioHealthBasedAction recommendation)242   void send_recommendation_for_group(
243       int group_id, const LeAudioHealthBasedAction recommendation) {
244     log::debug("group_id: {}, {}", group_id, ToString(recommendation));
245     /* Notify new user about known groups */
246     for (auto& cb : callbacks_) {
247       cb.Run(RawAddress::kEmpty, group_id, recommendation);
248     }
249   }
250 
add_device(const RawAddress & address)251   void add_device(const RawAddress& address) {
252     devices_stats_.emplace_back(device_stats(address));
253   }
254 
add_group(int group_id)255   void add_group(int group_id) {
256     group_stats_.emplace_back(group_stats(group_id));
257   }
258 
remove_group(int group_id)259   void remove_group(int group_id) {
260     if (group_id == kGroupUnknown) {
261       return;
262     }
263     auto iter = std::find_if(
264         group_stats_.begin(), group_stats_.end(),
265         [group_id](const auto& g) { return g.group_id_ == group_id; });
266     if (iter != group_stats_.end()) {
267       group_stats_.erase(iter);
268     }
269   }
270 
remove_device(const RawAddress & address)271   void remove_device(const RawAddress& address) {
272     auto iter = std::find_if(
273         devices_stats_.begin(), devices_stats_.end(),
274         [address](const auto& d) { return d.address_ == address; });
275     if (iter != devices_stats_.end()) {
276       devices_stats_.erase(iter);
277     }
278   }
279 
register_callback(LeAudioRecommendationActionCb cb)280   void register_callback(LeAudioRecommendationActionCb cb) {
281     callbacks_.push_back(std::move(cb));
282   }
283 
find_device(const RawAddress & address)284   device_stats* find_device(const RawAddress& address) {
285     auto iter = std::find_if(
286         devices_stats_.begin(), devices_stats_.end(),
287         [address](const auto& d) { return d.address_ == address; });
288     if (iter == devices_stats_.end()) return nullptr;
289 
290     return &(*iter);
291   }
292 
find_group(int group_id)293   group_stats* find_group(int group_id) {
294     auto iter = std::find_if(
295         group_stats_.begin(), group_stats_.end(),
296         [group_id](const auto& g) { return g.group_id_ == group_id; });
297     if (iter == group_stats_.end()) return nullptr;
298 
299     return &(*iter);
300   }
301 
log_counter_metrics_for_device(LeAudioHealthDeviceStatType type,bool in_allowlist)302   void log_counter_metrics_for_device(LeAudioHealthDeviceStatType type,
303                                       bool in_allowlist) {
304     log::debug("in_allowlist: {}, type: {}", in_allowlist, ToString(type));
305     android::bluetooth::CodePathCounterKeyEnum key;
306     if (in_allowlist) {
307       switch (type) {
308         case LeAudioHealthDeviceStatType::VALID_DB:
309         case LeAudioHealthDeviceStatType::VALID_CSIS:
310           key = android::bluetooth::CodePathCounterKeyEnum::
311               LE_AUDIO_ALLOWLIST_DEVICE_HEALTH_STATUS_GOOD;
312           break;
313         case LeAudioHealthDeviceStatType::INVALID_DB:
314           key = android::bluetooth::CodePathCounterKeyEnum::
315               LE_AUDIO_ALLOWLIST_DEVICE_HEALTH_STATUS_BAD_INVALID_DB;
316           break;
317         case LeAudioHealthDeviceStatType::INVALID_CSIS:
318           key = android::bluetooth::CodePathCounterKeyEnum::
319               LE_AUDIO_ALLOWLIST_DEVICE_HEALTH_STATUS_BAD_INVALID_CSIS;
320           break;
321         default:
322           log::error("Metric unhandled {}", type);
323           return;
324       }
325     } else {
326       switch (type) {
327         case LeAudioHealthDeviceStatType::VALID_DB:
328         case LeAudioHealthDeviceStatType::VALID_CSIS:
329           key = android::bluetooth::CodePathCounterKeyEnum::
330               LE_AUDIO_NONALLOWLIST_DEVICE_HEALTH_STATUS_GOOD;
331           break;
332         case LeAudioHealthDeviceStatType::INVALID_DB:
333           key = android::bluetooth::CodePathCounterKeyEnum::
334               LE_AUDIO_NONALLOWLIST_DEVICE_HEALTH_STATUS_BAD_INVALID_DB;
335           break;
336         case LeAudioHealthDeviceStatType::INVALID_CSIS:
337           key = android::bluetooth::CodePathCounterKeyEnum::
338               LE_AUDIO_NONALLOWLIST_DEVICE_HEALTH_STATUS_BAD_INVALID_CSIS;
339           break;
340         default:
341           log::error("Metric unhandled {}", type);
342           return;
343       }
344     }
345     bluetooth::shim::CountCounterMetrics(key, 1);
346   }
347 
log_counter_metrics_for_group(LeAudioHealthGroupStatType type,bool in_allowlist)348   void log_counter_metrics_for_group(LeAudioHealthGroupStatType type,
349                                      bool in_allowlist) {
350     log::debug("in_allowlist: {}, type: {}", in_allowlist, ToString(type));
351     android::bluetooth::CodePathCounterKeyEnum key;
352     if (in_allowlist) {
353       switch (type) {
354         case LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS:
355           key = android::bluetooth::CodePathCounterKeyEnum::
356               LE_AUDIO_ALLOWLIST_GROUP_HEALTH_STATUS_GOOD;
357           break;
358         case LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED:
359           key = android::bluetooth::CodePathCounterKeyEnum::
360               LE_AUDIO_ALLOWLIST_GROUP_HEALTH_STATUS_BAD_ONCE_CIS_FAILED;
361           break;
362         case LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED:
363           key = android::bluetooth::CodePathCounterKeyEnum::
364               LE_AUDIO_ALLOWLIST_GROUP_HEALTH_STATUS_BAD_ONCE_SIGNALING_FAILED;
365           break;
366         default:
367           log::error("Metric unhandled {}", type);
368           return;
369       }
370     } else {
371       switch (type) {
372         case LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS:
373           key = android::bluetooth::CodePathCounterKeyEnum::
374               LE_AUDIO_NONALLOWLIST_GROUP_HEALTH_STATUS_GOOD;
375           break;
376         case LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED:
377           key = android::bluetooth::CodePathCounterKeyEnum::
378               LE_AUDIO_NONALLOWLIST_GROUP_HEALTH_STATUS_BAD_ONCE_CIS_FAILED;
379           break;
380         case LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED:
381           key = android::bluetooth::CodePathCounterKeyEnum::
382               LE_AUDIO_NONALLOWLIST_GROUP_HEALTH_STATUS_BAD_ONCE_SIGNALING_FAILED;
383           break;
384         default:
385           log::error("Metric unhandled {}", type);
386           return;
387       }
388     }
389     bluetooth::shim::CountCounterMetrics(key, 1);
390   }
391 };
392 }  // namespace bluetooth::le_audio
393 
Get(void)394 LeAudioHealthStatus* LeAudioHealthStatus::Get(void) {
395   if (instance) {
396     return instance;
397   }
398   instance = new LeAudioHealthStatusImpl();
399   return instance;
400 }
401 
DebugDump(int fd)402 void LeAudioHealthStatus::DebugDump(int fd) {
403   if (instance) {
404     instance->Dump(fd);
405   }
406 }
407 
Cleanup(void)408 void LeAudioHealthStatus::Cleanup(void) {
409   if (!instance) {
410     return;
411   }
412   auto ptr = instance;
413   instance = nullptr;
414   delete ptr;
415 }
416