Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f5f419ea authored by Łukasz Rymanowski's avatar Łukasz Rymanowski
Browse files

LeAudioHealthStatus: Initial Implementation

Bug: 279233743
Test: atest le_audio_health_status_test
Test: atest BluetoothInstrumentationTests
Tag: #feature

Change-Id: I78d0a1dfd04a32bad616c22e56236f33e435f88c
parent 30eb9ea3
Loading
Loading
Loading
Loading
+47 −0
Original line number Diff line number Diff line
@@ -108,6 +108,7 @@ cc_library_static {
        "le_audio/content_control_id_keeper.cc",
        "le_audio/devices.cc",
        "le_audio/hal_verifier.cc",
        "le_audio/le_audio_health_status.cc",
        "le_audio/le_audio_log_history.cc",
        "le_audio/le_audio_set_configuration_provider.cc",
        "le_audio/le_audio_set_configuration_provider_json.cc",
@@ -667,6 +668,7 @@ cc_test {
        "le_audio/content_control_id_keeper_test.cc",
        "le_audio/devices.cc",
        "le_audio/devices_test.cc",
        "le_audio/le_audio_health_status.cc",
        "le_audio/le_audio_log_history.cc",
        "le_audio/le_audio_set_configuration_provider_json.cc",
        "le_audio/le_audio_types.cc",
@@ -743,6 +745,7 @@ cc_test {
        "le_audio/content_control_id_keeper.cc",
        "le_audio/devices.cc",
        "le_audio/le_audio_client_test.cc",
        "le_audio/le_audio_health_status.cc",
        "le_audio/le_audio_log_history.cc",
        "le_audio/le_audio_set_configuration_provider_json.cc",
        "le_audio/le_audio_types.cc",
@@ -810,6 +813,50 @@ cc_test {
    },
}

// health status unit tests for host
cc_test {
    name: "bluetooth_le_audio_health_status_test",
    test_suites: ["device-tests"],
    defaults: [
        "bluetooth_gtest_x86_asan_workaround",
        "fluoride_bta_defaults",
        "mts_defaults",
    ],
    host_supported: true,
    include_dirs: [
        "packages/modules/Bluetooth/system",
        "packages/modules/Bluetooth/system/bta/include",
    ],
    srcs: [
        ":TestCommonMockFunctions",
        //":TestMockBtif",
        "le_audio/le_audio_health_status.cc",
        "le_audio/le_audio_health_status_test.cc",
    ],
    shared_libs: [
        "libcrypto",
        "liblog",
    ],
    static_libs: [
        "crypto_toolbox_for_tests",
        "libbt-common",
        "libbt-protos-lite",
        "libchrome",
        "libgmock",
        "libosi",
    ],
    sanitize: {
        cfi: true,
        scs: true,
        address: true,
        all_undefined: true,
        integer_overflow: true,
        diag: {
            undefined: true,
        },
    },
}

cc_test {
    name: "bluetooth_test_broadcaster_state_machine",
    test_suites: ["device-tests"],
+287 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "le_audio_health_status.h"

#include <vector>

#include "bta/include/bta_groups.h"
#include "gd/common/strings.h"
#include "osi/include/log.h"

using bluetooth::common::ToString;
using bluetooth::groups::kGroupUnknown;
using le_audio::LeAudioHealthStatus;
using le_audio::LeAudioRecommendationActionCb;

namespace le_audio {
class LeAudioHealthStatusImpl;
LeAudioHealthStatusImpl* instance;

class LeAudioHealthStatusImpl : public LeAudioHealthStatus {
 public:
  LeAudioHealthStatusImpl(void) { LOG_DEBUG(" Initiated"); }

  ~LeAudioHealthStatusImpl(void) { clear_module(); }

  void RegisterCallback(LeAudioRecommendationActionCb cb) override {
    register_callback(std::move(cb));
  }

  void RemoveStatistics(const RawAddress& address, int group_id) override {
    LOG_DEBUG("%s, group_id: %d", ADDRESS_TO_LOGGABLE_CSTR(address), group_id);
    remove_device(address);
    remove_group(group_id);
  }

  void AddStatisticForDevice(const RawAddress& address,
                             LeAudioHealthDeviceStatType type) override {
    LOG_DEBUG("%s, %s", ADDRESS_TO_LOGGABLE_CSTR(address),
              ToString(type).c_str());

    auto dev = find_device(address);
    if (dev == nullptr) {
      add_device(address);
      dev = find_device(address);
      if (dev == nullptr) {
        LOG_ERROR("Could not add device %s", ADDRESS_TO_LOGGABLE_CSTR(address));
        return;
      }
    }

    LeAudioHealthBasedAction action;
    switch (type) {
      case LeAudioHealthDeviceStatType::VALID_DB:
        dev->is_valid_service_ = true;
        action = LeAudioHealthBasedAction::NONE;
        break;
      case LeAudioHealthDeviceStatType::INVALID_DB:
        dev->is_valid_service_ = false;
        action = LeAudioHealthBasedAction::DISABLE;
        break;
      case LeAudioHealthDeviceStatType::INVALID_CSIS:
        dev->is_valid_group_member_ = false;
        action = LeAudioHealthBasedAction::DISABLE;
        break;
      case LeAudioHealthDeviceStatType::VALID_CSIS:
        dev->is_valid_group_member_ = true;
        action = LeAudioHealthBasedAction::NONE;
        break;
    }

    if (dev->latest_recommendation_ != action) {
      dev->latest_recommendation_ = action;
      send_recommendation_for_device(address, action);
      return;
    }
  }

  void AddStatisticForGroup(int group_id,
                            LeAudioHealthGroupStatType type) override {
    LOG_DEBUG("group_id: %d, %s", group_id, ToString(type).c_str());

    auto group = find_group(group_id);
    if (group == nullptr) {
      add_group(group_id);
      group = find_group(group_id);
      if (group == nullptr) {
        LOG_ERROR("Could not add group %d", group_id);
        return;
      }
    }

    switch (type) {
      case LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS:
        group->stream_success_cnt_++;
        if (group->latest_recommendation_ == LeAudioHealthBasedAction::NONE) {
          return;
        }
        break;
      case LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED:
        group->stream_cis_failures_cnt_++;
        group->stream_failures_cnt_++;
        break;
      case LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED:
        group->stream_signaling_failures_cnt_++;
        group->stream_failures_cnt_++;
        break;
    }

    LeAudioHealthBasedAction action = LeAudioHealthBasedAction::NONE;
    if (group->stream_success_cnt_ == 0) {
      /* Never succeed in stream creation */
      if ((group->stream_failures_cnt_ >=
           MAX_ALLOWED_FAILURES_IN_A_ROW_WITHOUT_SUCCESS)) {
        action = LeAudioHealthBasedAction::DISABLE;
      }
    } else {
      /* Had some success before */
      if ((100 * group->stream_failures_cnt_ / group->stream_success_cnt_) >=
          THRESHOLD_FOR_DISABLE_CONSIDERATION) {
        action = LeAudioHealthBasedAction::CONSIDER_DISABLING;
      }
    }

    if (group->latest_recommendation_ != action) {
      group->latest_recommendation_ = action;
      send_recommendation_for_group(group_id, action);
    }
  }

  void Dump(int fd) {
    dprintf(fd, "  LeAudioHealthStats: \n    groups:");
    for (const auto& g : group_stats_) {
      dumpsys_group(fd, g);
    }
    dprintf(fd, "\n    devices: ");
    for (const auto& dev : devices_stats_) {
      dumpsys_dev(fd, dev);
    }
    dprintf(fd, "\n");
  }

 private:
  static constexpr int MAX_ALLOWED_FAILURES_IN_A_ROW_WITHOUT_SUCCESS = 3;
  static constexpr int THRESHOLD_FOR_DISABLE_CONSIDERATION = 70;

  std::vector<LeAudioRecommendationActionCb> callbacks_;
  std::vector<device_stats> devices_stats_;
  std::vector<group_stats> group_stats_;

  void dumpsys_group(int fd, const group_stats& group) {
    std::stringstream stream;

    stream << "\n group_id: " << group.group_id_ << ": "
           << group.latest_recommendation_
           << ", success: " << group.stream_success_cnt_
           << ", fail total: " << group.stream_failures_cnt_
           << ", fail cis: " << group.stream_cis_failures_cnt_
           << ", fail signaling: " << group.stream_signaling_failures_cnt_;

    dprintf(fd, "%s", stream.str().c_str());
  }

  void dumpsys_dev(int fd, const device_stats& dev) {
    std::stringstream stream;

    stream << "\n " << ADDRESS_TO_LOGGABLE_STR(dev.address_) << ": "
           << dev.latest_recommendation_
           << (dev.is_valid_service_ ? " service: OK" : " service : NOK")
           << (dev.is_valid_group_member_ ? " csis: OK" : " csis : NOK");

    dprintf(fd, "%s", stream.str().c_str());
  }

  void clear_module(void) {
    devices_stats_.clear();
    group_stats_.clear();
    callbacks_.clear();
  }

  void send_recommendation_for_device(const RawAddress& address,
                                      LeAudioHealthBasedAction recommendation) {
    LOG_DEBUG("%s, %s", ADDRESS_TO_LOGGABLE_CSTR(address),
              ToString(recommendation).c_str());
    /* Notify new user about known groups */
    for (auto& cb : callbacks_) {
      cb.Run(address, kGroupUnknown, recommendation);
    }
  }

  void send_recommendation_for_group(
      int group_id, const LeAudioHealthBasedAction recommendation) {
    LOG_DEBUG("group_id: %d, %s", group_id, ToString(recommendation).c_str());
    /* Notify new user about known groups */
    for (auto& cb : callbacks_) {
      cb.Run(RawAddress::kEmpty, group_id, recommendation);
    }
  }

  void add_device(const RawAddress& address) {
    devices_stats_.emplace_back(device_stats(address));
  }

  void add_group(int group_id) {
    group_stats_.emplace_back(group_stats(group_id));
  }

  void remove_group(int group_id) {
    if (group_id == kGroupUnknown) {
      return;
    }
    auto iter = std::find_if(
        group_stats_.begin(), group_stats_.end(),
        [group_id](const auto& g) { return g.group_id_ == group_id; });
    if (iter != group_stats_.end()) {
      group_stats_.erase(iter);
    }
  }

  void remove_device(const RawAddress& address) {
    auto iter = std::find_if(
        devices_stats_.begin(), devices_stats_.end(),
        [address](const auto& d) { return d.address_ == address; });
    if (iter != devices_stats_.end()) {
      devices_stats_.erase(iter);
    }
  }

  void register_callback(LeAudioRecommendationActionCb cb) {
    callbacks_.push_back(std::move(cb));
  }

  device_stats* find_device(const RawAddress& address) {
    auto iter = std::find_if(
        devices_stats_.begin(), devices_stats_.end(),
        [address](const auto& d) { return d.address_ == address; });
    if (iter == devices_stats_.end()) return nullptr;

    return &(*iter);
  }

  group_stats* find_group(int group_id) {
    auto iter = std::find_if(
        group_stats_.begin(), group_stats_.end(),
        [group_id](const auto& g) { return g.group_id_ == group_id; });
    if (iter == group_stats_.end()) return nullptr;

    return &(*iter);
  }
};
}  // namespace le_audio

LeAudioHealthStatus* LeAudioHealthStatus::Get(void) {
  if (instance) {
    return instance;
  }
  instance = new LeAudioHealthStatusImpl();
  return instance;
}

void LeAudioHealthStatus::DebugDump(int fd) {
  if (instance) {
    instance->Dump(fd);
  }
}

void LeAudioHealthStatus::Cleanup(void) {
  if (!instance) {
    return;
  }
  auto ptr = instance;
  instance = nullptr;
  delete ptr;
}
+151 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <base/functional/callback.h>

#include <ostream>

#include "hardware/bt_le_audio.h"
#include "types/raw_address.h"

using bluetooth::le_audio::LeAudioHealthBasedAction;

namespace le_audio {
using LeAudioRecommendationActionCb = base::RepeatingCallback<void(
    const RawAddress& address, int group_id, LeAudioHealthBasedAction action)>;

/* This should be set by the client of this module to provide information about
 * basic LeAudio support of the device which is exposing ASCS UUIDs. Should be
 * used with AddStatisticForDevice API
 */
enum class LeAudioHealthDeviceStatType {
  /* Should be used whenever LeAudio device has invalid GATT Database structure.
   * e.g. missing mandatory services or characteristics. */
  INVALID_DB = 0,
  /* Should be used when LeAudio devie GATT DB contains at least mandatory
   * services and characteristics. */
  VALID_DB,
  /* Should be used when device expose CSIS support but service is not valid. */
  INVALID_CSIS,
  /* Should be used when device expose CSIS and Group ID has been
   * successfully assigned to device. */
  VALID_CSIS,
};

/* When LeAudio device (s) are ready to use, we look at those as a group.
 * Using Group stats we measure how good we are in creating streams.
 * Should be used with AddStatisticForGroup API
 */
enum class LeAudioHealthGroupStatType {
  /* Whenever stream is successfully established. */
  STREAM_CREATE_SUCCESS,
  /* Whenever stream creation failes due to CIS failures */
  STREAM_CREATE_CIS_FAILED,
  /* Whenever stream creation failes due to ASCS signaling failures
   * e.g. ASE does not go to the proper State on time
   */
  STREAM_CREATE_SIGNALING_FAILED,
};

class LeAudioHealthStatus {
 public:
  virtual ~LeAudioHealthStatus(void) = default;
  static LeAudioHealthStatus* Get(void);
  static void Cleanup(void);
  static void DebugDump(int fd);

  virtual void RegisterCallback(LeAudioRecommendationActionCb cb) = 0;
  virtual void AddStatisticForDevice(const RawAddress& address,
                                     LeAudioHealthDeviceStatType type) = 0;
  virtual void AddStatisticForGroup(int group_id,
                                    LeAudioHealthGroupStatType type) = 0;
  virtual void RemoveStatistics(const RawAddress& address, int group) = 0;

  struct group_stats {
    group_stats(int group_id)
        : group_id_(group_id),
          latest_recommendation_(LeAudioHealthBasedAction::NONE),
          stream_success_cnt_(0),
          stream_failures_cnt_(0),
          stream_cis_failures_cnt_(0),
          stream_signaling_failures_cnt_(0){};

    int group_id_;
    LeAudioHealthBasedAction latest_recommendation_;

    int stream_success_cnt_;
    int stream_failures_cnt_;
    int stream_cis_failures_cnt_;
    int stream_signaling_failures_cnt_;
  };

  struct device_stats {
    device_stats(RawAddress address)
        : address_(address),
          latest_recommendation_(LeAudioHealthBasedAction::NONE),
          is_valid_service_(true),
          is_valid_group_member_(true){};
    RawAddress address_;
    LeAudioHealthBasedAction latest_recommendation_;

    bool is_valid_service_;
    bool is_valid_group_member_;
  };
};

inline std::ostream& operator<<(
    std::ostream& os, const le_audio::LeAudioHealthGroupStatType& stat) {
  switch (stat) {
    case le_audio::LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS:
      os << "STREAM_CREATE_SUCCESS";
      break;
    case le_audio::LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED:
      os << "STREAM_CREATE_CIS_FAILED";
      break;
    case le_audio::LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED:
      os << "STREAM_CREATE_SIGNALING_FAILED";
      break;
    default:
      os << "UNKNOWN";
      break;
  }
  return os;
}

inline std::ostream& operator<<(
    std::ostream& os, const le_audio::LeAudioHealthDeviceStatType& stat) {
  switch (stat) {
    case le_audio::LeAudioHealthDeviceStatType::INVALID_DB:
      os << "INVALID_DB";
      break;
    case le_audio::LeAudioHealthDeviceStatType::VALID_DB:
      os << "VALID_DB";
      break;
    case le_audio::LeAudioHealthDeviceStatType::INVALID_CSIS:
      os << "INVALID_CSIS";
      break;
    case le_audio::LeAudioHealthDeviceStatType::VALID_CSIS:
      os << "VALID_CSIS";
      break;
    default:
      os << "UNKNOWN";
      break;
  }
  return os;
}
}  // namespace le_audio
 No newline at end of file
+197 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "le_audio_health_status.h"

#include <base/functional/callback.h>
#include <base/functional/callback_forward.h>
#include <base/functional/callback_helpers.h>
#include <base/logging.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include "bta/include/bta_groups.h"
#include "gd/common/init_flags.h"
#include "test/common/mock_functions.h"
#include "types/bluetooth/uuid.h"
#include "types/raw_address.h"

using bluetooth::groups::kGroupUnknown;
using bluetooth::le_audio::LeAudioHealthBasedAction;
using le_audio::LeAudioHealthDeviceStatType;
using le_audio::LeAudioHealthGroupStatType;
using le_audio::LeAudioHealthStatus;

const char* test_flags[] = {
    "INIT_logging_debug_enabled_for_all=true",
    nullptr,
};

LeAudioHealthBasedAction recommentation_in_callback =
    LeAudioHealthBasedAction::NONE;
RawAddress address_in_callback = RawAddress::kEmpty;
int group_id_in_callback = kGroupUnknown;

static void healthCallback(const RawAddress& address, int group_id,
                           LeAudioHealthBasedAction recommendation) {
  address_in_callback = address;
  group_id_in_callback = group_id;
  recommentation_in_callback = recommendation;
}

class LeAudioHealthStatusTest : public ::testing::Test {
 protected:
  void SetUp() override {
    reset_mock_function_count_map();

    bluetooth::common::InitFlags::Load(test_flags);
    le_audio_health_status_instance_ = LeAudioHealthStatus::Get();
    le_audio_health_status_instance_->RegisterCallback(
        base::BindRepeating(healthCallback));
  }

  void TearDown() override {
    le_audio_health_status_instance_->Cleanup();
    recommentation_in_callback = LeAudioHealthBasedAction::NONE;
    address_in_callback = RawAddress::kEmpty;
  }

  LeAudioHealthStatus* le_audio_health_status_instance_;
};

RawAddress GetTestAddress(uint8_t index) {
  CHECK_LT(index, UINT8_MAX);
  RawAddress result = {{0xC0, 0xDE, 0xC0, 0xDE, 0x00, index}};
  return result;
}

TEST_F(LeAudioHealthStatusTest, test_initialize) {
  ASSERT_TRUE(le_audio_health_status_instance_ != nullptr);
}

TEST_F(LeAudioHealthStatusTest, test_invalid_db) {
  const RawAddress test_address0 = GetTestAddress(0);
  le_audio_health_status_instance_->AddStatisticForDevice(
      test_address0, LeAudioHealthDeviceStatType::INVALID_DB);
  ASSERT_TRUE(address_in_callback == test_address0);
  ASSERT_TRUE(recommentation_in_callback == LeAudioHealthBasedAction::DISABLE);
}

TEST_F(LeAudioHealthStatusTest, test_invalid_csis_member) {
  const RawAddress test_address0 = GetTestAddress(0);
  le_audio_health_status_instance_->AddStatisticForDevice(
      test_address0, LeAudioHealthDeviceStatType::INVALID_CSIS);
  ASSERT_TRUE(address_in_callback == test_address0);
  ASSERT_TRUE(recommentation_in_callback == LeAudioHealthBasedAction::DISABLE);
}

TEST_F(LeAudioHealthStatusTest, test_remove_statistic) {
  const RawAddress test_address0 = GetTestAddress(0);
  int group_id = 1;

  le_audio_health_status_instance_->AddStatisticForDevice(
      test_address0, LeAudioHealthDeviceStatType::INVALID_CSIS);
  le_audio_health_status_instance_->AddStatisticForGroup(
      group_id, LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS);
  le_audio_health_status_instance_->RemoveStatistics(test_address0, group_id);
}

TEST_F(LeAudioHealthStatusTest, test_all_is_good) {
  int group_id = 1;

  for (int i = 0; i < 100; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS);
  }

  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);
  ASSERT_TRUE(group_id_in_callback == kGroupUnknown);
}

TEST_F(LeAudioHealthStatusTest, test_disable_cis_no_stream_creation) {
  int group_id = 1;

  for (int i = 0; i < 3; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED);
  }
  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);
  ASSERT_TRUE(group_id_in_callback == group_id);
  ;
  ASSERT_TRUE(recommentation_in_callback == LeAudioHealthBasedAction::DISABLE);
}

