[Android]データ通信可能か確認する[データ専用SIM][Lollipop]

# Androidにおける通信状態

Androidにかかわらず、モバイル通信には色々な状態があります。
Androidでは、それらは[ConnectivityManager](http://developer.android.com/reference/android/net/ConnectivityManager.html)を通して[NetworkInfo](http://developer.android.com/reference/android/net/NetworkInfo.html)などを取得することで確認できますが、昨今ではMVNOなどを中心としてデータ通信専用のSIMカードを使っているユーザも増えてきていると思います。

今回はSIMカードが（というよりモバイル通信の状態が）、データ通信のみか音声通話も可能かを判定する標準APIについて調べたのでメモしておきます。

ちなみにLollipopの通知に出る「緊急通報のみ」という表示の切り替えのロジックを調べた結果です。

# モバイルデータ通信自体を端末がサポートしているか？

ConnectivityManager.isNetworkSupported()を使用することで端末がモバイルデータ通信可能かどうかを調べることができるようです。
APIはAndroid4.0から追加されています。
それ以前は代替手段はないかもしれません。

```java:ConnectivityManager.java


    /**
     * Returns true if the hardware supports the given network type
     * else it returns false.  This doesn't indicate we have coverage
     * or are authorized onto a network, just whether or not the
     * hardware supports it.  For example a GSM phone without a SIM
     * should still return {@code true} for mobile data, but a wifi only
     * tablet would return {@code false}.
     *
     * @param networkType The network type we'd like to check
     * @return {@code true} if supported, else {@code false}
     *
     * <p>This method requires the call to hold the permission
     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
     * @hide
     */
    public boolean isNetworkSupported(int networkType) {
        try {
            return mService.isNetworkSupported(networkType);
        } catch (RemoteException e) {}
        return false;
    }

```

## 実装例

上記を見てもらえば分かりますが、APIは@hideなので、使用する際にはリフレクションを使ってください。

```java:


        ConnectivityManager cm = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);

        Class clazz = ConnectivityManager.class;
        try {
            Method method = clazz.getDeclaredMethod("isNetworkSupported", int.class);
        // 今回はMobile通信が知りたいのでTYPE_MOBILEを使います。
            return (Boolean) method.invoke(cm, ConnectivityManager.TYPE_MOBILE);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;
```

検証が足りていないのでわかりませんが、JavaDocを見るとSIMスロットルがあるか？というようなものかと考えられます。

## 注意点

* WiMax対応端末について

Lollipopの通知表示の切り替えでは、この値はWiMax通信中は見ていなかったので、WiMaxのみ対応した端末の場合はfalseで帰ってくることも考えられます。

* Permissionについて

注意点としては、JavaDocに書いてある通り、[`android.Manifest.permission.ACCESS_NETWORK_STATE`](http://developer.android.com/reference/android/Manifest.permission.html#ACCESS_NETWORK_STATE)が必要です。

# 緊急通報のみの音声通話しか許可されていないか？

次に、データ通信専用かどうかを確認します。
今回は緊急通報のみかどうか、で音声通話が許可されていないと判断しています。
いろいろと条件がありますが、とりあえず目安にはなるかな、と思います。

こちらは[TelephonyManager](http://developer.android.com/reference/android/telephony/TelephonyManager.html)経由で取得できる[ServiceState](http://developer.android.com/reference/android/telephony/ServiceState.html)の関数で調べることができます。

## ServiceState.isEmergencyOnly

まず、ServiceStateの関数isEmergencyOnly()で判定することができます。
こちらは2.3系からAPIが追加されています。
しかし、4.2系で試したところ、正しく判別できていない端末もあったようだったので、きちんと検証する必要があると思います。
5.0系ではシステムの通知で使用しているのできちんと動いていると思われますので、基本的には5.0系以降で使ったほうがいいかもしれません。

## ServiceState.getState()
また、[ServiceState.getState()](http://developer.android.com/reference/android/telephony/ServiceState.html#getState())の返り値でも[ServiceState.STATE_EMERGENCY_ONLY](http://developer.android.com/reference/android/telephony/ServiceState.html#STATE_EMERGENCY_ONLY)の時は緊急通報のみと判定できそうですが、[ServiceState.STATE_OUT_OF_SERVICE](http://developer.android.com/reference/android/telephony/ServiceState.html#STATE_OUT_OF_SERVICE)（圏外？）になってしまう場合が多いようです。
STATE_EMERGENCY_ONLYを返すパターンを調べ切れていません。
こちらは2.0系でも存在を確認しています。それ以前は調べていません。

ただし、機内モードの時はServiceState.getState()が[ServiceState.STATE_POWER_OFF](http://developer.android.com/reference/android/telephony/ServiceState.html#STATE_POWER_OFF)となり、かつisEmergencyOnlyがfalseになってしまうため、注意が必要です。

## 併用まとめ

|                        | isEmergencyOnly |        getState |
|:-----------------------|----------------:|----------------:|
| 音声通話あり            |           false |          その他 |
| 音声通話あり(機内モード) |           false | STATE_POWER_OFF |
| 音声通話なし            |            true |          その他 |
| 音声通話なし(機内モード) |           false | STATE_POWER_OFF |

5.0系の場合は、getStateとisEmergencyOnlyを併用すれば、判定できそうです。
それ以前のバージョンでは端末やパターンによって確認が必要でしょう。


## 実装例
取得は同期ではできず、TelephonyManagerに[PhoneStateListener](http://developer.android.com/reference/android/telephony/PhoneStateListener.html)を登録して、[onServiceStateChanged](http://developer.android.com/reference/android/telephony/PhoneStateListener.html#onServiceStateChanged(android.telephony.ServiceState))で引数に与えられるServiceStateを使います。

```java:

        TelephonyManager mPhone = (TelephonyManager) context
                .getSystemService(Context.TELEPHONY_SERVICE);
        mPhone.listen(new PhoneStateListener() {
            @Override
            public void onServiceStateChanged(ServiceState serviceState) {
                super.onServiceStateChanged(serviceState);

                // isEmergencyOnlyはリフレクションで確認
                boolean isEmergencyOnly = false;
                Class clazz = ServiceState.class;
                try {
                    Method method = clazz.getDeclaredMethod("isEmergencyOnly");
                    isEmergencyOnly = (Boolean) method.invoke(serviceState);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

                // stateはそのまま呼べる。値で確認する
                int state = serviceState.getState();
            }
        }, PhoneStateListener.LISTEN_SERVICE_STATE);
        // 必要なcallbackイベントはserviceStateだけなので、ここではserviceStateだけリクエストしています。

```

これで緊急通報のみかどうかの判定ができます。

## 注意点

* バージョン依存

APIが実装されているバージョンにかかわらず、隠しAPIなど使えない場合もありそうです。
実際、isEmergencyOnlyは4.2系だとデータ通信専用SIMでもfalseを返す端末があったので、ひとまず使うとしても通知領域で表示されるようになった5.0系から使うべきでしょう。

* permission

listenerを登録する際に、[`android.Manifest.permission.READ_PHONE_STATE`](http://developer.android.com/reference/android/Manifest.permission.html#READ_PHONE_STATE)が必要なので注意してください。

* Listenerについて

また、lisetenerはunregister的なメソッドがなく、参照が残っているとずっとcallbackが呼ばれるので、UI更新イベントなど発行する際には注意が必要です。

# まとめ

誰得情報でしたが、もしMVNOのSIMで通話をサポートしていない場合は電話をかける？というような導線を制御したり、できるかもしれません。

あとはネットワーク状況など、再現条件の確認に使えるかもしれません。