Commit ce4a1d34 authored by David Parks's avatar David Parks
Browse files

Bug 1810421: Only scan wifi when the network changes or when on mobile r=necko-reviewers,kershaw

Simplifies the concurrent operations of wifi scanning and reduces the
frequency in common cases.  Wifi scanning when on mobile is reduced from
every 5 seconds to every minute.  Wifi scans will also happen whenever
a new listener is registered.

Differential Revision: https://phabricator.services.mozilla.com/D176200
parent 7043482e
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -318,7 +318,7 @@ NetworkGeolocationProvider.prototype = {
      this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(
        Ci.nsIWifiMonitor
      );
      this.wifiService.startWatching(this);
      this.wifiService.startWatching(this, false);
    }

    this.resetTimer();
+7 −0
Original line number Diff line number Diff line
@@ -12693,6 +12693,13 @@
  value: false
  mirror: always

# Wifi-scan polling period, in ms, when on a mobile network.
# A value of 0 indicates that no polling should be done.
- name: network.wifi.scanning_period
  type: RelaxedAtomicUint32
  value: 60000
  mirror: always

#---------------------------------------------------------------------------
# Prefs starting with "nglayout."
#---------------------------------------------------------------------------
+368 −0
Original line number Diff line number Diff line
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include "nsIWifiListener.h"
#include "nsWifiMonitor.h"
#include "nsWifiAccessPoint.h"
#include "WifiScanner.h"
#include "nsCOMPtr.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "nsIObserverService.h"
#include "nsINetworkLinkService.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"

#if defined(XP_WIN) && defined(_M_IX86)
#  include <objbase.h>  // STDMETHODCALLTYPE
#endif

// Tests that wifi scanning happens on the right network change events,
// and that wifi-scan polling is operable on mobile networks.

using ::testing::AtLeast;
using ::testing::Exactly;

static mozilla::LazyLogModule gLog("TestWifiMonitor");
#define LOGI(x) MOZ_LOG(gLog, mozilla::LogLevel::Info, x)
#define LOGD(x) MOZ_LOG(gLog, mozilla::LogLevel::Debug, x)

namespace mozilla {

// Timeout if no updates are received for 1s.
static const uint32_t WIFI_SCAN_TEST_RESULT_TIMEOUT_MS = 1000;

// ID counter, used to make sure each call to GetAccessPointsFromWLAN
// returns "new" access points.
static int gCurrentId = 0;

static uint32_t gNumScanResults = 0;

template <typename T>
bool IsNonZero(const T&) {
  return true;
}

template <>
bool IsNonZero<int>(const int& aVal) {
  return aVal != 0;
}

struct LinkTypeMobility {
  const char* mLinkType;
  bool mIsMobile;
};

class MockWifiScanner : public WifiScanner {
 public:
  MOCK_METHOD(nsresult, GetAccessPointsFromWLAN,
              (nsTArray<RefPtr<nsIWifiAccessPoint>> & aAccessPoints),
              (override));
};

class MockWifiListener : public nsIWifiListener {
  virtual ~MockWifiListener() = default;

 public:
  NS_DECL_THREADSAFE_ISUPPORTS
#if defined(XP_WIN) && defined(_M_IX86)
  MOCK_METHOD(nsresult, OnChange,
              (const nsTArray<RefPtr<nsIWifiAccessPoint>>& accessPoints),
              (override, Calltype(STDMETHODCALLTYPE)));
  MOCK_METHOD(nsresult, OnError, (nsresult error),
              (override, Calltype(STDMETHODCALLTYPE)));
#else
  MOCK_METHOD(nsresult, OnChange,
              (const nsTArray<RefPtr<nsIWifiAccessPoint>>& accessPoints),
              (override));
  MOCK_METHOD(nsresult, OnError, (nsresult error), (override));
#endif
};

NS_IMPL_ISUPPORTS(MockWifiListener, nsIWifiListener)

class TestWifiMonitor : public ::testing::Test {
 public:
  TestWifiMonitor() {
    mObs = mozilla::services::GetObserverService();
    MOZ_ASSERT(mObs);

    nsresult rv;
    nsCOMPtr<nsINetworkLinkService> nls =
        do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    EXPECT_TRUE(nls);
    rv = nls->GetLinkType(&mOrigLinkType);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    rv = nls->GetIsLinkUp(&mOrigIsLinkUp);
    EXPECT_TRUE(NS_SUCCEEDED(rv));
    rv = nls->GetLinkStatusKnown(&mOrigLinkStatusKnown);
    EXPECT_TRUE(NS_SUCCEEDED(rv));

    // Reduce wifi-polling interval.  0 turns polling off.
    mOldScanInterval = Preferences::GetInt(WIFI_SCAN_INTERVAL_MS_PREF);
    Preferences::SetInt(WIFI_SCAN_INTERVAL_MS_PREF, 10);
  }

