2016/11/25

Android で Wi-Fi ScanResult を WifiConfiguration に変換する方法


> この記事は [Android の Wi-Fi 実装に関する情報のまとめ](http://kokufu.blogspot.jp/2016/10/android-wi-fi_19.html) の一部として書かれました

「[Android の Wi-Fi Access Point を表す ScanResult, WifiConfiguration, WifiInfo の違い](https://kokufu.blogspot.jp/2016/11/android-wi-fi-access-point-scanresult.html)」 で書いたように、接続先を追加する際には [WifiConfiguration](https://developer.android.com/reference/android/net/wifi/WifiConfiguration.html) を新規作成する必要があります。
しかし、そのような変換ユーティリティメソッドなどは用意されていません。



### AOSP ではどうしているのか
では、[AOSP](https://source.android.com/) の設定アプリではどのようにしているのかを調べてみると参考にしたバージョンは Android 7.0.0_r19、 `com.android.settings.wifi.WifiConfigController.getConfig()` というメソッドで `WifiConfiguration` を作成していました。
以下2つのクラスを読むと大体の概要がわかるでしょう。

- [com.android.settings.wifi.WifiConfigController](https://android.googlesource.com/platform/packages/apps/Settings/+/android-7.0.0_r19/src/com/android/settings/wifi/WifiConfigController.java)

- [com.android.settingslib.wifi.AccessPoint](https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r19/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java)


### 同等なメソッドを実装
`com.android.settings.wifi.WifiConfigController` や  `com.android.settingslib.wifi.AccessPoint` は基本的には外部から利用することが出来ません。

ただ、実際の `getConfig()` はかなりシンプルです実装が簡単なので変換ユーティリティ等は用意されていないのだと思う。
そこで、同等のメソッドを実装してみました。

以下が `ScanResult` から `WifiConfiguration` に変換するためのメソッドですEAP を除く。

最も需要がありそうな `ScanResult` からの変換を実装しましたが、
`SSID`もしくは networkId, `SecurityType`, `password` がわかっていれば `WifiConfiguration` は新規作成可能なので `ScanResult` 以外から作成することも難しくないはずです。


```java
public static WifiConfiguration convertScanResultToWifiConfiguration(
        ScanResult scanResult,
        @Nullable String password) {
    WifiConfiguration config = new WifiConfiguration();

    // Quote を忘れないように
    config.SSID = '"' + scanResult.SSID + '"';

    // ScanResult を取得出来ているということは hiddenSSID ではない
    config.hiddenSSID = false;

    SecurityType securityType = getSecurity(scanResult);

    switch (securityType) {
        case SECURITY_NONE:
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
            break;
        case SECURITY_WEP:
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
            config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
            if (password != null) {
                int length = password.length();
                // WEP-40, WEP-104, and 256-bit WEP (WEP-232?)
                if ((length == 10 || length == 26 || length == 58)
                        && password.matches("[0-9A-Fa-f]*")) {
                    config.wepKeys[0] = password;
                } else {
                    config.wepKeys[0] = '"' + password + '"';
                }
            }
            break;
        case SECURITY_PSK:
            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
            if (password != null) {
                if (password.matches("[0-9A-Fa-f]{64}")) {
                    config.preSharedKey = password;
                } else {
                    config.preSharedKey = '"' + password + '"';
                }
            }
            break;
        case SECURITY_EAP:
            // EAP は省略
            // WifiConfigController.getConfig() 内の実装を参考にすれば
            // 同じように作れるはず
            Log.e(TAG, "EAP: Does not support");
            break;
        default:
            return null;
    }

    /**************************************************************
     以下は hide 属性のついたメソッド
     DHCP でつなぐ場合は問題ないが、それ以外の場合は設定しないと動かないかも
     その場合は Refrection しかないか?
     **************************************************************/
    //config.setIpConfiguration(
    //        new IpConfiguration(mIpAssignment, mProxySettings,
    //                mStaticIpConfiguration, mHttpProxy));
    return config;
}

private enum SecurityType {
    SECURITY_NONE,
    SECURITY_WEP,
    SECURITY_PSK,
    SECURITY_EAP
}

private static SecurityType getSecurity(ScanResult result) {
    if (result.capabilities.contains("WEP")) {
        return SecurityType.SECURITY_WEP;
    } else if (result.capabilities.contains("PSK")) {
        return SecurityType.SECURITY_PSK;
    } else if (result.capabilities.contains("EAP")) {
        return SecurityType.SECURITY_EAP;
    }
    return SecurityType.SECURITY_NONE;
}
```

気をつけなければならないのは、 `WifiConfiguration.setIpConfiguration()` が外部からのアクセス不可能なメソッドになっている点です。
調度良い環境がないので確かめていないのですが、DHCP を使っていなかったり、Proxy を使っていたりするとうまく IP がとれないかもしれません。


### WifiConfigController 内の getConfig() の実装
Android 7.0.0_r19 のコードをあげておきます。
EAP が長いので読みづらく感じますが、先にあげたものとほとんど同じであることがわかると思います。

```java
`first-line: 451
/* package */ WifiConfiguration getConfig() {
    if (mMode == WifiConfigUiBase.MODE_VIEW) {
        return null;
    }
    WifiConfiguration config = new WifiConfiguration();
    if (mAccessPoint == null) {
        config.SSID = AccessPoint.convertToQuotedString(
                mSsidView.getText().toString());
        // If the user adds a network manually, assume that it is hidden.
        config.hiddenSSID = true;
    } else if (!mAccessPoint.isSaved()) {
        config.SSID = AccessPoint.convertToQuotedString(
                mAccessPoint.getSsidStr());
    } else {
        config.networkId = mAccessPoint.getConfig().networkId;
    }
    config.shared = mSharedCheckBox.isChecked();
    switch (mAccessPointSecurity) {
        case AccessPoint.SECURITY_NONE:
            config.allowedKeyManagement.set(KeyMgmt.NONE);
            break;
        case AccessPoint.SECURITY_WEP:
            config.allowedKeyManagement.set(KeyMgmt.NONE);
            config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
            config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);
            if (mPasswordView.length() != 0) {
                int length = mPasswordView.length();
                String password = mPasswordView.getText().toString();
                // WEP-40, WEP-104, and 256-bit WEP (WEP-232?)
                if ((length == 10 || length == 26 || length == 58)
                        && password.matches("[0-9A-Fa-f]*")) {
                    config.wepKeys[0] = password;
                } else {
                    config.wepKeys[0] = '"' + password + '"';
                }
            }
            break;
        case AccessPoint.SECURITY_PSK:
            config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
            if (mPasswordView.length() != 0) {
                String password = mPasswordView.getText().toString();
                if (password.matches("[0-9A-Fa-f]{64}")) {
                    config.preSharedKey = password;
                } else {
                    config.preSharedKey = '"' + password + '"';
                }
            }
            break;
        case AccessPoint.SECURITY_EAP:
            config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
            config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
            config.enterpriseConfig = new WifiEnterpriseConfig();
            int eapMethod = mEapMethodSpinner.getSelectedItemPosition();
            int phase2Method = mPhase2Spinner.getSelectedItemPosition();
            config.enterpriseConfig.setEapMethod(eapMethod);
            switch (eapMethod) {
                case Eap.PEAP:
                    // PEAP supports limited phase2 values
                    // Map the index from the mPhase2PeapAdapter to the one used
                    // by the API which has the full list of PEAP methods.
                    switch(phase2Method) {
                        case WIFI_PEAP_PHASE2_NONE:
                            config.enterpriseConfig.setPhase2Method(Phase2.NONE);
                            break;
                        case WIFI_PEAP_PHASE2_MSCHAPV2:
                            config.enterpriseConfig.setPhase2Method(Phase2.MSCHAPV2);
                            break;
                        case WIFI_PEAP_PHASE2_GTC:
                            config.enterpriseConfig.setPhase2Method(Phase2.GTC);
                            break;
                        default:
                            Log.e(TAG, "Unknown phase2 method" + phase2Method);
                            break;
                    }
                    break;
                default:
                    // The default index from mPhase2FullAdapter maps to the API
                    config.enterpriseConfig.setPhase2Method(phase2Method);
                    break;
            }
            String caCert = (String) mEapCaCertSpinner.getSelectedItem();
            config.enterpriseConfig.setCaCertificateAliases(null);
            config.enterpriseConfig.setCaPath(null);
            config.enterpriseConfig.setDomainSuffixMatch(mEapDomainView.getText().toString());
            if (caCert.equals(mUnspecifiedCertString)
                    || caCert.equals(mDoNotValidateEapServerString)) {
                // ca_cert already set to null, so do nothing.
            } else if (caCert.equals(mUseSystemCertsString)) {
                config.enterpriseConfig.setCaPath(SYSTEM_CA_STORE_PATH);
            } else if (caCert.equals(mMultipleCertSetString)) {
                if (mAccessPoint != null) {
                    if (!mAccessPoint.isSaved()) {
                        Log.e(TAG, "Multiple certs can only be set "
                                + "when editing saved network");
                    }
                    config.enterpriseConfig.setCaCertificateAliases(
                            mAccessPoint
                                    .getConfig()
                                    .enterpriseConfig
                                    .getCaCertificateAliases());
                }
            } else {
                config.enterpriseConfig.setCaCertificateAliases(new String[] {caCert});
            }
            // ca_cert or ca_path should not both be non-null, since we only intend to let
            // the use either their own certificate, or the system certificates, not both.
            // The variable that is not used must explicitly be set to null, so that a
            // previously-set value on a saved configuration will be erased on an update.
            if (config.enterpriseConfig.getCaCertificateAliases() != null
                    && config.enterpriseConfig.getCaPath() != null) {
                Log.e(TAG, "ca_cert ("
                        + config.enterpriseConfig.getCaCertificateAliases()
                        + ") and ca_path ("
                        + config.enterpriseConfig.getCaPath()
                        + ") should not both be non-null");
            }
            String clientCert = (String) mEapUserCertSpinner.getSelectedItem();
            if (clientCert.equals(mUnspecifiedCertString)
                    || clientCert.equals(mDoNotProvideEapUserCertString)) {
                // Note: |clientCert| should not be able to take the value |unspecifiedCert|,
                // since we prevent such configurations from being saved.
                clientCert = "";
            }
            config.enterpriseConfig.setClientCertificateAlias(clientCert);
            if (eapMethod == Eap.SIM || eapMethod == Eap.AKA || eapMethod == Eap.AKA_PRIME) {
                config.enterpriseConfig.setIdentity("");
                config.enterpriseConfig.setAnonymousIdentity("");
            } else if (eapMethod == Eap.PWD) {
                config.enterpriseConfig.setIdentity(mEapIdentityView.getText().toString());
                config.enterpriseConfig.setAnonymousIdentity("");
            } else {
                config.enterpriseConfig.setIdentity(mEapIdentityView.getText().toString());
                config.enterpriseConfig.setAnonymousIdentity(
                        mEapAnonymousView.getText().toString());
            }
            if (mPasswordView.isShown()) {
                // For security reasons, a previous password is not displayed to user.
                // Update only if it has been changed.
                if (mPasswordView.length() > 0) {
                    config.enterpriseConfig.setPassword(mPasswordView.getText().toString());
                }
            } else {
                // clear password
                config.enterpriseConfig.setPassword(mPasswordView.getText().toString());
            }
            break;
        default:
            return null;
    }
    config.setIpConfiguration(
            new IpConfiguration(mIpAssignment, mProxySettings,
                                mStaticIpConfiguration, mHttpProxy));
    return config;
}
```

0 件のコメント: