📶

.NET MAUI (Android) でWiFi強度の取得をしてみる

2025/01/02に公開

はじめに

前回は携帯ネットワーク通信の電波強度について取得する手法についてお伝えしました。
https://zenn.dev/aynv/articles/2d1bf8f1625051

今回はWiFiの電波強度を取得する方法について考えます。
そして今回もAndroid固有のAPIを書くので、おおよそ対訳みたいな感じになります。

おことわり

プラットフォーム固有のAPIを使用するので、作成するcsファイルはすべて「Platform/Android」配下に置きます。また、余計な考慮をしたくないので、この説明中の「最小ターゲットAndroidフレームワーク」は「Android 10.0 (APIレベル29)」にしてあります。

WiFiの電波強度を取得する

今回登場するAPIは、ConnectivityManagerWifiManager、そしてNetworkRequestです。
また、ConnectivityManagerを利用する際に CHANGE_NETWORK_STATE の権限が必要になります。

権限を宣言する

MAUIでAndroidアプリ書いてる人ならだいたいわかるかと思いますがおさらいです。
MAUIではAndroidManifestへ権限の宣言をする際に2つの方法があります。
ひとつは直接 AndroidManifest.xml を書き換える方法、そしてプラットフォーム固有のクラスのどこかに属性をつけて宣言する方法です。今回は後者を選択します。

プラットフォーム固有のクラスのどこかに書けばよくて、例えば MainActivity.csMainApplication.cs に書いたりするとよいです。.NET MAUIのプロジェクトを新規作成したとき、MainApplication.cs に既に ACCESS_NETWORK_STATE の権限が書かれているかと思うので、それにあわせるのがよさそうです。

ぶっちゃけコード上ならどこに書いても大して変わらないですが、とりあえず並べておきます。

MainApplication.cs
using Android.App;
using Android.Runtime;

// ここで宣言しておく
[assembly: UsesPermission(Android.Manifest.Permission.AccessNetworkState)]
[assembly: UsesPermission(Android.Manifest.Permission.ChangeNetworkState)]

namespace MauiApp3;
[Application]
public class MainApplication : MauiApplication
{
    public MainApplication(IntPtr handle, JniHandleOwnership ownership)
        : base(handle, ownership)
    {
    }

    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

AndroidManifestに書くようなのはMAUIではコード上にも書けるのは便利ですが、うまいことやらないとxmlファイルにもcsファイルにも書いてるといったことが起きるので気を付けたいところ。

方法1: コールバックを使う

ConnectivityManager.NetworkCallbackクラスを継承したクラスを作り、そこで情報を受け取るという方法があります。

WiFiStrengthWatcher.cs
using Android.Net;

public class WiFiStrengthWatcher
{
    private class NetworkCallBack : ConnectivityManager.NetworkCallback
    {
        public event EventHandler<WiFiCapabilitiesChangedEventArgs>? CapabilitiesChanged;

        public override void OnCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities)
        {
            var isConnected = networkCapabilities.HasTransport(TransportType.Wifi);
            var strength = networkCapabilities.SignalStrength;

            this.CapabilitiesChanged?.Invoke(this, new WiFiCapabilitiesChangedEventArgs(isConnected, strength);
        }
    }

    public class WiFiCapabilitiesChangedEventArgs : EventArgs
    {
        public bool IsConnected { get; }
        public int Strength { get; }