  ~TestWifiMonitor() {
    Preferences::SetInt(WIFI_SCAN_INTERVAL_MS_PREF, mOldScanInterval);

    // Restore network link type
    const char* linkType = nullptr;
    switch (mOrigLinkType) {
      case nsINetworkLinkService::LINK_TYPE_UNKNOWN:
        linkType = NS_NETWORK_LINK_TYPE_UNKNOWN;
        break;
      case nsINetworkLinkService::LINK_TYPE_ETHERNET:
        linkType = NS_NETWORK_LINK_TYPE_ETHERNET;
        break;
      case nsINetworkLinkService::LINK_TYPE_USB:
        linkType = NS_NETWORK_LINK_TYPE_USB;
        break;
      case nsINetworkLinkService::LINK_TYPE_WIFI:
        linkType = NS_NETWORK_LINK_TYPE_WIFI;
        break;
      case nsINetworkLinkService::LINK_TYPE_MOBILE:
        linkType = NS_NETWORK_LINK_TYPE_MOBILE;
        break;
      case nsINetworkLinkService::LINK_TYPE_WIMAX:
        linkType = NS_NETWORK_LINK_TYPE_WIMAX;
        break;
    }
    EXPECT_TRUE(linkType);
    mObs->NotifyObservers(nullptr, NS_NETWORK_LINK_TYPE_TOPIC,
                          NS_ConvertUTF8toUTF16(linkType).get());

    const char* linkStatus = nullptr;
    if (mOrigLinkStatusKnown) {
      if (mOrigIsLinkUp) {
        linkStatus = NS_NETWORK_LINK_DATA_UP;
      } else {
        linkStatus = NS_NETWORK_LINK_DATA_DOWN;
      }
    } else {
      linkStatus = NS_NETWORK_LINK_DATA_UNKNOWN;
    }
    EXPECT_TRUE(linkStatus);
    mObs->NotifyObservers(nullptr, NS_NETWORK_LINK_TOPIC,
                          NS_ConvertUTF8toUTF16(linkStatus).get());
  }

  void SetUp() override {
    mWifiMonitor = MakeRefPtr<nsWifiMonitor>(MakeUnique<MockWifiScanner>());
    EXPECT_TRUE(!mWifiMonitor->IsPolling());

    mWifiListener = new MockWifiListener();
    LOGI(("monitor: %p | scanner: %p | listener: %p", mWifiMonitor.get(),
          mWifiMonitor->mWifiScanner.get(), mWifiListener.get()));
  }

  void TearDown() override {
    ::testing::Mock::VerifyAndClearExpectations(
        mWifiMonitor->mWifiScanner.get());
    ::testing::Mock::VerifyAndClearExpectations(mWifiListener.get());

    // Manually disconnect observers so that the monitor can be destroyed.
    // This would normally be done on xpcom-shutdown but that is sent after
    // the tests run, which is too late to avoid a gtest memory-leak error.
    mWifiMonitor->Close();

    mWifiMonitor = nullptr;
    mWifiListener = nullptr;
    gCurrentId = 0;
  }

