Reland "Add an active ICE controller that wraps a legacy controller (#7/n)"

This is a reland of commit 6326c9c201c7331d68c9beb0a93f6f6e21063cd2

Original change's description:
> Add an active ICE controller that wraps a legacy controller (#7/n)
>
> The wrapping ICE controller will allow existing ICE controller implementations to migrate to the active interface, and eventually deprecate the legacy interface.
>
> Follow-up CL has unit tests for P2PTransportChannel using the new wrapping controller.
>
> Bug: webrtc:14367, webrtc:14131
> Change-Id: I6c517449ff1e503e8268a7ef91afda793723fdeb
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/275302
> Reviewed-by: Per Kjellander <perkj@webrtc.org>
> Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
> Commit-Queue: Sameer Vijaykar <samvi@google.com>
> Cr-Commit-Position: refs/heads/main@{#38130}

Bug: webrtc:14367, webrtc:14131
Change-Id: I5662595db1df8c06b3acac9b39749f236906fa7e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/276044
Auto-Submit: Sameer Vijaykar <samvi@google.com>
Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
Reviewed-by: Per Kjellander <perkj@webrtc.org>
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38149}
diff --git a/api/ice_transport_interface.h b/api/ice_transport_interface.h
index cd6ada8..2ec41aa 100644
--- a/api/ice_transport_interface.h
+++ b/api/ice_transport_interface.h
@@ -99,8 +99,10 @@
   //      constructed and used.
   //
   //   2. If the field trial is enabled
-  //      - then an active ICE controller factory must be supplied and is used.
-  //      - the legacy ICE controller factory is not used in this case.
+  //      a. If an active ICE controller factory is supplied, it is used and
+  //      the legacy ICE controller factory is not used.
+  //      b. If not, a default active ICE controller is used, wrapping over the
+  //      supplied or the default legacy ICE controller.
   void set_active_ice_controller_factory(
       cricket::ActiveIceControllerFactoryInterface*
           active_ice_controller_factory) {
diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn
index 1009304..85ac605 100644
--- a/p2p/BUILD.gn
+++ b/p2p/BUILD.gn
@@ -81,6 +81,8 @@
     "base/turn_port.cc",
     "base/turn_port.h",
     "base/udp_port.h",
+    "base/wrapping_active_ice_controller.cc",
+    "base/wrapping_active_ice_controller.h",
     "client/basic_port_allocator.cc",
     "client/basic_port_allocator.h",
     "client/relay_port_factory_interface.h",
@@ -201,6 +203,7 @@
       "base/fake_packet_transport.h",
       "base/mock_active_ice_controller.h",
       "base/mock_async_resolver.h",
+      "base/mock_ice_agent.h",
       "base/mock_ice_controller.h",
       "base/mock_ice_transport.h",
       "base/test_stun_server.cc",
@@ -260,6 +263,7 @@
       "base/transport_description_unittest.cc",
       "base/turn_port_unittest.cc",
       "base/turn_server_unittest.cc",
+      "base/wrapping_active_ice_controller_unittest.cc",
       "client/basic_port_allocator_unittest.cc",
     ]
     deps = [
diff --git a/p2p/base/mock_ice_agent.h b/p2p/base/mock_ice_agent.h
new file mode 100644
index 0000000..e4100ec
--- /dev/null
+++ b/p2p/base/mock_ice_agent.h
@@ -0,0 +1,50 @@
+/*
+ *  Copyright 2018 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef P2P_BASE_MOCK_ICE_AGENT_H_
+#define P2P_BASE_MOCK_ICE_AGENT_H_
+
+#include <vector>
+
+#include "p2p/base/connection.h"
+#include "p2p/base/ice_agent_interface.h"
+#include "p2p/base/ice_switch_reason.h"
+#include "p2p/base/transport_description.h"
+#include "test/gmock.h"
+
+namespace cricket {
+
+class MockIceAgent : public IceAgentInterface {
+ public:
+  ~MockIceAgent() override = default;
+
+  MOCK_METHOD(int64_t, GetLastPingSentMs, (), (override, const));
+  MOCK_METHOD(IceRole, GetIceRole, (), (override, const));
+  MOCK_METHOD(void, OnStartedPinging, (), (override));
+  MOCK_METHOD(void, UpdateConnectionStates, (), (override));
+  MOCK_METHOD(void, UpdateState, (), (override));
+  MOCK_METHOD(void,
+              ForgetLearnedStateForConnections,
+              (std::vector<const Connection*>),
+              (override));
+  MOCK_METHOD(void, SendPingRequest, (const Connection*), (override));
+  MOCK_METHOD(void,
+              SwitchSelectedConnection,
+              (const Connection*, IceSwitchReason),
+              (override));
+  MOCK_METHOD(bool,
+              PruneConnections,
+              (std::vector<const Connection*>),
+              (override));
+};
+
+}  // namespace cricket
+
+#endif  // P2P_BASE_MOCK_ICE_AGENT_H_
diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc
index 2b0e906..0a0d8c8 100644
--- a/p2p/base/p2p_transport_channel.cc
+++ b/p2p/base/p2p_transport_channel.cc
@@ -33,6 +33,7 @@
 #include "p2p/base/connection.h"
 #include "p2p/base/connection_info.h"
 #include "p2p/base/port.h"
+#include "p2p/base/wrapping_active_ice_controller.h"
 #include "rtc_base/checks.h"
 #include "rtc_base/crc32.h"
 #include "rtc_base/experiments/struct_parameters_parser.h"
@@ -2472,10 +2473,15 @@
     P2PTransportChannel* transport)
     : transport_(transport) {
   if (UseActiveIceControllerFieldTrialEnabled(field_trials)) {
-    RTC_DCHECK(active_ice_controller_factory);
-    ActiveIceControllerFactoryArgs active_args{args,
-                                               /* ice_agent= */ transport};
-    active_ice_controller_ = active_ice_controller_factory->Create(active_args);
+    if (active_ice_controller_factory) {
+      ActiveIceControllerFactoryArgs active_args{args,
+                                                 /* ice_agent= */ transport};
+      active_ice_controller_ =
+          active_ice_controller_factory->Create(active_args);
+    } else {
+      active_ice_controller_ = std::make_unique<WrappingActiveIceController>(
+          /* ice_agent= */ transport, ice_controller_factory, args);
+    }
   } else {
     if (ice_controller_factory != nullptr) {
       legacy_ice_controller_ = ice_controller_factory->Create(args);
diff --git a/p2p/base/wrapping_active_ice_controller.cc b/p2p/base/wrapping_active_ice_controller.cc
new file mode 100644
index 0000000..c665921
--- /dev/null
+++ b/p2p/base/wrapping_active_ice_controller.cc
@@ -0,0 +1,253 @@
+/*
+ *  Copyright 2022 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "p2p/base/wrapping_active_ice_controller.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "api/sequence_checker.h"
+#include "api/task_queue/pending_task_safety_flag.h"
+#include "api/units/time_delta.h"
+#include "p2p/base/basic_ice_controller.h"
+#include "p2p/base/connection.h"
+#include "p2p/base/ice_agent_interface.h"
+#include "p2p/base/ice_controller_interface.h"
+#include "p2p/base/ice_switch_reason.h"
+#include "p2p/base/ice_transport_internal.h"
+#include "p2p/base/transport_description.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/thread.h"
+#include "rtc_base/time_utils.h"
+
+namespace {
+using ::webrtc::SafeTask;
+using ::webrtc::TimeDelta;
+}  // unnamed namespace
+
+namespace cricket {
+
+WrappingActiveIceController::WrappingActiveIceController(
+    IceAgentInterface* ice_agent,
+    std::unique_ptr<IceControllerInterface> wrapped)
+    : network_thread_(rtc::Thread::Current()),
+      wrapped_(std::move(wrapped)),
+      agent_(*ice_agent) {
+  RTC_DCHECK(ice_agent != nullptr);
+}
+
+WrappingActiveIceController::WrappingActiveIceController(
+    IceAgentInterface* ice_agent,
+    IceControllerFactoryInterface* wrapped_factory,
+    const IceControllerFactoryArgs& wrapped_factory_args)
+    : network_thread_(rtc::Thread::Current()), agent_(*ice_agent) {
+  RTC_DCHECK(ice_agent != nullptr);
+  if (wrapped_factory) {
+    wrapped_ = wrapped_factory->Create(wrapped_factory_args);
+  } else {
+    wrapped_ = std::make_unique<BasicIceController>(wrapped_factory_args);
+  }
+}
+
+WrappingActiveIceController::~WrappingActiveIceController() {}
+
+void WrappingActiveIceController::SetIceConfig(const IceConfig& config) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  wrapped_->SetIceConfig(config);
+}
+
+bool WrappingActiveIceController::GetUseCandidateAttribute(
+    const Connection* connection,
+    NominationMode mode,
+    IceMode remote_ice_mode) const {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  return wrapped_->GetUseCandidateAttr(connection, mode, remote_ice_mode);
+}
+
+void WrappingActiveIceController::OnConnectionAdded(
+    const Connection* connection) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  wrapped_->AddConnection(connection);
+}
+
+void WrappingActiveIceController::OnConnectionPinged(
+    const Connection* connection) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  wrapped_->MarkConnectionPinged(connection);
+}
+
+void WrappingActiveIceController::OnConnectionUpdated(
+    const Connection* connection) {
+  RTC_LOG(LS_VERBOSE) << "Connection report for " << connection->ToString();
+  // Do nothing. Native ICE controllers have direct access to Connection, so no
+  // need to update connection state separately.
+}
+
+void WrappingActiveIceController::OnConnectionSwitched(
+    const Connection* connection) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  selected_connection_ = connection;
+  wrapped_->SetSelectedConnection(connection);
+}
+
+void WrappingActiveIceController::OnConnectionDestroyed(
+    const Connection* connection) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  wrapped_->OnConnectionDestroyed(connection);
+}
+
+void WrappingActiveIceController::MaybeStartPinging() {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  if (started_pinging_) {
+    return;
+  }
+
+  if (wrapped_->HasPingableConnection()) {
+    network_thread_->PostTask(
+        SafeTask(task_safety_.flag(), [this]() { SelectAndPingConnection(); }));
+    agent_.OnStartedPinging();
+    started_pinging_ = true;
+  }
+}
+
+void WrappingActiveIceController::SelectAndPingConnection() {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  agent_.UpdateConnectionStates();
+
+  IceControllerInterface::PingResult result =
+      wrapped_->SelectConnectionToPing(agent_.GetLastPingSentMs());
+  HandlePingResult(result);
+}
+
+void WrappingActiveIceController::HandlePingResult(
+    IceControllerInterface::PingResult result) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+
+  if (result.connection.has_value()) {
+    agent_.SendPingRequest(result.connection.value());
+  }
+
+  network_thread_->PostDelayedTask(
+      SafeTask(task_safety_.flag(), [this]() { SelectAndPingConnection(); }),
+      TimeDelta::Millis(result.recheck_delay_ms));
+}
+
+void WrappingActiveIceController::OnSortAndSwitchRequest(
+    IceSwitchReason reason) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  if (!sort_pending_) {
+    network_thread_->PostTask(SafeTask(task_safety_.flag(), [this, reason]() {
+      SortAndSwitchToBestConnection(reason);
+    }));
+    sort_pending_ = true;
+  }
+}
+
+void WrappingActiveIceController::OnImmediateSortAndSwitchRequest(
+    IceSwitchReason reason) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  SortAndSwitchToBestConnection(reason);
+}
+
+void WrappingActiveIceController::SortAndSwitchToBestConnection(
+    IceSwitchReason reason) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+
+  // Make sure the connection states are up-to-date since this affects how they
+  // will be sorted.
+  agent_.UpdateConnectionStates();
+
+  // Any changes after this point will require a re-sort.
+  sort_pending_ = false;
+
+  IceControllerInterface::SwitchResult result =
+      wrapped_->SortAndSwitchConnection(reason);
+  HandleSwitchResult(reason, result);
+  UpdateStateOnConnectionsResorted();
+}
+
+bool WrappingActiveIceController::OnImmediateSwitchRequest(
+    IceSwitchReason reason,
+    const Connection* selected) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  IceControllerInterface::SwitchResult result =
+      wrapped_->ShouldSwitchConnection(reason, selected);
+  HandleSwitchResult(reason, result);
+  return result.connection.has_value();
+}
+
+void WrappingActiveIceController::HandleSwitchResult(
+    IceSwitchReason reason_for_switch,
+    IceControllerInterface::SwitchResult result) {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  if (result.connection.has_value()) {
+    RTC_LOG(LS_INFO) << "Switching selected connection due to: "
+                     << IceSwitchReasonToString(reason_for_switch);
+    agent_.SwitchSelectedConnection(result.connection.value(),
+                                    reason_for_switch);
+  }
+
+  if (result.recheck_event.has_value()) {
+    // If we do not switch to the connection because it missed the receiving
+    // threshold, the new connection is in a better receiving state than the
+    // currently selected connection. So we need to re-check whether it needs
+    // to be switched at a later time.
+    network_thread_->PostDelayedTask(
+        SafeTask(task_safety_.flag(),
+                 [this, recheck_reason = result.recheck_event->reason]() {
+                   SortAndSwitchToBestConnection(recheck_reason);
+                 }),
+        TimeDelta::Millis(result.recheck_event->recheck_delay_ms));
+  }
+
+  agent_.ForgetLearnedStateForConnections(
+      result.connections_to_forget_state_on);
+}
+
+void WrappingActiveIceController::UpdateStateOnConnectionsResorted() {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  PruneConnections();
+
+  // Update the internal state of the ICE agentl.
+  agent_.UpdateState();
+
+  // Also possibly start pinging.
+  // We could start pinging if:
+  // * The first connection was created.
+  // * ICE credentials were provided.
+  // * A TCP connection became connected.
+  MaybeStartPinging();
+}
+
+void WrappingActiveIceController::PruneConnections() {
+  RTC_DCHECK_RUN_ON(network_thread_);
+
+  // The controlled side can prune only if the selected connection has been
+  // nominated because otherwise it may prune the connection that will be
+  // selected by the controlling side.
+  // TODO(honghaiz): This is not enough to prevent a connection from being
+  // pruned too early because with aggressive nomination, the controlling side
+  // will nominate every connection until it becomes writable.
+  if (agent_.GetIceRole() == ICEROLE_CONTROLLING ||
+      (selected_connection_ && selected_connection_->nominated())) {
+    std::vector<const Connection*> connections_to_prune =
+        wrapped_->PruneConnections();
+    agent_.PruneConnections(connections_to_prune);
+  }
+}
+
+// Only for unit tests
+const Connection* WrappingActiveIceController::FindNextPingableConnection() {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  return wrapped_->FindNextPingableConnection();
+}
+
+}  // namespace cricket
diff --git a/p2p/base/wrapping_active_ice_controller.h b/p2p/base/wrapping_active_ice_controller.h
new file mode 100644
index 0000000..449c0f0
--- /dev/null
+++ b/p2p/base/wrapping_active_ice_controller.h
@@ -0,0 +1,97 @@
+/*
+ *  Copyright 2022 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_
+#define P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_
+
+#include <memory>
+
+#include "absl/types/optional.h"
+#include "api/task_queue/pending_task_safety_flag.h"
+#include "p2p/base/active_ice_controller_interface.h"
+#include "p2p/base/connection.h"
+#include "p2p/base/ice_agent_interface.h"
+#include "p2p/base/ice_controller_factory_interface.h"
+#include "p2p/base/ice_controller_interface.h"
+#include "p2p/base/ice_switch_reason.h"
+#include "p2p/base/ice_transport_internal.h"
+#include "p2p/base/transport_description.h"
+#include "rtc_base/thread.h"
+#include "rtc_base/thread_annotations.h"
+
+namespace cricket {
+
+// WrappingActiveIceController provides the functionality of a legacy passive
+// ICE controller but packaged as an active ICE Controller.
+class WrappingActiveIceController : public ActiveIceControllerInterface {
+ public:
+  // Constructs an active ICE controller wrapping an already constructed legacy
+  // ICE controller. Does not take ownership of the ICE agent, which must
+  // already exist and outlive the ICE controller.
+  WrappingActiveIceController(IceAgentInterface* ice_agent,
+                              std::unique_ptr<IceControllerInterface> wrapped);
+  // Constructs an active ICE controller that wraps over a legacy ICE
+  // controller. The legacy ICE controller is constructed through a factory, if
+  // one is supplied. If not, a default BasicIceController is wrapped instead.
+  // Does not take ownership of the ICE agent, which must already exist and
+  // outlive the ICE controller.
+  WrappingActiveIceController(
+      IceAgentInterface* ice_agent,
+      IceControllerFactoryInterface* wrapped_factory,
+      const IceControllerFactoryArgs& wrapped_factory_args);
+  virtual ~WrappingActiveIceController();
+
+  void SetIceConfig(const IceConfig& config) override;
+  bool GetUseCandidateAttribute(const Connection* connection,
+                                NominationMode mode,
+                                IceMode remote_ice_mode) const override;
+
+  void OnConnectionAdded(const Connection* connection) override;
+  void OnConnectionPinged(const Connection* connection) override;
+  void OnConnectionUpdated(const Connection* connection) override;
+  void OnConnectionSwitched(const Connection* connection) override;
+  void OnConnectionDestroyed(const Connection* connection) override;
+
+  void OnSortAndSwitchRequest(IceSwitchReason reason) override;
+  void OnImmediateSortAndSwitchRequest(IceSwitchReason reason) override;
+  bool OnImmediateSwitchRequest(IceSwitchReason reason,
+                                const Connection* selected) override;
+
+  // Only for unit tests
+  const Connection* FindNextPingableConnection() override;
+
+ private:
+  void MaybeStartPinging();
+  void SelectAndPingConnection();
+  void HandlePingResult(IceControllerInterface::PingResult result);
+
+  void SortAndSwitchToBestConnection(IceSwitchReason reason);
+  void HandleSwitchResult(IceSwitchReason reason_for_switch,
+                          IceControllerInterface::SwitchResult result);
+  void UpdateStateOnConnectionsResorted();
+
+  void PruneConnections();
+
+  rtc::Thread* const network_thread_;
+  webrtc::ScopedTaskSafety task_safety_;
+
+  bool started_pinging_ RTC_GUARDED_BY(network_thread_) = false;
+  bool sort_pending_ RTC_GUARDED_BY(network_thread_) = false;
+  const Connection* selected_connection_ RTC_GUARDED_BY(network_thread_) =
+      nullptr;
+
+  std::unique_ptr<IceControllerInterface> wrapped_
+      RTC_GUARDED_BY(network_thread_);
+  IceAgentInterface& agent_ RTC_GUARDED_BY(network_thread_);
+};
+
+}  // namespace cricket
+
+#endif  // P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_
diff --git a/p2p/base/wrapping_active_ice_controller_unittest.cc b/p2p/base/wrapping_active_ice_controller_unittest.cc
new file mode 100644
index 0000000..7dfdfef
--- /dev/null
+++ b/p2p/base/wrapping_active_ice_controller_unittest.cc
@@ -0,0 +1,313 @@
+/*
+ *  Copyright 2009 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "p2p/base/wrapping_active_ice_controller.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "p2p/base/connection.h"
+#include "p2p/base/mock_ice_agent.h"
+#include "p2p/base/mock_ice_controller.h"
+#include "rtc_base/fake_clock.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/thread.h"
+
+namespace {
+
+using ::cricket::Connection;
+using ::cricket::IceConfig;
+using ::cricket::IceControllerFactoryArgs;
+using ::cricket::IceControllerInterface;
+using ::cricket::IceMode;
+using ::cricket::IceRecheckEvent;
+using ::cricket::IceSwitchReason;
+using ::cricket::MockIceAgent;
+using ::cricket::MockIceController;
+using ::cricket::MockIceControllerFactory;
+using ::cricket::NominationMode;
+using ::cricket::WrappingActiveIceController;
+
+using ::testing::_;
+using ::testing::NiceMock;
+using ::testing::Ref;
+using ::testing::Return;
+using ::testing::Sequence;
+
+using ::rtc::AutoThread;
+using ::rtc::Event;
+using ::rtc::ScopedFakeClock;
+using ::webrtc::TimeDelta;
+
+using NiceMockIceController = NiceMock<MockIceController>;
+
+static const Connection* kConnection =
+    reinterpret_cast<const Connection*>(0xabcd);
+static const Connection* kConnectionTwo =
+    reinterpret_cast<const Connection*>(0xbcde);
+static const Connection* kConnectionThree =
+    reinterpret_cast<const Connection*>(0xcdef);
+
+static const std::vector<const Connection*> kEmptyConnsList =
+    std::vector<const Connection*>();
+
+static const TimeDelta kTick = TimeDelta::Millis(1);
+
+TEST(WrappingActiveIceControllerTest, CreateLegacyIceControllerFromFactory) {
+  AutoThread main;
+  MockIceAgent agent;
+  IceControllerFactoryArgs args;
+  MockIceControllerFactory legacy_controller_factory;
+  EXPECT_CALL(legacy_controller_factory, RecordIceControllerCreated()).Times(1);
+  WrappingActiveIceController controller(&agent, &legacy_controller_factory,
+                                         args);
+}
+
+TEST(WrappingActiveIceControllerTest, PassthroughIceControllerInterface) {
+  AutoThread main;
+  MockIceAgent agent;
+  std::unique_ptr<MockIceController> will_move =
+      std::make_unique<MockIceController>(IceControllerFactoryArgs{});
+  MockIceController* wrapped = will_move.get();
+  WrappingActiveIceController controller(&agent, std::move(will_move));
+
+  IceConfig config{};
+  EXPECT_CALL(*wrapped, SetIceConfig(Ref(config)));
+  controller.SetIceConfig(config);
+
+  EXPECT_CALL(*wrapped,
+              GetUseCandidateAttr(kConnection, NominationMode::AGGRESSIVE,
+                                  IceMode::ICEMODE_LITE))
+      .WillOnce(Return(true));
+  EXPECT_TRUE(controller.GetUseCandidateAttribute(
+      kConnection, NominationMode::AGGRESSIVE, IceMode::ICEMODE_LITE));
+
+  EXPECT_CALL(*wrapped, AddConnection(kConnection));
+  controller.OnConnectionAdded(kConnection);
+
+  EXPECT_CALL(*wrapped, OnConnectionDestroyed(kConnection));
+  controller.OnConnectionDestroyed(kConnection);
+
+  EXPECT_CALL(*wrapped, SetSelectedConnection(kConnection));
+  controller.OnConnectionSwitched(kConnection);
+
+  EXPECT_CALL(*wrapped, MarkConnectionPinged(kConnection));
+  controller.OnConnectionPinged(kConnection);
+
+  EXPECT_CALL(*wrapped, FindNextPingableConnection())
+      .WillOnce(Return(kConnection));
+  EXPECT_EQ(controller.FindNextPingableConnection(), kConnection);
+}
+
+TEST(WrappingActiveIceControllerTest, HandlesImmediateSwitchRequest) {
+  AutoThread main;
+  ScopedFakeClock clock;
+  NiceMock<MockIceAgent> agent;
+  std::unique_ptr<NiceMockIceController> will_move =
+      std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+  NiceMockIceController* wrapped = will_move.get();
+  WrappingActiveIceController controller(&agent, std::move(will_move));
+
+  IceSwitchReason reason = IceSwitchReason::NOMINATION_ON_CONTROLLED_SIDE;
+  std::vector<const Connection*> conns_to_forget{kConnectionTwo};
+  int recheck_delay_ms = 10;
+  IceControllerInterface::SwitchResult switch_result{
+      kConnection,
+      IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK,
+                      recheck_delay_ms),
+      conns_to_forget};
+
+  // ICE controller should switch to given connection immediately.
+  Sequence check_then_switch;
+  EXPECT_CALL(*wrapped, ShouldSwitchConnection(reason, kConnection))
+      .InSequence(check_then_switch)
+      .WillOnce(Return(switch_result));
+  EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason))
+      .InSequence(check_then_switch);
+  EXPECT_CALL(agent, ForgetLearnedStateForConnections(conns_to_forget));
+
+  EXPECT_TRUE(controller.OnImmediateSwitchRequest(reason, kConnection));
+
+  // No rechecks before recheck delay.
+  clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms - 1));
+
+  // ICE controller should recheck for best connection after the recheck delay.
+  Sequence recheck_sort;
+  EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(recheck_sort);
+  EXPECT_CALL(*wrapped,
+              SortAndSwitchConnection(IceSwitchReason::ICE_CONTROLLER_RECHECK))
+      .InSequence(recheck_sort)
+      .WillOnce(Return(IceControllerInterface::SwitchResult{}));
+  EXPECT_CALL(agent, ForgetLearnedStateForConnections(kEmptyConnsList));
+
+  clock.AdvanceTime(kTick);
+}
+
+TEST(WrappingActiveIceControllerTest, HandlesImmediateSortAndSwitchRequest) {
+  AutoThread main;
+  ScopedFakeClock clock;
+  NiceMock<MockIceAgent> agent;
+  std::unique_ptr<NiceMockIceController> will_move =
+      std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+  NiceMockIceController* wrapped = will_move.get();
+  WrappingActiveIceController controller(&agent, std::move(will_move));
+
+  IceSwitchReason reason = IceSwitchReason::NEW_CONNECTION_FROM_LOCAL_CANDIDATE;
+  std::vector<const Connection*> conns_to_forget{kConnectionTwo};
+  std::vector<const Connection*> conns_to_prune{kConnectionThree};
+  int recheck_delay_ms = 10;
+  IceControllerInterface::SwitchResult switch_result{
+      kConnection,
+      IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK,
+                      recheck_delay_ms),
+      conns_to_forget};
+
+  Sequence sort_and_switch;
+  EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(sort_and_switch);
+  EXPECT_CALL(*wrapped, SortAndSwitchConnection(reason))
+      .InSequence(sort_and_switch)
+      .WillOnce(Return(switch_result));
+  EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason))
+      .InSequence(sort_and_switch);
+  EXPECT_CALL(*wrapped, PruneConnections())
+      .InSequence(sort_and_switch)
+      .WillOnce(Return(conns_to_prune));
+  EXPECT_CALL(agent, PruneConnections(conns_to_prune))
+      .InSequence(sort_and_switch);
+
+  controller.OnImmediateSortAndSwitchRequest(reason);
+
+  // No rechecks before recheck delay.
+  clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms - 1));
+
+  // ICE controller should recheck for best connection after the recheck delay.
+  Sequence recheck_sort;
+  EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(recheck_sort);
+  EXPECT_CALL(*wrapped,
+              SortAndSwitchConnection(IceSwitchReason::ICE_CONTROLLER_RECHECK))
+      .InSequence(recheck_sort)
+      .WillOnce(Return(IceControllerInterface::SwitchResult{}));
+  EXPECT_CALL(*wrapped, PruneConnections())
+      .InSequence(recheck_sort)
+      .WillOnce(Return(kEmptyConnsList));
+  EXPECT_CALL(agent, PruneConnections(kEmptyConnsList))
+      .InSequence(recheck_sort);
+
+  clock.AdvanceTime(kTick);
+}
+
+TEST(WrappingActiveIceControllerTest, HandlesSortAndSwitchRequest) {
+  AutoThread main;
+  ScopedFakeClock clock;
+
+  // Block the main task queue until ready.
+  Event init;
+  TimeDelta init_delay = TimeDelta::Millis(10);
+  main.PostTask([&init, &init_delay] { init.Wait(init_delay); });
+
+  NiceMock<MockIceAgent> agent;
+  std::unique_ptr<NiceMockIceController> will_move =
+      std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+  NiceMockIceController* wrapped = will_move.get();
+  WrappingActiveIceController controller(&agent, std::move(will_move));
+
+  IceSwitchReason reason = IceSwitchReason::NETWORK_PREFERENCE_CHANGE;
+
+  // No action should occur immediately
+  EXPECT_CALL(agent, UpdateConnectionStates()).Times(0);
+  EXPECT_CALL(*wrapped, SortAndSwitchConnection(_)).Times(0);
+  EXPECT_CALL(agent, SwitchSelectedConnection(_, _)).Times(0);
+
+  controller.OnSortAndSwitchRequest(reason);
+
+  std::vector<const Connection*> conns_to_forget{kConnectionTwo};
+  int recheck_delay_ms = 10;
+  IceControllerInterface::SwitchResult switch_result{
+      kConnection,
+      IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK,
+                      recheck_delay_ms),
+      conns_to_forget};
+
+  // Sort and switch should take place as the subsequent task.
+  Sequence sort_and_switch;
+  EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(sort_and_switch);
+  EXPECT_CALL(*wrapped, SortAndSwitchConnection(reason))
+      .InSequence(sort_and_switch)
+      .WillOnce(Return(switch_result));
+  EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason))
+      .InSequence(sort_and_switch);
+
+  // Unblock the init task.
+  clock.AdvanceTime(init_delay);
+}
+
+TEST(WrappingActiveIceControllerTest, StartPingingAfterSortAndSwitch) {
+  AutoThread main;
+  ScopedFakeClock clock;
+
+  // Block the main task queue until ready.
+  Event init;
+  TimeDelta init_delay = TimeDelta::Millis(10);
+  main.PostTask([&init, &init_delay] { init.Wait(init_delay); });
+
+  NiceMock<MockIceAgent> agent;
+  std::unique_ptr<NiceMockIceController> will_move =
+      std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+  NiceMockIceController* wrapped = will_move.get();
+  WrappingActiveIceController controller(&agent, std::move(will_move));
+
+  // Pinging does not start automatically, unless triggered through a sort.
+  EXPECT_CALL(*wrapped, HasPingableConnection()).Times(0);
+  EXPECT_CALL(*wrapped, SelectConnectionToPing(_)).Times(0);
+  EXPECT_CALL(agent, OnStartedPinging()).Times(0);
+
+  controller.OnSortAndSwitchRequest(IceSwitchReason::DATA_RECEIVED);
+
+  // Pinging does not start if no pingable connection.
+  EXPECT_CALL(*wrapped, HasPingableConnection()).WillOnce(Return(false));
+  EXPECT_CALL(*wrapped, SelectConnectionToPing(_)).Times(0);
+  EXPECT_CALL(agent, OnStartedPinging()).Times(0);
+
+  // Unblock the init task.
+  clock.AdvanceTime(init_delay);
+
+  int recheck_delay_ms = 10;
+  IceControllerInterface::PingResult ping_result(kConnection, recheck_delay_ms);
+
+  // Pinging starts when there is a pingable connection.
+  Sequence start_pinging;
+  EXPECT_CALL(*wrapped, HasPingableConnection())
+      .InSequence(start_pinging)
+      .WillOnce(Return(true));
+  EXPECT_CALL(agent, OnStartedPinging()).InSequence(start_pinging);
+  EXPECT_CALL(agent, GetLastPingSentMs())
+      .InSequence(start_pinging)
+      .WillOnce(Return(123));
+  EXPECT_CALL(*wrapped, SelectConnectionToPing(123))
+      .InSequence(start_pinging)
+      .WillOnce(Return(ping_result));
+  EXPECT_CALL(agent, SendPingRequest(kConnection)).InSequence(start_pinging);
+
+  controller.OnSortAndSwitchRequest(IceSwitchReason::DATA_RECEIVED);
+  clock.AdvanceTime(kTick);
+
+  // ICE controller should recheck and ping after the recheck delay.
+  // No ping should be sent if no connection selected to ping.
+  EXPECT_CALL(agent, GetLastPingSentMs()).WillOnce(Return(456));
+  EXPECT_CALL(*wrapped, SelectConnectionToPing(456))
+      .WillOnce(Return(IceControllerInterface::PingResult(
+          /* connection= */ nullptr, recheck_delay_ms)));
+  EXPECT_CALL(agent, SendPingRequest(kConnection)).Times(0);
+
+  clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms));
+}
+
+}  // namespace