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

Commit 4912dcf9 authored by Łukasz Rymanowski's avatar Łukasz Rymanowski Committed by Android (Google) Code Review
Browse files

Merge "leaudio: Fix race between ASE state and CIS disconnection" into tm-qpr-dev

parents e0a9343e 0938d11a
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