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

Commit 0938d11a authored by Łukasz Rymanowski's avatar Łukasz Rymanowski
Browse files

leaudio: Fix race between ASE state and CIS disconnection

ASE shall not move to Codec Configured state or Idle state back from
Streaming if CIS are connected.
However, it might happen, that ASE notification arrives faster then CIS
Disconnection Complete event, which could lead to break LeAudio state machine.
With this patch, state machine is checking CIS state, and if not yet
disconnected, the upper layer is not notified about new state until
CISes are disconnected

Bug: 255291874
Test: atest bluetooth_le_audio_test
Test: atest BluetoothInstrumentationTests
Tag: #feature
Merged-In: I577c1db4c84296a600f2c3dc168985a5498a3712
Change-Id: I577c1db4c84296a600f2c3dc168985a5498a3712
(cherry picked from commit 9ddba66b)
parent 4602def1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -460,6 +460,8 @@ class LeAudioClientImpl : public LeAudioClient {
        ToString(group->GetTargetState()).c_str());
    group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);

    group->PrintDebugState();

    /* There is an issue with a setting up stream or any other operation which
     * are gatt operations. It means peer is not responsable. Lets close ACL
     */
+15 −17
Original line number Diff line number Diff line
@@ -882,16 +882,12 @@ bool LeAudioDeviceGroup::IsGroupStreamReady(void) {
  return iter == leAudioDevices_.end();
}

