🛜

AndroidシステムアプリのWi-Fi実装(Kotlin)

に公開

はじめに

こんにちは!KGモーターズ株式会社でエンジニアをしている中村です。

KGモーターズは、広島を拠点に1人乗り小型 EV mibot を開発しているスタートアップです。
最近東京拠点もでき、エンジニア採用を強化しているところです!

https://kg-m.jp/posts/tokyo-office

mibotの車載アプリではユーザーにWi-Fiの設定をしてもらう機能があるのですが、この機能の実装について調べたことを簡単にまとめておこうと思います。

https://developer.android.com/reference/android/net/wifi/WifiManager

Wi-Fi接続のON/OFF

WifiManager を利用することで、Wi-Fiの状態を直接切り替えることができます。
該当のAPIリファレンスは以下です。
https://developer.android.com/reference/android/net/wifi/WifiManager#isWifiEnabled()

実装は、現在のステータスを取得し、現在とは逆のステータスになるように制御してあげれば良いです。

val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager

val currentState: Boolean = wifiManager.isWifiEnabled
wifiManager.isWifiEnabled = !currentState

トグルをON/OFFにすると、右上のWi-Fiアイコンが連動し、SSIDも取得できます。ここまで実現するには、Callback を使ってイベントを購読し、ViewModel を経由して UI に通知する必要があります。この仕組みについては後述の監視の仕組みで説明します。

注意

APIレベル29以降では多くのAPIが非推奨になっています[1]
公式によると、以下のようにシステムの Wi-Fi 設定画面を開くことが推奨設定となっております。
関連の質問がstackoverflowにも存在しております。

真面目に従うと、Wi-Fiを有効/無効にする場合は以下のようになります。

val intent = Intent(Settings.Panel.ACTION_WIFI)
context.startActivity(intent)

ボタンを押した時に上記が実行されるようにすると、このような画面が出てくるはずです。

またACTION_WIFIACTION_WIFI_SETTINGSに変えると、OSのInternet設定に飛べます。

公式ドキュメントは開発ドキュメントのため「システムアプリなら使える」と書いてあるわけではないですが、AOSPのソースコードを見ることでシステムアプリや署名アプリではAPIが使用可能なことがわかります。

https://android.googlesource.com/platform/frameworks/base/+/8221b05/wifi/java/android/net/wifi/WifiManager.java

  • getter
    返り値がbooleanの場合はJavaBeans規約でgetXXXメソッドではなく、isXXXになります。
    public boolean isWifiEnabled() {
        return getWifiState() == WIFI_STATE_ENABLED;
    }
...
    public int getWifiState() {
        try {
            return mService.getWifiEnabledState();
        } catch (RemoteException e) {
            return WIFI_STATE_UNKNOWN;
        }
    }
  • setter
    public boolean setWifiEnabled(boolean enabled) {
        try {
            return mService.setWifiEnabled(enabled);
        } catch (RemoteException e) {
            return false;
        }
    }

Serviceに処理を委ねておりますが、Serviceを見てみると権限チェックが行われています。

https://android.googlesource.com/platform/frameworks/opt/net/wifi/%2B/23685b8604571ec623e539f4f9c66db65c9dde81/service/java/com/android/server/wifi/WifiServiceImpl.java#768

ちなみに通常アプリでは APIレベル29 以降では、このメソッドは常に false を返します。

This method was deprecated in API level 29.
Starting with Build.VERSION_CODES#Q, applications are not allowed to enable/disable Wi-Fi. Compatibility Note: For applications targeting Build.VERSION_CODES.Q or above, this API will always fail and return false. If apps are targeting an older SDK (Build.VERSION_CODES.P or below), they can continue to use this API.

現在接続しているWi-Fiの取得

現在どのSSIDに接続しているかは WifiManager.connectionInfo から取得できます。

https://developer.android.com/reference/android/net/wifi/WifiManager#getConnectionInfo()

WifiManager.connectionInfoWifiInfoが返ってきますが、WifiInfoで得られる情報は以下になります。

public WifiInfo getConnectionInfo ()

取得可能情報抜粋

