Androidアプリでモバイルデータ通信とWi-Fiを同時に使う

## はじめに
Androidアプリでモバイルデータ通信とWi-FIを同時に使用します。
(TCPサーバを立てたり、GETリクエストを送ったりします)
本記事ではJavaを使用しAPIレベルは21以上とします。開発者オプションが必須です。

## 前提
### 開発者オプションの設定

「開発者向けオプション」→「ネットワーク」→「モバイルデータを常にON」を「オン」にすること。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/e381edad-0eb0-972b-e497-8e91d93dc373.png)

### permissionについて
INTERNET、ACCESS_NETWORK_STATE、ACCESS_WIFI_STATE、CHANGE_NETWORK_STATEを使用する。
いずれも保護レベルが「normal」のpermissionである。

```AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.valuesccg.testapplication">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="Android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />

　　：
　　：
　　：
</manifest>
```

## サンプルコード

後述のAndroidServerSocket.javaを配置したうえで、以下の順でメソッド呼び出しを行う。
（UIスレッドで実行するとNetworkOnMainThreadExceptionが発生する）
 - NetworkHandler.setNetworks()
 - NetworkHandler.main():

```MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Thread networkThread = new Thread(new Runnable() {
            @Override
            public void run() {
                NetworkHandler.setNetworks();
                NetworkHandler.main();
            }
        });
        networkThread.start();
    }
}
```

setNetworks()を実行すると、static変数mWiFi、mMobileにandroid.net.Networkのインスタンスが格納される。
mWiFi、mMobileをあとから参照してネットワーク通信を実行する。

```NetworkHandler.java
package com.valuesccg.testapplication;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;

public class NetworkHandler {
    // あらかじめ、getApplicationContext()を呼び出した戻り値をここへ代入してください
    public static Context applicationContext = null;

    private static final String TAG = "NetworkHandler";
    public static Network mWiFi = null;
    public static Network mMobile = null;

    public static void setNetworks(){
        /*
        * モバイルネットワーク、Wi-Fiを見つけ出し、public static Networkに代入する
         */
        Log.d(TAG, "setNetworks()");
        Context context = applicationContext;
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Network[] networks = connectivityManager.getAllNetworks();
            for (Network network : networks) {
                if (network != null) {
                    NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
                    if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
                        continue;
                    if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                        Log.i(TAG, "Wi-Fiが見つかりました");
                        mWiFi = network;
                    }
                    if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
                        Log.i(TAG, "モバイルデータ通信が見つかりました");
                        mMobile = network;
                    }
                }
            }
        }
    }

    private static String sendGET(Network network, String url){
        /*
        * networkで指定したネットワークを使用して、指定したURLでレスポンスボディを取得する
         */
        HttpURLConnection conn = null;
        InputStream is = null;
        BufferedReader br = null;
        String line;
        StringBuilder sb;
        String body = "";
        try {
            URL url_ = new URL(url);

            conn = (HttpURLConnection) network.openConnection(url_);
            conn.connect();
            is = conn.getInputStream();
            br = new BufferedReader(new InputStreamReader(is));
            sb = new StringBuilder();
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }
            body = sb.toString();
        }
        catch (Exception e){
            e.printStackTrace();
        }
        finally {
            conn.disconnect();
        }
        return body;
    }

    private static void requestHandler(Socket sock) throws IOException {
        /*
        * TCPでテキストを受け取ってLogcatに出すだけ
         */
        BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));

        StringBuilder requestBuilder = new StringBuilder();
        while (br.ready()) {
            String line = br.readLine();
            requestBuilder.append(line + "\r\n");
        }
        String result = requestBuilder.toString();
        Log.i(TAG, result);
    }

    public static void main(){
        /*
        * ------------------------------------------------
        * サンプルコード
        * ------------------------------------------------
         */

        // URLアクセスを行う（GETリスエクトのみ）
        String url = "http://なにかのURL";
        Log.i(TAG, sendGET(mMobile, url));
        Log.i(TAG, sendGET(mWiFi, url));

        // モバイルデータ通信と同時にWiFiで使えるTCPサーバ
        ServerSocket serverSocket = null;
        boolean isActive = true;
        try {
            serverSocket = new AndroidServerSocket(mWiFi, 8080);
            Log.i(TAG, "HTTP server has started!");
            while (isActive) {
                Socket sock = null;
                try {
                    sock = serverSocket.accept();
                    Log.d(TAG, "received");
                    requestHandler(sock);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                finally {
                    if (sock != null) sock.close();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (serverSocket != null) serverSocket.close();
            }catch (IOException e) {}
        }
    }

}
```