TEST_F(LeAudioHealthStatusTest, test_disable_signaling_no_stream_creation) {
  int group_id = 1;

  for (int i = 0; i < 3; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED);
  }
  /* No recommendation shall be sent */
  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);
  ASSERT_TRUE(group_id_in_callback == group_id);
  ASSERT_TRUE(recommentation_in_callback == LeAudioHealthBasedAction::DISABLE);
}

TEST_F(LeAudioHealthStatusTest, test_disable_signaling_cis_no_stream_creation) {
  int group_id = 1;

  for (int i = 0; i < 2; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED);
  }
  le_audio_health_status_instance_->AddStatisticForGroup(
      group_id, LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED);

  /* No recommendation shall be sent */
  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);
  ASSERT_TRUE(group_id_in_callback == group_id);
  ASSERT_TRUE(recommentation_in_callback == LeAudioHealthBasedAction::DISABLE);
}

TEST_F(LeAudioHealthStatusTest, test_consider_disabling) {
  int group_id = 1;

  for (int i = 0; i < 10; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_SUCCESS);
  }

  for (int i = 0; i < 2; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED);
  }

  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);

  for (int i = 0; i < 2; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_SIGNALING_FAILED);
  }

  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);

  for (int i = 0; i < 3; i++) {
    le_audio_health_status_instance_->AddStatisticForGroup(
        group_id, LeAudioHealthGroupStatType::STREAM_CREATE_CIS_FAILED);
  }

  ASSERT_TRUE(address_in_callback == RawAddress::kEmpty);
  ASSERT_TRUE(group_id_in_callback == group_id);
  ASSERT_TRUE(recommentation_in_callback ==
              LeAudioHealthBasedAction::CONSIDER_DISABLING);
}
 No newline at end of file
+26 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@
#include <array>
#include <map>
#include <optional>
#include <ostream>
#include <vector>

#include "raw_address.h"
@@ -27,6 +28,31 @@
namespace bluetooth {
namespace le_audio {

enum class LeAudioHealthBasedAction {
  NONE = 0,
  DISABLE,
  CONSIDER_DISABLING,
};

inline std::ostream& operator<<(std::ostream& os,
                                const LeAudioHealthBasedAction action) {
  switch (action) {
    case LeAudioHealthBasedAction::NONE:
      os << "NONE";
      break;
    case LeAudioHealthBasedAction::DISABLE:
      os << "DISABLE";
      break;
    case LeAudioHealthBasedAction::CONSIDER_DISABLING:
      os << "CONSIDER_DISABLING";
      break;
    default:
      os << "UNKNOWN";
      break;
  }
  return os;
}

enum class ConnectionState {
  DISCONNECTED = 0,
  CONNECTING,