 protected:
  // Fn must be the type of a parameterless function that returns a boolean
  // indicating success.
  template <typename Card1, typename Card2, typename Card3, typename Fn>
  void CheckAsync(Card1&& nScans, Card2&& nChanges, Card3&& nErrors, Fn fn) {
    // Only add WillRepeatedly handler if scans is more than 0, to avoid a
    // VERY LOUD gtest warning.
    if (IsNonZero(nScans)) {
      EXPECT_CALL(
          *static_cast<MockWifiScanner*>(mWifiMonitor->mWifiScanner.get()),
          GetAccessPointsFromWLAN)
          .Times(nScans)
          .WillRepeatedly(
              [](nsTArray<RefPtr<nsIWifiAccessPoint>>& aAccessPoints) {
                EXPECT_TRUE(!NS_IsMainThread());
                EXPECT_TRUE(aAccessPoints.IsEmpty());
                nsWifiAccessPoint* ap = new nsWifiAccessPoint();
                // Signal will be unique so we won't match the prior access
                // point list.
                ap->mSignal = gCurrentId++;
                aAccessPoints.AppendElement(RefPtr(ap));
                return NS_OK;
              });
    } else {
      EXPECT_CALL(
          *static_cast<MockWifiScanner*>(mWifiMonitor->mWifiScanner.get()),
          GetAccessPointsFromWLAN)
          .Times(nScans);
    }

    if (IsNonZero(nChanges)) {
      EXPECT_CALL(*mWifiListener, OnChange)
          .Times(nChanges)
          .WillRepeatedly(
              [](const nsTArray<RefPtr<nsIWifiAccessPoint>>& aAccessPoints) {
                EXPECT_TRUE(NS_IsMainThread());
                EXPECT_EQ(aAccessPoints.Length(), 1u);
                ++gNumScanResults;
                return NS_OK;
              });
    } else {
      EXPECT_CALL(*mWifiListener, OnChange).Times(nChanges);
    }

    if (IsNonZero(nErrors)) {
      EXPECT_CALL(*mWifiListener, OnError)
          .Times(nErrors)
          .WillRepeatedly([](nsresult aError) {
            EXPECT_TRUE(NS_IsMainThread());
            return NS_OK;
          });
    } else {
      EXPECT_CALL(*mWifiListener, OnError).Times(nErrors);
    }

    EXPECT_TRUE(fn());
  }

  void CheckStartWatching(bool aShouldPoll) {
    LOGI(("CheckStartWatching | aShouldPoll: %s",
          aShouldPoll ? "true" : "false"));
    CheckAsync(aShouldPoll ? AtLeast(1) : Exactly(1) /* nScans */,
               aShouldPoll ? AtLeast(1) : Exactly(1) /* nChanges */,
               0 /* nErrors */, [&] {
                 return NS_SUCCEEDED(mWifiMonitor->StartWatching(
                            mWifiListener, aShouldPoll)) &&
                        WaitForScanResult(1);
               });
  }

  void CheckStopWatching() {
    LOGI(("CheckStopWatching"));
    CheckAsync(0 /* nScans */, 0 /* nChanges */, 0 /* nErrors */, [&] {
      return NS_SUCCEEDED(mWifiMonitor->StopWatching(mWifiListener));
    });
  }

  void CheckNotifyAndNoScanResults(const char* aTopic, const char16_t* aData) {
    LOGI(("CheckNotifyAndNoScanResults: (%s, %s)", aTopic,
          NS_ConvertUTF16toUTF8(aData).get()));
    CheckAsync(0 /* nScans */, 0 /* nChanges */, 0 /* nErrors */, [&] {
      return NS_SUCCEEDED(mObs->NotifyObservers(nullptr, aTopic, aData));
    });
  }

  void CheckNotifyAndWaitForScanResult(const char* aTopic,
                                       const char16_t* aData) {
    LOGI(("CheckNotifyAndWaitForScanResult: (%s, %s)", aTopic,
          NS_ConvertUTF16toUTF8(aData).get()));
    CheckAsync(
        AtLeast(1) /* nScans */, AtLeast(1) /* nChanges */, 0 /* nErrors */,
        [&] {
          return NS_SUCCEEDED(mObs->NotifyObservers(nullptr, aTopic, aData)) &&
                 WaitForScanResult(1);
        });
  }

  void CheckNotifyAndWaitForPollingScans(const char* aTopic,
                                         const char16_t* aData) {
    LOGI(("CheckNotifyAndWaitForPollingScans: (%s, %s)", aTopic,
          NS_ConvertUTF16toUTF8(aData).get()));
    CheckAsync(
        AtLeast(2) /* nScans */, AtLeast(2) /* nChanges */, 0 /* nErrors */,
        [&] {
          // Wait for more than one scan.
          return NS_SUCCEEDED(mObs->NotifyObservers(nullptr, aTopic, aData)) &&
                 WaitForScanResult(2);
        });
  }