java標準APIのServerSocketクラスを用いることで、TCPサーバを立てれることが知られている。
ServerSocket.accept()で「待ち受け」ができるが、その中の実装ではimplAccept()メソッドが呼ばれている。

ここで、implAccept()の引数としてandroid.net.Networkオブジェクトを渡すことで、そのネットワークでTCPサーバを立てることができるはず。

モバイルデータ通信を行いながらWi-Fi側でTCPサーバを立てることを実現するために、java.net.ServerSocketを継承したAndroidServerSocketを実装する。


```AndroidServerSocket.java
package com.valuesccg.testapplication;

import android.net.Network;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import javax.net.SocketFactory;

public class AndroidServerSocket extends ServerSocket {
    /*
    * android.net.Networkを指定し、そのNetworkでHTTPServerを立てられるように改造したServerSocket
     */
    private Network mNetwork;

    public AndroidHttpServerSocket(Network _network, int port) throws IOException {
        super(port);
        mNetwork = _network;
    }

    @Override
    public Socket accept() throws IOException{

        Socket socket;
        if(mNetwork == null){
            socket = new Socket();
        }
        else {
            SocketFactory socketFactory = mNetwork.getSocketFactory();
            socket = socketFactory.createSocket();
        }

        try
        {
            implAccept(socket);
        }
        catch (IOException e)
        {
            try
            {
                socket.close();
            }
            catch (IOException e2)
            {
            }

            throw e;
        }
        catch (SecurityException e)
        {
            try
            {
                socket.close();
            }
            catch (IOException e2)
            {
            }

            throw e;
        }

        return socket;
    }
}
```

## 解説

### ネットワークの識別およびNetworkオブジェクト取得

getApplicationContext()の戻り値として得られたcontextに対してgetSystemService()メソッドを呼び出すことで、ConnectivityManagerのインスタンスを得ることが出来る。
これに対しgetAllNetworks()メソッドを呼び出すと、各ネットワークを表すandroid.net.Networkインスタンスの配列が返ってくる。

```java
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network[] networks = connectivityManager.getAllNetworks();
```

ところが、ここで得られたNetworkには様々な種類のネットワークや、実際に使用できないものが含まれるケースがある。
意図したNetworkインスタンスを取得するため、ループ処理を用いてバリデーションを行う。

getNetworkCapabilities()を呼び出すことでNetworkCapabilitiesオブジェクトを得られる。
以下のように実装することで、「モバイルデータかWi-Fiか」「インターネットが使えるネットワークか」等を確認することができる。

```java
for (Network network : networks) {
    if (network != null) {
        NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
        if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
            continue;
        if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
            Log.i(TAG, "Wi-Fiが見つかりました");
            mWiFi = network;
        }
        if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
            Log.i(TAG, "モバイルデータ通信が見つかりました");
            mMobile = network;
        }
    }
}
```

### HTTPリクエスト

以上の実装から、モバイルデータとWi-Fiを表すNetworkオブジェクトをそれぞれ得ることが出来た。
android.net.NetworkにはopenConnection()メソッドが用意されているため、これを呼び出すだけでGETリクエストを行うことが出来る。

```java
URL url = new URL("http://なにかのURL");
HttpUrlConnection conn = (HttpURLConnection) network.openConnection(url);
conn.connect();
```

java.net.HttpUrlConnectionにキャストすることが出来るので、後続の処理では通常のHTTPリクエストと同様に処理することができる。

```java
is = conn.getInputStream();
br = new BufferedReader(new InputStreamReader(is));
sb = new StringBuilder();
while ((line = br.readLine()) != null) {
    sb.append(line);
    sb.append("\n");
}
body = sb.toString();
```

### ソケットプログラミング

android.net.NetworkにはgetSocketFactory()メソッドが用意されている。
SocketFactoryオブジェクトが返ってくるので、createSocket()を呼び出すことで、そのネットワークで使うことが出来るsocketを得ることが出来る。

これはjava.net.Socketインスタンスのため、後続の処理では通常のソケットプログラミングと同様に処理することができる。

```java
SocketFactory socketFactory = network.getSocketFactory();
socket = socketFactory.createSocket();
```