Kotlin(プロパティ) Java(対応メソッド) ざっくり用途 / 注意
ssid getSSID() String 接続中SSID。位置情報なしで UNKNOWN_SSID になり得る
rssi getRssi() int 受信感度(dBm)。UIの電波バー、しきい値判定
frequency getFrequency() int 周波数(MHz)。2.4/5/6GHz 判定など
currentSecurityType getCurrentSecurityType() int OPEN/PSK/WPA3/OWE/Passpoint 等
macAddress getMacAddress() String 接続に使うMAC(ランダム化あり)。権限ないと 02:00:00:00:00:00
networkId getNetworkId() int 現在のNetwork ID。未接続/権限不足で -1
ipAddress getIpAddress() int API31で非推奨。IPは ConnectivityManager.getLinkProperties() を使う

スキャン

利用可能なWi-Fiネットワークを取得するには、wifiManager.startScan() を呼び出した上で、wifiManager.scanResultsWifiInfoのリストを参照することができます。

https://developer.android.com/develop/connectivity/wifi/wifi-scan?hl=ja

val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager

val success = wifiManager.startScan()
if (!success) {
    // スキャン失敗の処理
}
val results = wifiManager.scanResults

このresultsの中にScanResultのリストが入っているので、あとはビジネスロジックでフィルター処理や並び替えなどをすると良いかと思います。

監視の仕組み

Androidでは、Wi-Fiやネットワークの状態が変化するとOSがブロードキャストやコールバックを発行します。これをアプリ側で受け取り、ViewModel を経由して UI に通知することで、監視の仕組みを構築します。

具体的には、ConnectivityManagerNetworkCallbackWifiManager のブロードキャストを利用しています。

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

公式ドキュメントでも説明されている通り、アプリはシステムから通知されるイベントを通じて「ネットワークが利用可能か」「インターネットに到達できるか」といった情報を検知できます。

このプロジェクトでは以下の流れになっています。

  1. OSのイベントを購読
    • ConnectivityManager.registerNetworkCallback で Wi-Fi の接続・切断をフック[2]
    • BroadcastReceiver で Wi-Fi の状態変化やスキャン結果を受け取る。
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network : Network) {
        // 接続開始を検知
        // → Flowにイベントを流す処理を呼ぶ
    }

    override fun onLost(network : Network) {
        // 切断を検知
        // → Flowにイベントを流す処理を呼ぶ
    }

    override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
        // インターネットに出られるかどうか
        // → Flowにイベントを流す処理を呼ぶ
    }
})

  1. イベント発生時に状態を集約
    • 1の このイベントが発生した時の処理を書く で呼び出す処理
    • Wi-Fi有効/無効、接続中のSSID、周囲のネットワーク一覧を取得
  2. Flowに流す
    • MutableSharedFlow に値を流す。
private val _wifiStatusEvents = MutableSharedFlow<Boolean>()
val wifiStatusEvents: SharedFlow<Boolean> = _wifiStatusEvents.asSharedFlow()
...
_wifiStatusEvents.emit(event)
  1. 購読しているViewModelがキャッチする

つまり、「OSイベント → Callback/Receiver → Flow → ViewModel.collect → UI更新」というパイプラインを構築しているため、リアルタイムで監視ができる、というわけです。

おわりに

AndroidのWi-Fi制御はバージョンごとに制約が強まっており、一般アプリからはほとんど触れられなくなっていますが、システムアプリであればまだ直接的に操作できます!

今後はAOSPの進化やOEM独自拡張によりさらに仕様が変わっていく可能性があるため、実装の際はAPIレベルとデバイス要件を常に確認することをおすすめします。

また、KGモーターズではこのように車載のAndroidアプリを一緒に開発する仲間を募集していますので、mibotのソフトウェア開発に興味のある方はぜひお気軽にご連絡ください!

https://kg-m.jp/recruit/android-development-engineer

脚注
  1. This method was deprecated in API level 29. ↩︎

  2. https://developer.android.com/develop/connectivity/network-ops/reading-network-state?hl=ja#listening-events ↩︎

KGモーターズ Tech Blog

Discussion