bool LeAudioDeviceGroup::HaveAllActiveDevicesCisDisc(void) {
  auto iter =
      std::find_if(leAudioDevices_.begin(), leAudioDevices_.end(), [](auto& d) {
        if (d.expired())
          return false;
        else
          return !(((d.lock()).get())->HaveAllAsesCisDisc());
      });

  return iter == leAudioDevices_.end();
bool LeAudioDeviceGroup::HaveAllCisesDisconnected(void) {
  for (auto const dev : leAudioDevices_) {
    if (dev.expired()) continue;
    if (dev.lock().get()->HaveAnyCisConnected()) return false;
  }
  return true;
}

uint8_t LeAudioDeviceGroup::GetFirstFreeCisId(void) {
@@ -2400,13 +2396,15 @@ bool LeAudioDevice::HaveAllActiveAsesCisEst(void) {
  return iter == ases_.end();
}

bool LeAudioDevice::HaveAllAsesCisDisc(void) {
  auto iter = std::find_if(ases_.begin(), ases_.end(), [](const auto& ase) {
    return ase.active &&
           (ase.data_path_state != AudioStreamDataPathState::CIS_ASSIGNED);
  });

  return iter == ases_.end();
bool LeAudioDevice::HaveAnyCisConnected(void) {
  /* Pending and Disconnecting is considered as connected in this function */
  for (auto const ase : ases_) {
    if (ase.data_path_state != AudioStreamDataPathState::CIS_ASSIGNED &&
        ase.data_path_state != AudioStreamDataPathState::IDLE) {
      return true;
    }
  }
  return false;
}

bool LeAudioDevice::HasCisId(uint8_t id) {
+2 −2
Original line number Diff line number Diff line
@@ -155,7 +155,7 @@ class LeAudioDevice {
  bool IsReadyToCreateStream(void);
  bool IsReadyToSuspendStream(void);
  bool HaveAllActiveAsesCisEst(void);
  bool HaveAllAsesCisDisc(void);
  bool HaveAnyCisConnected(void);
  bool HasCisId(uint8_t id);
  uint8_t GetMatchingBidirectionCisId(const struct types::ase* base_ase);
  const struct types::acs_ac_record* GetCodecConfigurationSupportedPac(
@@ -286,7 +286,7 @@ class LeAudioDeviceGroup {
  bool IsDeviceInTheGroup(LeAudioDevice* leAudioDevice);
  bool HaveAllActiveDevicesAsesTheSameState(types::AseState state);
  bool IsGroupStreamReady(void);
  bool HaveAllActiveDevicesCisDisc(void);
  bool HaveAllCisesDisconnected(void);
  uint8_t GetFirstFreeCisId(void);
  uint8_t GetFirstFreeCisId(types::CisType cis_type);
  void CigGenerateCisIds(types::LeAudioContextType context_type);
+90 −14
Original line number Diff line number Diff line
@@ -638,7 +638,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
    LOG_DEBUG(
        " device: %s, group connected: %d, all active ase disconnected:: %d",
        leAudioDevice->address_.ToString().c_str(),
        group->IsAnyDeviceConnected(), group->HaveAllActiveDevicesCisDisc());
        group->IsAnyDeviceConnected(), group->HaveAllCisesDisconnected());

    /* Update the current group audio context availability which could change
     * due to disconnected group member.
@@ -649,8 +649,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
     * If there is active CIS, do nothing here. Just update the available
     * contexts table.
     */
    if (group->IsAnyDeviceConnected() &&
        !group->HaveAllActiveDevicesCisDisc()) {
    if (group->IsAnyDeviceConnected() && !group->HaveAllCisesDisconnected()) {
      if (group->GetState() == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
        /* We keep streaming but want others to let know user that it might be
         * need to update offloader with new CIS configuration
@@ -693,7 +692,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
       * or pending. If CIS is established, this will be handled in disconnected
       * complete event
       */
      if (group->HaveAllActiveDevicesCisDisc()) {
      if (group->HaveAllCisesDisconnected()) {
        RemoveCigForGroup(group);
      }

@@ -837,7 +836,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
         * If there is other device connected and streaming, just leave it as it
         * is, otherwise stop the stream.
         */
        if (!group->HaveAllActiveDevicesCisDisc()) {
        if (!group->HaveAllCisesDisconnected()) {
          /* There is ASE streaming for some device. Continue streaming. */
          LOG_WARN(
              "Group member disconnected during streaming. Cis handle 0x%04x",
@@ -863,7 +862,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
         */
        if ((group->GetState() ==
             AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) &&
            group->HaveAllActiveDevicesCisDisc()) {
            group->HaveAllCisesDisconnected()) {
          /* No more transition for group */
          alarm_cancel(watchdog_);

@@ -873,15 +872,58 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
        }
        break;
      case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
      case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED:
      case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
        /* Those two are used when closing the stream and CIS disconnection is
         * expected */
        if (group->HaveAllActiveDevicesCisDisc()) {
          RemoveCigForGroup(group);
        if (!group->HaveAllCisesDisconnected()) {
          LOG_DEBUG(
              "Still waiting for all CISes being disconnected for group:%d",
              group->group_id_);
          return;
        }

        break;
        auto current_group_state = group->GetState();
        LOG_INFO("group %d current state: %s, target state: %s",
                 group->group_id_,
                 bluetooth::common::ToString(current_group_state).c_str(),
                 bluetooth::common::ToString(target_state).c_str());
        /* It might happen that controller notified about CIS disconnection
         * later, after ASE state already changed.
         * In such an event, there is need to notify upper layer about state
         * from here.
         */
        if (current_group_state == AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
          LOG_INFO(
              "Cises disconnected for group %d, we are good in Idle state.",
              group->group_id_);
          ReleaseCisIds(group);
          state_machine_callbacks_->StatusReportCb(group->group_id_,
                                                   GroupStreamStatus::IDLE);
        } else if (current_group_state ==
                   AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED) {
          auto reconfig = group->IsPendingConfiguration();
          LOG_INFO(
              "Cises disconnected for group: %d, we are good in Configured "
              "state, reconfig=%d.",
              group->group_id_, reconfig);
          if (reconfig) {
            group->ClearPendingConfiguration();
            state_machine_callbacks_->StatusReportCb(
                group->group_id_, GroupStreamStatus::CONFIGURED_BY_USER);
            /* No more transition for group */
            alarm_cancel(watchdog_);
          } else {
            /* This is Autonomous change if both, target and current state
             * is CODEC_CONFIGURED
             */
            if (target_state == current_group_state) {
              state_machine_callbacks_->StatusReportCb(
                  group->group_id_, GroupStreamStatus::CONFIGURED_AUTONOMOUS);
            }
          }
        }
        RemoveCigForGroup(group);
      } break;
      default:
        break;
    }
@@ -1468,9 +1510,21 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
          PrepareAndSendRelease(leAudioDeviceNext);
        } else {
          /* Last node is in releasing state*/
          if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);

          group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);

          group->PrintDebugState();
          /* If all CISes are disconnected, notify upper layer about IDLE state,
           * otherwise wait for */
          if (!group->HaveAllCisesDisconnected()) {
            LOG_WARN(
                "Not all CISes removed before going to IDLE for group %d, "
                "waiting...",
                group->group_id_);
            group->PrintDebugState();
            return;
          }

          if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);
          ReleaseCisIds(group);
          state_machine_callbacks_->StatusReportCb(group->group_id_,
                                                   GroupStreamStatus::IDLE);
@@ -1651,6 +1705,19 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
                  AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED &&
              group->IsPendingConfiguration()) {
            LOG_INFO(" Configured state completed ");

            /* If all CISes are disconnected, notify upper layer about IDLE
             * state, otherwise wait for */
            if (!group->HaveAllCisesDisconnected()) {
              LOG_WARN(
                  "Not all CISes removed before going to CONFIGURED for group "
                  "%d, "
                  "waiting...",
                  group->group_id_);
              group->PrintDebugState();
              return;
            }

            group->ClearPendingConfiguration();
            state_machine_callbacks_->StatusReportCb(
                group->group_id_, GroupStreamStatus::CONFIGURED_BY_USER);
@@ -1789,7 +1856,6 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
          PrepareAndSendRelease(leAudioDeviceNext);
        } else {
          /* Last node is in releasing state*/
          if (alarm_is_scheduled(watchdog_)) alarm_cancel(watchdog_);

          group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
          /* Remote device has cache and keep staying in configured state after
@@ -1797,6 +1863,16 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
           * remote device.
           */
          group->SetTargetState(group->GetState());

          if (!group->HaveAllCisesDisconnected()) {
            LOG_WARN(
                "Not all CISes removed before going to IDLE for group %d, "
                "waiting...",
                group->group_id_);
            group->PrintDebugState();
            return;
          }

          state_machine_callbacks_->StatusReportCb(
              group->group_id_, GroupStreamStatus::CONFIGURED_AUTONOMOUS);
        }
@@ -1885,7 +1961,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {

        group->SetState(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);

        if (!group->HaveAllActiveDevicesCisDisc()) return;
        if (!group->HaveAllCisesDisconnected()) return;

        if (group->GetTargetState() ==
            AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED) {
+257 −9
Original line number Diff line number Diff line
@@ -2885,15 +2885,15 @@ static void InjectCisDisconnected(LeAudioDeviceGroup* group,
                                  uint8_t reason) {
  bluetooth::hci::iso_manager::cis_disconnected_evt event;

  auto* ase = leAudioDevice->GetFirstActiveAse();
  while (ase) {
  for (auto const ase : leAudioDevice->ases_) {
    if (ase.data_path_state != types::AudioStreamDataPathState::CIS_ASSIGNED &&
        ase.data_path_state != types::AudioStreamDataPathState::IDLE) {
      event.reason = reason;
      event.cig_id = group->group_id_;
    event.cis_conn_hdl = ase->cis_conn_hdl;
      event.cis_conn_hdl = ase.cis_conn_hdl;
      LeAudioGroupStateMachine::Get()->ProcessHciNotifCisDisconnected(
          group, leAudioDevice, &event);

    ase = leAudioDevice->GetNextActiveAse(ase);
    }
  }
}

@@ -3332,5 +3332,253 @@ TEST_F(StateMachineTest, BoundedHeadphonesConversationalToMediaChannelCount_1) {

  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
}

TEST_F(StateMachineTest, lateCisDisconnectedEvent_ConfiguredByUser) {
  const auto context_type = kContextTypeMedia;
  const auto leaudio_group_id = 6;
  const auto num_devices = 1;

  ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid);

  // Prepare multiple fake connected devices in a group
  auto* group =
      PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
  ASSERT_EQ(group->Size(), num_devices);

  PrepareConfigureCodecHandler(group, 0, true);
  PrepareConfigureQosHandler(group);
  PrepareEnableHandler(group);
  PrepareDisableHandler(group);
  PrepareReleaseHandler(group);

  auto* leAudioDevice = group->GetFirstDevice();
  auto expected_devices_written = 0;

  /* Three Writes:
   * 1: Codec Config
   * 2: Codec QoS
   * 3: Enabling
   */
  EXPECT_CALL(gatt_queue, WriteCharacteristic(leAudioDevice->conn_id_,
                                              leAudioDevice->ctp_hdls_.val_hdl,
                                              _, GATT_WRITE_NO_RSP, _, _))
      .Times(AtLeast(3));
  expected_devices_written++;

  ASSERT_EQ(expected_devices_written, num_devices);

  EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);

  InjectInitialIdleNotification(group);

  // Start the configuration and stream Media content
  LeAudioGroupStateMachine::Get()->StartStream(
      group, static_cast<LeAudioContextType>(context_type),
      types::AudioContexts(context_type));

  // Check if group has transitioned to a proper state
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
  testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);

  /* Prepare DisconnectCis mock to not symulate CisDisconnection */
  ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());

  /* Do reconfiguration */
  group->SetPendingConfiguration();

  // 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::CONFIGURED_BY_USER))
      .Times(0);
  LeAudioGroupStateMachine::Get()->StopStream(group);

  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);

  EXPECT_CALL(mock_callbacks_,
              StatusReportCb(
                  leaudio_group_id,
                  bluetooth::le_audio::GroupStreamStatus::CONFIGURED_BY_USER));

  // Inject CIS and ACL disconnection of first device
  InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
}

TEST_F(StateMachineTest, lateCisDisconnectedEvent_AutonomousConfigured) {
  const auto context_type = kContextTypeMedia;
  const auto leaudio_group_id = 6;
  const auto num_devices = 1;

  ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid);

  // Prepare multiple fake connected devices in a group
  auto* group =
      PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
  ASSERT_EQ(group->Size(), num_devices);

  PrepareConfigureCodecHandler(group, 0, true);
  PrepareConfigureQosHandler(group);
  PrepareEnableHandler(group);
  PrepareDisableHandler(group);
  PrepareReleaseHandler(group);

  auto* leAudioDevice = group->GetFirstDevice();
  auto expected_devices_written = 0;

  /* Three Writes:
   * 1: Codec Config
   * 2: Codec QoS
   * 3: Enabling
   */
  EXPECT_CALL(gatt_queue, WriteCharacteristic(leAudioDevice->conn_id_,
                                              leAudioDevice->ctp_hdls_.val_hdl,
                                              _, GATT_WRITE_NO_RSP, _, _))
      .Times(AtLeast(3));
  expected_devices_written++;

  ASSERT_EQ(expected_devices_written, num_devices);

  EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);

  InjectInitialIdleNotification(group);

  // Start the configuration and stream Media content
  LeAudioGroupStateMachine::Get()->StartStream(
      group, static_cast<LeAudioContextType>(context_type),
      types::AudioContexts(context_type));

  // Check if group has transitioned to a proper state
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
  testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);

  /* Prepare DisconnectCis mock to not symulate CisDisconnection */
  ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());

  // 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::CONFIGURED_AUTONOMOUS))
      .Times(0);

  // Stop the stream
  LeAudioGroupStateMachine::Get()->StopStream(group);

  // Check if group has transitioned to a proper state
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);

  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);

  EXPECT_CALL(
      mock_callbacks_,
      StatusReportCb(
          leaudio_group_id,
          bluetooth::le_audio::GroupStreamStatus::CONFIGURED_AUTONOMOUS));

  // Inject CIS and ACL disconnection of first device
  InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
}