        public WiFiCapabilitiesChangedEventArgs(bool isConnected, int strength)
        {
            this.IsConnected = isConnected;
            this.Strength = strength;
        }
    }
}

ConnectivityManager.NetworkCallback クラスを継承した NetworkCallBack を用意し、このあとの実装でコールバックとして使うように定義しました。
今回は、コールバックを受けた際にイベントを使って転送するような形を取っておきました。

作った NetworkCallback クラスを変数として持ち、イベントを購読しつつコールバック登録をします。

WiFiStrengthWatcher.cs
using Android.Content;
using Android.Net;
using Android.Net.Wifi;

private readonly ConnectivityManager? connectivityManager;
private readonly WifiManager? wifiManager;

private NetworkCallBack networkCallBack = new();

public WiFiStrengthWatcher(MainActivity mainActivity)
{
    this.connectivityManager = mainActivity.GetSystemService(Context.ConnectivityService) as ConnectivityManager;
    this.wifiManager = mainActivity.GetSystemService(Context.WifiService) as WifiManager;
}

public void Listen()
{
    var networkRequestBuilder = new NetworkRequest.Builder();
    // 携帯ネットワーク通信とWiFiを受け取れるようにしておく
    var networkRequest = networkRequestBuilder.AddCapability(NetCapability.Internet)
        ?.AddTransportType(TransportType.Cellular)
        ?.AddTransportType(TransportType.Wifi)
        ?.Build();

    if (networkRequest is null)
    {
        return;
    }

    this.networkCallBack.CapabilitiesChanged += OnCapabilitiesChanged;
    this.connectivityManager?.RequestNetwork(networkRequest, this.networkCallBack);
}

WiFiStrengthWatcher クラスで購読するイベントのメソッドは以下のような感じにしました。
Android API Levelが30以上の場合、WiFiManager のインスタンスメソッド CalculateSignalLevelを、そうでない場合は WiFiManager静的メソッドCalculateSignalLevelを使います。どっちも同じメソッド名ですが、呼び出し方が違うのがまあ面倒。

WiFiStrengthWatcher.cs
private void OnCapabilitiesChanged(object? sender, WiFiCapabilitiesChangedEventArgs e)
{
    var currentLevel = -1;

    if (e.IsConnected)
    {
        // Android API Level によって取得方法が異なる
        if (OperatingSystem.IsAndroidVersionAtLeast(30))
        {
            currentLevel = this.wifiManager?.CalculateSignalLevel(e.Strength) ?? -1;
        }
        else
        {
            // とりあえず 5 段階くらいで
            currentLevel = WifiManager.CalculateSignalLevel(e.Strength, 5);
        }
    }

    Console.WriteLine($"WiFi Connected: {e.IsConnected}, Strength: {currentLevel}");
}

これでとりあえず大丈夫かと思います。
コールバックの登録をする際、networkRequestBuilder.AddCapabilityTransportType.Cellular も登録しましたが、こうすることでWiFiが切断されて携帯ネットワーク通信に移行した際にもコールバックが呼ばれるようになります。

ということは、TransportType.Cellular を登録せずWiFiが切断された場合、コールバックは呼び出されません。さらに、TransportType.Cellular を登録したとて、携帯ネットワーク通信が何らかの理由で使えなかった場合も呼び出されないのです。さあ困った。

方法2: タイマーなどを使って監視し続ける

コールバックを諦めるパターンです。何でも良いのでタイマーを使って時間になったら接続状態を確認しに行く方法です。とりあえずこれならすべてのネットワークが使えなかったとしてもWiFiの接続状態を取得することができます。

何でも良いんですが、今回はReactivePropertyの機能であるReactiveTimerを使います。いろいろある中では手っ取り早く使えるので重宝しています。
https://www.nuget.org/packages/ReactiveProperty/

WiFiStrengthWatcher.cs
using Android.Content;
using Android.Net;
using Android.Net.Wifi;
using Reactive.Bindings;

private readonly ConnectivityManager? connectivityManager;
private readonly WifiManager? wifiManager;
// とりあえず 1 秒単位で取るようにしておきます
private readonly ReactiveTimer watchTimer = new(TimeSpan.FromSeconds(1));

public WiFiStrengthWatcher(MainActivity mainActivity)
{
    this.connectivityManager = mainActivity.GetSystemService(Context.ConnectivityService) as ConnectivityManager;
    this.wifiManager = mainActivity.GetSystemService(Context.WifiService) as WifiManager;
    this.watchTimer.Subscribe(_ => Watch());
}

public void Listen()
{
    this.watchTimer.Start();
}

(結構いい加減なので、ちゃんと後始末とかしてくださいね)

タイマーが発動したときに登録してある Watch メソッドは以下の通り。先ほどコールバッククラス内でもらっていた情報を、このメソッド内で取得しに行くようにしています。

WiFiStrengthWatcher.cs
private void Watch()
{
    // 現在アクティブなネットワークを取得し…
    var currentNetwork = this.connectivityManager?.ActiveNetwork;
    // ネットワーク情報を取得します。
    // もしアクティブなネットワークがなければ、
    // currentNetwork は null なので、ここも null です
    var capabilities = this.connectivityManager?.GetNetworkCapabilities(currentNetwork);
    // WiFi が有効だよねを確認
    var isConnected = capabilities?.HasTransport(TransportType.Wifi) ?? false;
}

ここまでやれば、capabilitiesがコールバックで得たときと同じ NetworkCapabilities なので、電波強度の取り方は同じようにできます。

var currentLevel = -1;

if (isConnected)
{
    // Android API Level によって取得方法が異なる
    if (OperatingSystem.IsAndroidVersionAtLeast(30))
    {
        // isConnected で null チェックは済んでいるはず
        currentLevel = this.wifiManager?.CalculateSignalLevel(capabilities!.SignalStrength) ?? -1;
    }
    else
    {
        currentLevel = WifiManager.CalculateSignalLevel(capabilities!.SignalStrength, 5);
    }
}

こんな感じでやっておけば大丈夫そうです。すべてのネットワークが使えなかったとしても、これなら確実に状態を取得できるようになったはずです。

おしまい

Androidにおいて、以上2つの方法のどちらかを使えば、WiFiの状態と電波強度を取得することができるようになります。
ACCESS_WIFI_STATE 権限を宣言することでさらに情報を取得することはできるかと思いますが、なくてもここまで取得できるなら充分かなーとは思います。

先日の記事で携帯ネットワーク通信の電波強度をとりましたが、組み合わせることで両方の状態を監視することができますので、ぜひお試しください。

参考

https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ja

Discussion