Android(5.0以上) + OkHttpでWi-Fi接続中でもモバイルネットワークが使われる
Androidアプリを開発していて、接続しているネットワーク状態に応じて処理を切り替えるような実装をすることは多いと思います。
よくあるのが大容量コンテンツのダウンロード処理をWi-Fi接続中だけ行うとか、Wi-Fiかモバイルネットワークかで画像ファイルの解像度を切り替えたりとか。
今回、そのような実装をしていて気になる動きがあったのでメモ書きです。
具体的な事象はタイトルの通りですが、Android 5.0以上の端末でWi-Fi接続中にもかかわらず、OkHttp
を使用した通信にモバイルネットワークが使用される場合がありました。
執筆時点で使用していたOkHttp
のバージョンは3.6.0
です。
問題発生時の状況
Javaコード上でネットワーク状態を取得すると、Wi-Fi接続中と判定される状態です。
ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); boolean isWiFiConnected = activeNetwork != null && activeNetwork.isConnected() && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI; Log.i(TAG, String.format("isWiFiConnected : %b", isWiFiConnected)); // true
しかし、サーバ側でログを見ていると、Wi-Fi接続中であってもアクセス元がキャリア網のグローバルIPアドレスになっていました。発生する条件としては、アプリ起動中にネットワークがモバイルネットワークからWi-Fiに切り替わった場合です。
原因
状況から推測することは難しくないと思いますが、モバイルネットワークのコネクションが生きていて、Wi-Fiに切り替わった後も継続して使用されていました。
原因はOkHttpClient
で使用しているConnectionPool
でした。
Create a new connection pool with tuning parameters appropriate for a single-user application. The tuning parameters in this pool are subject to change in future OkHttp releases. Currently this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
ConnectionPool
は3.6.0
時点の実装では「5つのidle connectionを保持し、5分間使用しないとプールが破棄される」という実装になっています。
また、通常であれば、ネットワーク状態が切り替わったタイミングでコネクションが使用不可になるため、プールは破棄されて新たにSocketが作られるようになっていました。
ただ、Android 5.0からアプリケーションが複数のNetwork Interfaceを使用できるようになったため、Wi-Fiが接続されてもモバイルネットワーク用のNetwork Interfaceがそのまま接続可能な状態になっているようでした。そのため、アプリがフォアグラウンドで頻繁にAPIコールをする場合などは、5分間の未使用という条件が満たされないので、ConnectionPool
は破棄されず、モバイルネットワークのConnection
を維持したままネットワーク通信が行われていたと考えられます。
なお、Wi-Fiからモバイルネットワークに切り替わった場合は特に問題なく(モバイルネットワークに切り替わる=Wi-Fi用のNetwork Interfaceはコネクションロストしている)、Wi-Fiから別のWi-Fiに切り替わった場合も問題ありません(同じNetwork Interfaceを使っているので、元Wi-Fiのコネクションはロスト)。
対応したこと
ということで、以下のような対応をいれることで問題は回避できたのですが、もっといい方法があれば知りたいところです。
ConnectivityManager.CONNECTIVITY_ACTION
を受ける適当なReceiver
を作成して
private OkHttpClient mOkHttpClient; private boolean mIsWiFiConnected; @Override public void onReceive(Context context, Intent intent) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { return; } ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); boolean isWiFiConnected = activeNetwork != null && activeNetwork.isConnected() && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI; // Clear pooled connections on network state changed to Wi-Fi from others. if (isWiFiConnected && !mIsWiFiConnected) { mOkHttpClient.connectionPool().evictAll(); } mIsWiFiConnected = isWiFiConnected; }
余談ですが、Build.VERSION.SDK_INT
を判定してOSバージョンに応じたネットワーク変更検知(NetworkCallbacks
を使うやつ。LとMでも違うし、ちょっと面倒)するように実装したところ、一部の端末でコールバックが飛んでこないという不具合が発生したため、上記のようにレガシーな方法をとりました。