TEST_F(StateMachineTest, lateCisDisconnectedEvent_Idle) {
  const auto context_type = kContextTypeMedia;
  const auto leaudio_group_id = 6;
  const auto num_devices = 1;

  ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid);

  // Prepare multiple fake connected devices in a group
  auto* group =
      PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
  ASSERT_EQ(group->Size(), num_devices);

  PrepareConfigureCodecHandler(group);
  PrepareConfigureQosHandler(group);
  PrepareEnableHandler(group);
  PrepareDisableHandler(group);
  PrepareReleaseHandler(group);

  auto* leAudioDevice = group->GetFirstDevice();
  auto expected_devices_written = 0;

  /* Three Writes:
   * 1: Codec Config
   * 2: Codec QoS
   * 3: Enabling
   */
  EXPECT_CALL(gatt_queue, WriteCharacteristic(leAudioDevice->conn_id_,
                                              leAudioDevice->ctp_hdls_.val_hdl,
                                              _, GATT_WRITE_NO_RSP, _, _))
      .Times(AtLeast(3));
  expected_devices_written++;

  ASSERT_EQ(expected_devices_written, num_devices);

  EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);

  InjectInitialIdleNotification(group);

  // Start the configuration and stream Media content
  LeAudioGroupStateMachine::Get()->StartStream(
      group, static_cast<LeAudioContextType>(context_type),
      types::AudioContexts(context_type));

  // Check if group has transitioned to a proper state
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
  testing::Mock::VerifyAndClearExpectations(&mock_iso_manager_);

  /* Prepare DisconnectCis mock to not symulate CisDisconnection */
  ON_CALL(*mock_iso_manager_, DisconnectCis).WillByDefault(Return());

  // 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))
      .Times(0);

  // Stop the stream
  LeAudioGroupStateMachine::Get()->StopStream(group);

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

  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);

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

  // Inject CIS and ACL disconnection of first device
  InjectCisDisconnected(group, leAudioDevice, HCI_ERR_CONN_CAUSE_LOCAL_HOST);
  testing::Mock::VerifyAndClearExpectations(&mock_callbacks_);
}
}  // namespace internal
}  // namespace le_audio