  void CheckMessages(bool aShouldPoll) {
    // NS_NETWORK_LINK_TOPIC messages should cause a new scan.
    const char* kLinkTopicDatas[] = {
        NS_NETWORK_LINK_DATA_UP, NS_NETWORK_LINK_DATA_DOWN,
        NS_NETWORK_LINK_DATA_CHANGED, NS_NETWORK_LINK_DATA_UNKNOWN};

    auto checkFn = aShouldPoll
                       ? &TestWifiMonitor::CheckNotifyAndWaitForPollingScans
                       : &TestWifiMonitor::CheckNotifyAndWaitForScanResult;

    for (const auto& data : kLinkTopicDatas) {
      CheckStartWatching(aShouldPoll);
      EXPECT_EQ(mWifiMonitor->IsPolling(), aShouldPoll);
      (this->*checkFn)(NS_NETWORK_LINK_TOPIC,
                       NS_ConvertUTF8toUTF16(data).get());
      EXPECT_EQ(mWifiMonitor->IsPolling(), aShouldPoll);
      CheckStopWatching();
      EXPECT_TRUE(!mWifiMonitor->IsPolling());
    }

    // NS_NETWORK_LINK_TYPE_TOPIC should cause wifi scan polling iff the topic
    // says we have switched to a mobile network (LINK_TYPE_MOBILE or
    // LINK_TYPE_WIMAX) or we are polling the wifi-scanner (aShouldPoll).
    const LinkTypeMobility kLinkTypeTopicDatas[] = {
        {NS_NETWORK_LINK_TYPE_UNKNOWN, false /* mIsMobile */},
        {NS_NETWORK_LINK_TYPE_ETHERNET, false},
        {NS_NETWORK_LINK_TYPE_USB, false},
        {NS_NETWORK_LINK_TYPE_WIFI, false},
        {NS_NETWORK_LINK_TYPE_WIMAX, true},
        {NS_NETWORK_LINK_TYPE_MOBILE, true}};

    for (const auto& data : kLinkTypeTopicDatas) {
      bool shouldPoll = (aShouldPoll || data.mIsMobile);
      checkFn = shouldPoll ? &TestWifiMonitor::CheckNotifyAndWaitForPollingScans
                           : &TestWifiMonitor::CheckNotifyAndNoScanResults;

      CheckStartWatching(aShouldPoll);
      (this->*checkFn)(NS_NETWORK_LINK_TYPE_TOPIC,
                       NS_ConvertUTF8toUTF16(data.mLinkType).get());
      EXPECT_EQ(mWifiMonitor->IsPolling(), shouldPoll);
      CheckStopWatching();
      EXPECT_TRUE(!mWifiMonitor->IsPolling());
    }
  }

  bool WaitForScanResult(uint32_t nScansToWaitFor) {
    uint32_t oldScanResults = gNumScanResults;
    auto startTime = TimeStamp::Now();

    return mozilla::SpinEventLoopUntil(
        "TestWifiMonitor::WaitForScanResult"_ns, [&]() {
          LOGD(
              ("gNumScanResults: %d | oldScanResults: %d | "
               "nScansToWaitFor: %d | timeMs: %d",
               (int)gNumScanResults, (int)oldScanResults, (int)nScansToWaitFor,
               (int)(TimeStamp::Now() - startTime).ToMilliseconds()));
          return gNumScanResults >= (oldScanResults + nScansToWaitFor) ||
                 (TimeStamp::Now() - startTime).ToMilliseconds() >
                     WIFI_SCAN_TEST_RESULT_TIMEOUT_MS * nScansToWaitFor;
        });
  }

  RefPtr<nsWifiMonitor> mWifiMonitor;
  nsCOMPtr<nsIObserverService> mObs;

  RefPtr<MockWifiListener> mWifiListener;

  int mOldScanInterval;
  uint32_t mOrigLinkType = 0;
  bool mOrigIsLinkUp = false;
  bool mOrigLinkStatusKnown = false;
};

TEST_F(TestWifiMonitor, WifiScanNoPolling) { CheckMessages(false); }

TEST_F(TestWifiMonitor, WifiScanPolling) { CheckMessages(true); }

}  // namespace mozilla
+13 −0
Original line number Diff line number Diff line
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

LOCAL_INCLUDES += ["/netwerk/wifi"]

UNIFIED_SOURCES += [
    "TestWifiMonitor.cpp",
]

FINAL_LIBRARY = "xul-gtest"
+2 −0
Original line number Diff line number Diff line
@@ -56,6 +56,8 @@ elif CONFIG["NECKO_WIFI_DBUS"]:
    ]
    CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"]

TEST_DIRS += ["gtest"]

FINAL_LIBRARY = "xul"

include("/ipc/chromium/chromium-config.mozbuild")
Loading