text: // Copyright 2017 The Fuchsia Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "low_energy_discovery_manager.h"
  
  #include <zircon/assert.h>
  
  #include "garnet/drivers/bluetooth/lib/hci/legacy_low_energy_scanner.h"
  #include "garnet/drivers/bluetooth/lib/hci/transport.h"
  
  #include "remote_device.h"
  #include "remote_device_cache.h"
  
  namespace btlib {
  namespace gap {
  
  LowEnergyDiscoverySession::LowEnergyDiscoverySession(
      fxl::WeakPtr<LowEnergyDiscoveryManager> manager)
      : active_(true), manager_(manager) {
    ZX_DEBUG_ASSERT(manager_);
  }
  
  LowEnergyDiscoverySession::~LowEnergyDiscoverySession() {
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
    if (active_)
      Stop();
  }
  
  void LowEnergyDiscoverySession::SetResultCallback(
      DeviceFoundCallback callback) {
    device_found_callback_ = std::move(callback);
    if (!manager_)
      return;
    for (const auto& cached_device_id : manager_->cached_scan_results()) {
      auto device = manager_->device_cache()->FindDeviceById(cached_device_id);
      ZX_DEBUG_ASSERT(device);
      NotifyDiscoveryResult(*device);
    }
  }
  
  void LowEnergyDiscoverySession::Stop() {
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
    ZX_DEBUG_ASSERT(active_);
    if (manager_) {
      manager_->RemoveSession(this);
    }
    active_ = false;
  }
  
  void LowEnergyDiscoverySession::NotifyDiscoveryResult(
      const RemoteDevice& device) const {
    ZX_DEBUG_ASSERT(device.le());
    if (device_found_callback_ &&
        filter_.MatchLowEnergyResult(device.le()->advertising_data(),
                                     device.connectable(), device.rssi())) {
      device_found_callback_(device);
    }
  }
  
  void LowEnergyDiscoverySession::NotifyError() {
    active_ = false;
    if (error_callback_)
      error_callback_();
  }
  
  LowEnergyDiscoveryManager::LowEnergyDiscoveryManager(
      Mode mode, fxl::RefPtr<hci::Transport> hci, RemoteDeviceCache* device_cache)
      : dispatcher_(async_get_default_dispatcher()),
        device_cache_(device_cache),
        background_scan_enabled_(false),
        weak_ptr_factory_(this) {
    ZX_DEBUG_ASSERT(hci);
    ZX_DEBUG_ASSERT(dispatcher_);
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
    ZX_DEBUG_ASSERT(device_cache_);
  
    // We currently do not support the Extended Advertising feature.
    ZX_DEBUG_ASSERT(mode == Mode::kLegacy);
  
    scanner_ =
        std::make_unique<hci::LegacyLowEnergyScanner>(this, hci, dispatcher_);
  }
  
  LowEnergyDiscoveryManager::~LowEnergyDiscoveryManager() {
    // TODO(armansito): Invalidate all known session objects here.
  }
  
  void LowEnergyDiscoveryManager::StartDiscovery(SessionCallback callback) {
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
    ZX_DEBUG_ASSERT(callback);
    bt_log(INFO, "gap-le", "start discovery");
  
    // If a request to start or stop is currently pending then this one will
    // become pending until the HCI request completes (this does NOT include the
    // state in which we are stopping and restarting scan in between scan
    // periods).
    if (!pending_.empty() ||
        (scanner_->state() == hci::LowEnergyScanner::State::kStopping &&
         sessions_.empty())) {
      ZX_DEBUG_ASSERT(!scanner_->IsScanning());
      pending_.push(std::move(callback));
      return;
    }
  
    // If a device scan is already in progress, then the request succeeds (this
    // includes the state in which we are stopping and restarting scan in between
    // scan periods).
    if (!sessions_.empty()) {
      // Invoke |callback| asynchronously.
      auto session = AddSession();
      async::PostTask(dispatcher_, [callback = std::move(callback),
                                    session = std::move(session)]() mutable {
        callback(std::move(session));
      });
      return;
    }
  
    pending_.push(std::move(callback));
  
    // If currently scanning in the background, stop it and wait for
    // OnScanStatus() to initiate the active scan. Otherwise, request an active
    // scan if the scanner is idle.
    if (scanner_->IsPassiveScanning()) {
      ZX_DEBUG_ASSERT(background_scan_enabled_);
      scanner_->StopScan();
    } else if (!background_scan_enabled_ && scanner_->IsIdle()) {
      StartActiveScan();
    }
  }
  
  void LowEnergyDiscoveryManager::EnableBackgroundScan(bool enable) {
    if (background_scan_enabled_ == enable) {
      bt_log(TRACE, "gap-le", "background scan already %s",
             (enable ? "enabled" : "disabled"));
      return;
    }
  
    background_scan_enabled_ = enable;
  
    // Do nothing if an active scan is in progress.
    if (!sessions_.empty() || !pending_.empty()) {
      return;
    }
  
    if (enable && scanner_->IsIdle()) {
      StartPassiveScan();
    } else if (!enable && scanner_->IsPassiveScanning()) {
      scanner_->StopScan();
    }
    // If neither condition is true, we'll apply a scan policy in OnScanStatus().
  }
  
  std::unique_ptr<LowEnergyDiscoverySession>
  LowEnergyDiscoveryManager::AddSession() {
    // Cannot use make_unique here since LowEnergyDiscoverySession has a private
    // constructor.
    std::unique_ptr<LowEnergyDiscoverySession> session(
        new LowEnergyDiscoverySession(weak_ptr_factory_.GetWeakPtr()));
    ZX_DEBUG_ASSERT(sessions_.find(session.get()) == sessions_.end());
    sessions_.insert(session.get());
    return session;
  }
  
  void LowEnergyDiscoveryManager::RemoveSession(
      LowEnergyDiscoverySession* session) {
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
    ZX_DEBUG_ASSERT(session);
  
    // Only active sessions are allowed to call this method. If there is at least
    // one active session object out there, then we MUST be scanning.
    ZX_DEBUG_ASSERT(session->active());
  
    ZX_DEBUG_ASSERT(sessions_.find(session) != sessions_.end());
    sessions_.erase(session);
  
    // Stop scanning if the session count has dropped to zero.
    if (sessions_.empty())
      scanner_->StopScan();
  }
  
  void LowEnergyDiscoveryManager::OnDeviceFound(
      const hci::LowEnergyScanResult& result, const common::ByteBuffer& data) {
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
  
    // Ignore regular scan results during a passive scan.
    if (scanner_->IsPassiveScanning()) {
      return;
    }
  
    auto device = device_cache_->FindDeviceByAddress(result.address);
    if (!device) {
      device = device_cache_->NewDevice(result.address, result.connectable);
    }
    device->MutLe().SetAdvertisingData(result.rssi, data);
  
    cached_scan_results_.insert(device->identifier());
  
    for (const auto& session : sessions_) {
      session->NotifyDiscoveryResult(*device);
    }
  }
  
  void LowEnergyDiscoveryManager::OnDirectedAdvertisement(
      const hci::LowEnergyScanResult& result) {
    ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
  
    // TODO(NET-1572): Resolve the address in the host if it is random and
    // |result.resolved| is false.
    bt_log(SPEW, "gap", "Received directed advertisement (address: %s, %s)",
           result.address.ToString().c_str(),
           (result.resolved ? "resolved" : "not resolved"));
  
    auto device = device_cache_->FindDeviceByAddress(result.address);
    if (!device) {
      bt_log(TRACE, "gap",
             "ignoring connection request from unknown peripheral: %s",
             result.address.ToString().c_str());
      return;
    }
  
    if (!device->le() || !device->le()->bonded()) {
      bt_log(TRACE, "gap",
             "rejecting connection request from unbonded peripheral: %s",
             result.address.ToString().c_str());
      return;
    }
  
    // TODO(armansito): We shouldn't always accept connection requests from all
    // bonded peripherals (e.g. if one is explicitly disconnected). Maybe add an
    // "auto_connect()" property to RemoteDevice?
    if (directed_conn_cb_) {
      directed_conn_cb_(device->identifier());
    }
  }
  
  void LowEnergyDiscoveryManager::OnScanStatus(
      hci::LowEnergyScanner::ScanStatus status) {
    switch (status) {
      case hci::LowEnergyScanner::ScanStatus::kFailed: {
        bt_log(ERROR, "gap-le", "failed to initiate scan!");
  
        // Clear all sessions.
        auto sessions = std::move(sessions_);
        for (auto& s : sessions) {
          s->NotifyError();
        }
  
        // Report failure on all currently pending requests. If any of the
        // callbacks issue a retry the new requests will get re-queued and
        // notified of failure in the same loop here.
        while (!pending_.empty()) {
          auto callback = std::move(pending_.front());
          pending_.pop();
  
          callback(nullptr);
        }
        return;
      }
      case hci::LowEnergyScanner::ScanStatus::kPassive:
        bt_log(SPEW, "gap-le", "passive scan started");
  
        // Stop the background scan if active scan was requested or background
        // scan was disabled while waiting for the scan to start. If an active
        // scan was requested then the active scan we'll start it once the passive
        // scan stops.
        if (!pending_.empty() || !background_scan_enabled_) {
          scanner_->StopScan();
        }
        return;
      case hci::LowEnergyScanner::ScanStatus::kActive:
        bt_log(SPEW, "gap-le", "active scan started");
  
        // Create and register all sessions before notifying the clients. We do
        // this so that the reference count is incremented for all new sessions
        // before the callbacks execute, to prevent a potential case in which a
        // callback stops its session immediately which could cause the reference
        // count to drop the zero before all clients receive their session object.
        if (!pending_.empty()) {
          size_t count = pending_.size();
          std::unique_ptr<LowEnergyDiscoverySession> new_sessions[count];
          std::generate(new_sessions, new_sessions + count,
                        [this] { return AddSession(); });
          for (size_t i = 0; i < count; i++) {
            auto callback = std::move(pending_.front());
            pending_.pop();
  
            callback(std::move(new_sessions[i]));
          }
        }
        ZX_DEBUG_ASSERT(pending_.empty());
        return;
      case hci::LowEnergyScanner::ScanStatus::kStopped:
        bt_log(TRACE, "gap-le", "stopped scanning");
  
        cached_scan_results_.clear();
  
        // Some clients might have requested to start scanning while we were
        // waiting for it to stop. Restart active scanning if that is the case.
        // Otherwise start a background scan, if enabled.
        if (!pending_.empty()) {
          bt_log(TRACE, "gap-le", "initiate active scan");
          StartActiveScan();
        } else if (background_scan_enabled_) {
          bt_log(TRACE, "gap-le", "initiate background scan");
          StartPassiveScan();
        }
        return;
      case hci::LowEnergyScanner::ScanStatus::kComplete:
        bt_log(SPEW, "gap-le", "end of scan period");
        cached_scan_results_.clear();
  
        // If |sessions_| is empty this is because sessions were stopped while the
        // scanner was shutting down after the end of the scan period. Restart the
        // scan as long as clients are waiting for it.
        if (!sessions_.empty() || !pending_.empty()) {
          bt_log(SPEW, "gap-le", "continuing periodic scan");
          StartActiveScan();
        } else if (background_scan_enabled_) {
          bt_log(SPEW, "gap-le", "continuing periodic background scan");
          StartPassiveScan();
        }
        return;
    }
  }
  
  void LowEnergyDiscoveryManager::StartScan(bool active) {
    auto cb = [self = weak_ptr_factory_.GetWeakPtr()](auto status) {
      if (self)
        self->OnScanStatus(status);
    };
  
    // TODO(armansito): A client that is interested in scanning nearby beacons and
    // calculating proximity based on RSSI changes may want to disable duplicate
    // filtering. We generally shouldn't allow this unless a client has the
    // capability for it. Processing all HCI events containing advertising reports
    // will both generate a lot of bus traffic and performing duplicate filtering
    // on the host will take away CPU cycles from other things. It's a valid use
    // case but needs proper management. For now we always make the controller
    // filter duplicate reports.
  
    // See Vol 3, Part C, 9.3.11 "Connection Establishment Timing Parameters".
    uint16_t interval, window;
    if (active) {
      interval = kLEScanFastInterval;
      window = kLEScanFastWindow;
    } else {
      interval = kLEScanSlowInterval1;
      window = kLEScanSlowWindow1;
      // TODO(armansito): Use the controller whitelist to filter advertisements.
    }
  
    // Since we use duplicate filtering, we stop and start the scan periodically
    // to re-process advertisements. We use the minimum required scan period for
    // general discovery (by default; |scan_period_| can be modified, e.g. by unit
    // tests).
    scanner_->StartScan(active, interval, window, true /* filter_duplicates */,
                        hci::LEScanFilterPolicy::kNoWhiteList, scan_period_, cb);
  }
  
  }  // namespace gap
  }  // namespace btlib
  