読者です 読者をやめる 読者になる 読者になる

なら@はてなブログ

福岡で働くスマートフォンエンジニア(おっさん)のブログ。更新頻度がとにかく低いのが悩み。

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でした。

ConnectionPool.java

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.

ConnectionPool3.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でも違うし、ちょっと面倒)するように実装したところ、一部の端末でコールバックが飛んでこないという不具合が発生したため、上記のようにレガシーな方法をとりました。