背景
Android Q以降から、セキュリティ強化の関係でWiFi接続の切り替えが制限された。
インターネットに接続する目的では接続先WiFiをプログラムから制御することができなくなり、デバイスとのP2P (ネットワークへの接続が不可)に限られるようになった。
さらに、接続したネットワークにアクセスできるのも接続したアプリ内に限られる。
この操作はWifiNetworkSpecifierを介して実行できるが、Switchの「スマートフォンに送る」から画像を取得しようとして、数時間ハマったので陥りやすい点をまとめた。
WiFi P2Pで忘れがちな点まとめ
- AndroidManifest.xmlに権限を設定
<maninfest ...>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
...
<application
...
android:usesCleartextTraffic="true"
>
...
- たまに権限の追加が反映されないため、その場合はAndroid StudioのBuild → Clean Projectをした後、エミュレータ or テスト端末から一度アンインストールする。
- 解説ページによって記載漏れしているが、
bindProcessToNetwork
をcallback内で実行。 - VPN接続していると、
bindProcessToNetwork
がfalseを返して失敗する(これがどこにも記載がなくてハマった) - そもそも一部OEM ROM (OPPO, OnePlus, Samsungの一部端末)でWiFiを維持できない不具合がある模様です(以下ページ; 未確認)
実際のコード(Kotlin)
別途AndroidManifest.xmlの設定もすること。
val wifiNetworkSpecifier = WifiNetworkSpecifier.Builder()
.setSsid(ssid)
.setWpa2Passphrase(password)
.build()
val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(wifiNetworkSpecifier)
.build()
// contextはapplicationContextなり、CurrentLocal.contextなりで取得
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
// これを実行することで、URL#openConnectionが全て設定したnetworkを介するようになる。
// VPNがONの場合など、実行に失敗するとfalseが返り、openConnectionも失敗する。
connectivityManager.bindProcessToNetwork(network)
CoroutineScope(Dispatchers.IO).launch {
val url = "http://example.com/"
val connection = URL(url).openConnection() as HttpURLConnection
// ここで、URL#openConnectionではなく、
// val connection = network.openConnection(URL(url)) as HttpURLConnection
// だと、接続できないため注意
try{
val statusCode = connection.responseCode
if(statusCode){
// 以下はhtmlなどのテキスト内容をそのまま取得する場合
val str = connection.inputStream.bufferedReader(Charsets.UTF_8)
.use { br ->
br.readLines().joinToString("")
}
println("Reponse: $str")
} else {
println("Request Failed: $status")
}
} catch(e: IOException) {
e.printStackTrace()
}
// 接続の解除。モバイル回線などPrimary回線につながるようになる
connectivityManager.bindProcessToNetwork(null)
// WiFi接続の解除。基本的には元々接続されていたWiFiに自動でつながるはず。
connectivityManager.unregisterNetworkCallback(this)
}
}
override fun onUnavailable() {
super.onUnavailable()
println("NetworkCallback: Unavailable")
}
override fun onLost(network: Network) {
super.onLost(network)
println("NetworkCallback: Lost")
}
}
connectivityManager.requestNetwork(
networkRequest,
networkCallback
)