さて。「iPhoneと同じように、Androidのスマフォで、ネットへのHTTP接続は3G経由で行い、ローカルネットワークへのHTTP接続はWi-Fi経由で行う」ということがやりたい、いや、やりたかったポンコツな通信処理があったりして、そのソースコードにあまり手を入れずにAndroid 4.xに加えてAndroid 5.0をサポートできないだろうかと調査してみました。
スマフォがWi-Fiで接続したローカルネットワークにインターネットへのゲートウェイがない場合、Android 4.xまではモバイルデータ通信やWi-Fiの切り替えをオンオフしながら使うしかなかったようなので微妙な感じでしたが、Android 5.0では複数ネットワークへの同時接続がようやくサポートされたのでこれをなんとかサポートしてみたい、というお題です。
巷では、スマフォを4.xから5.0にアップデートしたらWi-Fiに繋がらなくなった(怒)、とかいう話も聞くのでやっておいて損することはなさそうです。
なお、当方はAndroidは苦手項目なので、以下は完成品ではない断片的情報の羅列になっています。個人の趣味の範囲でやるにしても、開発環境も実機も回線も持っていない上に、完成したコードを晒すと後で他で何か政治的な問題が起きる可能性もあるので、ここではあえてボカした次第です。あらかじめご了承ください。
では、両方をサポートできない元のコードはこちら。
HttpGet request = new HttpGet("http://192.168.0.1/"); HttpClient client = new DefaultHttpClient(); HttpResponse response = client.execute(request);
非常にお手軽ですが、Android 5.0で動かすと生成されたクライアントの通信先がデフォルトゲートウェイ(3G経由)を向いてしまっていて、このままだとリクエストを投げてもWi-Fi側には届きません。たぶん。エラー処理とかは省いていますが、通信タイムアウトなどが起きてIOExceptionなどを投げてくるのではないかと思います。
んなもんで、Wi-Fi側へ通信できるネットワークインターフェースを取得してそれに紐付いているソケットファクトリを引っ張り出して、そのソケットファクトリをHTTPクライアントがソケットを作るときに使ってもらうような仕組みにします。(実質的にこの一文が解答です)
まずは、カスタムのソケットファクトリを作ります。
このソケットファクトリでは、Wi-Fiのインターフェースを探すのにConnectivityManagerが必要で、すなわち遠回しにContextが必要なのでこのクラスのインスタンス生成時に渡せるようにしておきます。ついでに色々肩代わりしてもらうためにPlainSocketFactoryのソケットファクトリも用意しておきます。
HTTPの通信が始まる時にソケットを作ってくれと言われるのでそのタイミングで、5.0以降ならWi-Fiのソケットファクトリを探してそいつにソケットを作ってもらいます。4.xならPlainSocketFactoryに作ってもらいます。Wi-Fiのネットワーク探しはAndroid 5.0以降のAPIレベルを使う必要があるので、別なプライベートメソッドに出しておくと良さそうです。
public class CustomSocketFactory implements SocketFactory { private Context mContext; private PlainSocketFactory mFactory; public CustomSocketFactory(Context context) { mContext = context; mFactory = PlainSocketFactory.getSocketFactory(); } @Override public Socket connectSocket(省略) throws 省略 { return mFactory.connectSocket(省略); } @Override public Socket createSocket(省略) throws 省略 { if (mContext == null || Build.VERSION.SDK_INT < 21) { return mFactory.createSocket(省略); } else { return privateCreateSocket(省略); } } @Override public boolean isSecure(省略) throws 省略 { return false; } @TargetApi(21) private Socket privateCreateSocket(省略) throws 省略 { // 次で説明します } }
省略したところはお察しください。Eclipseとか、今時のIDEなら自動生成してくれます。
上記クラスのprivateCreateSocket(省略)メソッドの中は以下のようになります。これは、日本語で書かれた唯一の情報として見つけた、http://qiita.com/hishida/items/e43d5ada3d83c71ce953 を思いっきり参考にしました。助かりました。
ConnectivityManager manager = mContext.getSystemService(Context.CONNECTIVITY_SERVICE); Network[] networks = manager.getAllNetworks(); for (Network network : networks) { NetworkCapabilities capabilities = manager.getNetworkCapabilities(network); if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { SocketFactory factory = network.getSocketFactory(); return factory.createSocket(省略); } } return mFactory.createSocket(省略);
みたいな感じです。
PlainSocketFactoryで取得できるソケットファクトリとnetwork.getSocketFactory()で取得できるソケットファクトリとで、どちらも同じ型のSokcetを生成できるくせになぜかそれぞれ名前空間が違う型なので、上記のままだと不可解なコンパイルエラーが出ると思います。どっちかをインポートせずに名前空間込みのクラス名で型指定しないといけません。
つぎに、カスタムのHTTPクライアントを作ります。
カスタムのソケットファクトリをHTTPクライアントが使用するソケットファクトリに指定します。
public class CustomHttpClient extends DefaultHttpClient { private Context mContext; public CustomHttpClient(Context context) { mContext = context; } @Override protected ClientConnectionManager createClientConnectionManager() { SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", new CustomSocketFactory(mContext), 80)); return new SingleClientConnManager(getParams(), registry); } }
みたいな感じです。httpsのスキーマ登録は難しそうなので今回は見送りました。
最終的に利用する側のコードは、
HttpGet request = new HttpGet("http://192.168.0.1/"); HttpClient client = new CustomHttpClient(this); HttpResponse response = client.execute(request);
みたいな感じになります。
カスタムのHTTPクライアントに指定するコンテキストはアプリケーションのコンテキストなのかアクティビティのコンテキストなのかは慎重に考えたほうが良いかも知れません。
元のコードを含んでいるアプリをJDK 1.6でコンパイルしていた場合は、Android 5.0に対応させる都合によりJDK 1.7でコンパイルするように変更の必要があります。
ここで説明した方法論は、別のところで実際に実装して、Android 4.4の端末で旧来の動作と互換があることとAndroid 5.0の端末で新しい機能が動くことを、それぞれ確認済みです。
しかし、ここに挙げたソースコードについては、書きっぱなしで一度もコンパイルも実行もしていません。どこか間違っていたらごめんなさい。
長文、失礼いたしました。
残念ながらDefaultHttpClientはAPIレベル22で廃止になったようです。
そろそろ本気で「HttpURLConnectionに書き換えしろ」ということらしい。