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

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

leaudio: Improve rejecting stream from remote device

If during stream configuration, remote device rejects any of the
configuration stream, instead of disconnecting link as a result of not
moving forward in the ASE state machine, Android will just drop creating
stream and notify audio framework about that

Bug: 294943711
Test: atest bluetooth_le_audio_test
Tag: #feature
Change-Id: I10b13448a8d43da8f13ced6ffa0b79dfc704ddc6
parent bd3429bb
Loading
Loading
Loading
Loading
+1 −33
Original line number Diff line number Diff line
@@ -101,12 +101,6 @@ using le_audio::types::LeAudioContextType;
using le_audio::utils::GetAudioContextsFromSinkMetadata;
using le_audio::utils::GetAudioContextsFromSourceMetadata;

using le_audio::client_parser::ascs::
    kCtpResponseCodeInvalidConfigurationParameterValue;
using le_audio::client_parser::ascs::kCtpResponseCodeSuccess;
using le_audio::client_parser::ascs::kCtpResponseInvalidAseCisMapping;
using le_audio::client_parser::ascs::kCtpResponseNoReason;

/* Enums */
enum class AudioReconfigurationResult {
  RECONFIGURATION_NEEDED = 0x00,
@@ -639,28 +633,6 @@ class LeAudioClientImpl : public LeAudioClient {
    }
  }

  void ControlPointNotificationHandler(
      struct le_audio::client_parser::ascs::ctp_ntf& ntf) {
    for (auto& entry : ntf.entries) {
      switch (entry.response_code) {
        case kCtpResponseCodeInvalidConfigurationParameterValue:
          switch (entry.reason) {
            case kCtpResponseInvalidAseCisMapping:
              CancelStreamingRequest();
              break;
            case kCtpResponseNoReason:
            default:
              break;
          }
          break;
        case kCtpResponseCodeSuccess:
          FALLTHROUGH;
        default:
          break;
      }
    }
  }

  void group_add_node(const int group_id, const RawAddress& address,
                      bool update_group_module = false) {
    LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
@@ -1849,11 +1821,7 @@ class LeAudioClientImpl : public LeAudioClient {
            supp_audio_contexts.source.value());
      }
    } else if (hdl == leAudioDevice->ctp_hdls_.val_hdl) {
      auto ntf =
          std::make_unique<struct le_audio::client_parser::ascs::ctp_ntf>();

      if (ParseAseCtpNotification(*ntf, len, value))
        ControlPointNotificationHandler(*ntf);
      groupStateMachine_->ProcessGattCtpNotification(group, value, len);
    } else if (hdl == leAudioDevice->tmap_role_hdl_) {
      le_audio::client_parser::tmap::ParseTmapRole(leAudioDevice->tmap_role_,
                                                   len, value);
+4 −0
Original line number Diff line number Diff line
@@ -54,6 +54,10 @@ class MockLeAudioGroupStateMachine : public le_audio::LeAudioGroupStateMachine {
               le_audio::LeAudioDeviceGroup* group),
              (override));

  MOCK_METHOD((void), ProcessGattCtpNotification,
              (le_audio::LeAudioDeviceGroup * group, uint8_t* value,
               uint16_t len),
              (override));
  MOCK_METHOD((void), ProcessHciNotifOnCigCreate,
              (le_audio::LeAudioDeviceGroup * group, uint8_t status,
               uint8_t cig_id, std::vector<uint16_t> conn_handles),
+58 −0
Original line number Diff line number Diff line
@@ -302,6 +302,64 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
    state_machine_callbacks_->StatusReportCb(group->group_id_, status);
  }

  void ProcessGattCtpNotification(LeAudioDeviceGroup* group, uint8_t* value,
                                  uint16_t len) {
    auto ntf =
        std::make_unique<struct le_audio::client_parser::ascs::ctp_ntf>();

    bool valid_notification = ParseAseCtpNotification(*ntf, len, value);
    if (group == nullptr) {
      LOG_WARN("Notification received to invalid group");
      return;
    }

    /* State machine looks on ASE state and base on it take decisions.
     * If ASE state is not achieve on time, timeout is reported and upper
     * layer mostlikely drops ACL considers that remote is in bad state.
     * However, it might happen that remote device rejects ASE configuration for
     * some reason and ASCS specification defines tones of different reasons.
     * Maybe in the future we will be able to handle all of them but for now it
     * seems to be important to allow remote device to reject ASE configuration
     * when stream is creating. e.g. Allow remote to reject Enable on unwanted
     * context type.
     */

    auto target_state = group->GetTargetState();
    auto in_transition = group->IsInTransition();
    if (!in_transition ||
        target_state != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
      LOG_DEBUG(
          "Not interested in ctp result for group %d inTransistion: %d , "
          "targetState: %s",
          group->group_id_, in_transition, ToString(target_state).c_str());
      return;
    }

    if (!valid_notification) {
      /* Do nothing, just allow guard timer to fire */
      LOG_ERROR("Invalid CTP notification for group %d", group->group_id_);
      return;
    }

    for (auto& entry : ntf->entries) {
      if (entry.response_code !=
          le_audio::client_parser::ascs::kCtpResponseCodeSuccess) {
        /* Gracefully stop the stream */
        LOG_ERROR(
            "Stoping stream due to control point error for ase: %d, error: "
            "0x%02x, reason: 0x%02x",
            entry.ase_id, entry.response_code, entry.reason);
        StopStream(group);
        return;
      }
    }

    LOG_DEBUG(
        "Ctp result OK for group %d inTransistion: %d , "
        "targetState: %s",
        group->group_id_, in_transition, ToString(target_state).c_str());
  }

  void ProcessGattNotifEvent(uint8_t* value, uint16_t len, struct ase* ase,
                             LeAudioDevice* leAudioDevice,
                             LeAudioDeviceGroup* group) override {
+2 −0
Original line number Diff line number Diff line
@@ -62,6 +62,8 @@ class LeAudioGroupStateMachine {
      types::BidirectionalPair<std::vector<uint8_t>> ccid_lists = {
          .sink = {}, .source = {}}) = 0;
  virtual void StopStream(LeAudioDeviceGroup* group) = 0;
  virtual void ProcessGattCtpNotification(LeAudioDeviceGroup* group,
                                          uint8_t* value, uint16_t len) = 0;
  virtual void ProcessGattNotifEvent(uint8_t* value, uint16_t len,
                                     struct types::ase* ase,
                                     LeAudioDevice* leAudioDevice,
+108 −0
Original line number Diff line number Diff line
@@ -997,6 +997,49 @@ class StateMachineTest : public Test {
        };
  }

  void PrepareCtpNotificationError(LeAudioDeviceGroup* group, uint8_t opcode,
                                   uint8_t response_code, uint8_t reason) {
    ase_ctp_handlers[opcode] = [group, opcode, response_code, reason](
                                   LeAudioDevice* device,
                                   std::vector<uint8_t> value,
                                   GATT_WRITE_OP_CB cb, void* cb_data) {
      auto num_ase = value[1];
      std::vector<uint8_t> notif_value(
          2 + num_ase * sizeof(struct client_parser::ascs::ctp_ase_entry));
      auto* p = notif_value.data();

      UINT8_TO_STREAM(p, opcode);
      UINT8_TO_STREAM(p, num_ase);

      auto* ase_p = &value[2];
      for (auto i = 0u; i < num_ase; ++i) {
        /* Check if this is a valid ASE ID  */
        auto ase_id = *ase_p++;
        auto it =
            std::find_if(device->ases_.begin(), device->ases_.end(),
                         [ase_id](auto& ase) { return (ase.id == ase_id); });
        ASSERT_NE(it, device->ases_.end());

        auto meta_len = *ase_p++;
        auto num_handled_bytes = ase_p - value.data();
        ase_p += meta_len;

        client_parser::ascs::ase_transient_state_params enable_params = {
            .metadata = std::vector<uint8_t>(
                value.begin() + num_handled_bytes,
                value.begin() + num_handled_bytes + meta_len)};

        // Inject error response
        UINT8_TO_STREAM(p, ase_id);
        UINT8_TO_STREAM(p, response_code);
        UINT8_TO_STREAM(p, reason);
      }

      LeAudioGroupStateMachine::Get()->ProcessGattCtpNotification(
          group, notif_value.data(), notif_value.size());
    };
  }

  void PrepareEnableHandler(LeAudioDeviceGroup* group, int verify_ase_count = 0,
                            bool inject_enabling = true) {
    ase_ctp_handlers[ascs::kAseCtpOpcodeEnable] =
@@ -1479,6 +1522,71 @@ TEST_F(StateMachineTest, testConfigureQosMultiple) {
  ASSERT_EQ(0, get_func_call_count("alarm_cancel"));
}

TEST_F(StateMachineTest, testStreamCreationError) {
  /* Device is banded headphones with 1x snk + 0x src ase
   * (1xunidirectional CIS) with channel count 2 (for stereo
   */
  const auto context_type = kContextTypeRingtone;
  const int leaudio_group_id = 4;
  channel_count_ = kLeAudioCodecLC3ChannelCountSingleChannel |
                   kLeAudioCodecLC3ChannelCountTwoChannel;

  // Prepare fake connected device group
  auto* group = PrepareSingleTestDeviceGroup(leaudio_group_id, context_type);

  /* Ringtone with channel count 1 for single device and 1 ASE sink will
   * end up with 1 Sink ASE being configured.
   */
  PrepareConfigureCodecHandler(group, 1);
  PrepareConfigureQosHandler(group, 1);
  PrepareCtpNotificationError(
      group, ascs::kAseCtpOpcodeEnable,
      client_parser::ascs::kCtpResponseCodeUnspecifiedError,
      client_parser::ascs::kCtpResponseNoReason);
  PrepareReleaseHandler(group);

  auto* leAudioDevice = group->GetFirstDevice();

  /*
   * 1 - Configure ASE
   * 2 - QoS ASE
   * 3 - Enable ASE
   * 4 - Release ASE
   */
  EXPECT_CALL(gatt_queue,
              WriteCharacteristic(1, leAudioDevice->ctp_hdls_.val_hdl, _,
                                  GATT_WRITE_NO_RSP, _, _))
      .Times(4);

  EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0);
  EXPECT_CALL(*mock_iso_manager_, RemoveIsoDataPath(_, _)).Times(0);
  EXPECT_CALL(*mock_iso_manager_, DisconnectCis(_, _)).Times(0);
  EXPECT_CALL(*mock_iso_manager_, RemoveCig(_, _)).Times(1);

  InjectInitialIdleNotification(group);

  // Validate GroupStreamStatus
  EXPECT_CALL(
      mock_callbacks_,
      StatusReportCb(leaudio_group_id,
                     bluetooth::le_audio::GroupStreamStatus::RELEASING));
  EXPECT_CALL(mock_callbacks_,
              StatusReportCb(leaudio_group_id,
                             bluetooth::le_audio::GroupStreamStatus::IDLE));

  // Start the configuration and stream Media content
  ASSERT_TRUE(LeAudioGroupStateMachine::Get()->StartStream(
      group, context_type,
      {.sink = types::AudioContexts(context_type),
       .source = types::AudioContexts(context_type)}));

  // Check if group has transitioned to a proper state
  ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
  ASSERT_EQ(2, get_func_call_count("alarm_cancel"));
}

TEST_F(StateMachineTest, testStreamSingle) {
  /* Device is banded headphones with 1x snk + 0x src ase
   * (1xunidirectional CIS) with channel count 2 (for stereo