WiFi DirectライブラリSalut

https://github.com/markrjr/Salut/blob/master/README.md の2016/05/12時点の日本語訳です。

# Salut

SalutはAndroidのWiFi Direct APIのラッパーです。Salutを使用する前に下記のドキュメントをざっと読んでおくことをおすすめします。

### Recommended Reading

* [General Overview](http://developer.android.com/guide/topics/connectivity/wifip2p.html)  
* [Service Discovery](http://developer.android.com/training/connect-devices-wirelessly/nsd-wifi-direct.html)  
* [Power Consumption](http://www.drjukka.com/blog/wordpress/?p=95)  
* [More Recommended Reading](http://www.drjukka.com/blog/wordpress/?p=81)

このライブラリはAPI 16 (Android 4.1 Jelly Bean)以上をサポートします。技術的にはWiFi DirectはAndroid 4.0から使用できますが、4.1以上の方が信頼性が高いです。

### 注意

このライブラリは現在ベータ状態であり、APIや機能は変更される可能性があります。

### なぜこの名前？どんな意味が？

Salutはフランス語の挨拶で、helloやgoodbyeみたいなものです。なぜこの名前かというと、iOSで使われているAppleの同じような技術にBonjourという名前が付いているからです。

## Dependencies

このライブラリは以下のライブラリに依存します。

* [LoganSquare (Serialization)](https://github.com/bluelinelabs/LoganSquare)  
* [AsyncJob Library](https://github.com/Arasthel/AsyncJobLibrary)  

**LoganSquareはアプリにも含める必要があります。**そのために、プロジェクトのbuild.gradeを修正します。

```groovy
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    apt 'com.bluelinelabs:logansquare-compiler:1.3.4'
    compile 'com.bluelinelabs:logansquare:1.3.4'
}
```

## インストール

[JitPack](https://jitpack.io/#markrjr/Salut)を使ってインストールするのが簡単です。

## 使い方

### [Sample Activity](https://gist.github.com/markrjr/0519268f69a5823da17b)

### 利用手順

まず、次のパーミッションをAndroidManifest.xmlに追加します。

```xml
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- AndroidではINTERNETパーミッションがないとソケットをオープンできません -->
```

*Andorid 6.0以上では、上記のパーミッションは自動で付与されます。[(詳しくはこちら)](http://developer.android.com/guide/topics/security/normal-permissions.html)*

次に、データを受信したいクラスに`SalutDataCallback`を実装します。このコールバック処理は呼び出し元と同じスレッドで発生します。
そして、`SalutDataReceiver`と`SalutServiceData`オブジェクトを作成します。

```java
    SalutDataReceiver dataReceiver = new SalutDataReceiver(myActivity, myActivity);
    SalutServiceData serviceData = new SalutServiceData("sas", 50489, superAwesomeUser.name);
```

`SalutDataReceiver`は二つの引数を取ります`(Activity activity, SalutDataCallback dataCallback)`。上記の例では、Activityが`SalutDataCallback`を実装しています。なので`mActivity`を二度渡しています。ここにActivityを渡すとSalutが自動的にBroadcastReceiverを登録・解除するようになります。

`SalutServiceData`はサービス名、ポート、インスタンス名を受け取ります。サービス名はユーザーに表示される読みやすい文字列です。暗号的な文字列にはしないことをおすすめします。**Android 5.0未満をサポートする場合、サービス名と表示名は短めの文字列を使用してください。これはシステム自体の制限です。**

最後に、`Salut`インスタンスを作ります。

```java
    Salut network = new Salut(dataReceiver, serviceData, new SalutCallback() {
        @Override
        public void call() {
            Log.e(TAG, "Sorry, but this device does not support WiFi Direct.");
        }
    });
    
```

**ホストとして動作するかそうでないかを保持する変数を用意しておくのは良い方法です。`isRunningAsHost`という論理型がフレームワークで提供されており、ホストとして動作しているかどうかを判断するときに使われます。しかしこれはグループオーナーとして接続しているかどうかやサーバーが動作しているかどうかとは関係ありません。**

### Working with services

インスタンスを作成したら、WiFi direct サービスを作成したり探したりできます。

#### ホスト側

```java
    network.startNetworkService(new SalutDeviceCallback() {
        @Override
        public void call(SalutDevice device) {
            Log.d(TAG, device.readableName + " has connected!");
        }
    });
```

デバイスが接続して登録されると、コールバックが呼び出されます。**`registeredClients`で登録されている全てのデバイスにアクセスできます。**

#### クライアント側

サービスを探すにはいくつかの方法があります。**Salutは同じタイプのサービスにのみ接続します。**

```java
    network.discoverNetworkServices(new SalutDeviceCallback() {
        @Override
        public void call(SalutDevice device) {
           Log.d(TAG, "A device has connected with the name " + device.deviceName);
        }
    }, false);
    
    //または
    
    network.discoverNetworkServices(new SalutCallback() {
        @Override
        public void call() {
           Log.d(TAG, "All I know is that a device has connected.");
        }
    }, true);
```

どちらのメソッドにも、論理型の引数を渡します。これはコールバックが繰り返し呼び出されるかどうかを指定します。もし**true**だった場合は、デバイスが見つかる度にコールバックを呼び出します。**false**なら、最初のデバイスを発見した時に一度だけコールバックを呼び出します。**true,falseどちらを渡したかに関係なく、`stopServiceDiscovery()`を呼び出すまでデバイスを探し続けます。**

もう一つ、`discoverNetworkServicesWithTimeout()`メソッドがあります。これは名前が示すとおり、指定した時間だけデバイスを探して自動的に`stopServiceDiscovery()`を呼びます。**`foundDevices`フィールドで見つかった全てのデバイスを参照できます。**

```java
    network.discoverNetworkServicesWithTimeout(new SalutCallback() {
        @Override
        public void call() {
            Log.d(TAG, "Look at all these devices! " + network.foundDevices.toString());
        }
    }, new SalutCallback() {
        @Override
        public void call() {
            Log.d(TAG, "Bummer, we didn't find anyone. ");
        }
    }, 5000);
```

デバイスがホストを見つけたら、`registerWithHost()`を呼び出します。

```java
    network.registerWithHost(possibleHost, new SalutCallback() {
        @Override
        public void call() {
            Log.d(TAG, "We're now registered.");
        }
    }, new SalutCallback() {
        @Override
        public void call() {
            Log.d(TAG, "We failed to register.");
        }
    });
```

このメソッドは実際にWiFi Directでデバイス同士を接続します。フレームワークは通常のソケットでデータを交換します。デバイスは`unregisterClient`がクライアント側で呼び出されるか、`stopNetworkService`がホスト側で呼び出されるまで接続し続けます。

### データを作る

LoganSquare がライブラリの中でデータのシリアライゼーションを行います。LoganSquareは単純な文字列をシリアライズできないので、ラッパーオブジェクトを作成する必要があります。

```java
@JsonObject
public class Message{

    /*
     * Annotate a field that you want sent with the @JsonField marker.
     */
    @JsonField
    public String description;

    /*
     * Note that since this field isn't annotated as a
     * @JsonField, LoganSquare will ignore it when parsing
     * and serializing this class.
     */
    public int nonJsonField;
}
```

### データを送る

クライアントがホストに登録されると、クライアントにデータを送ることができます。失敗した場合のコールバックも指定します。

全てのデバイスにデータを送るには、以下のようにします。

```java
    Message myMessage = new Message();
    myMessage.description = "See you on the other side!";
    
    network.sendToAllDevices(myMessage, new SalutCallback() {
        @Override
        public void call() {
            Log.e(TAG, "Oh no! The data failed to send.");
        }
    });
```

ホストだけが全てのデバイスのアドレスを知っているので、上記メソッドを呼び出せます。将来的にクライアントからも同じように全てのデバイスにデータを送れるようになる予定です。今のところはまずデータをホストに送ってから上記メソッドでその他のデバイスに伝達するという方法になります。

特定のデバイスにデータを送るには、以下のようにします。

```java
    Message myMessage = new Message();
    myMessage.description = "See you on the other side!";

    network.sendToDevice(deviceToSendTo, myMessage, new SalutCallback() {
        @Override
        public void call() {
            Log.e(TAG, "Oh no! The data failed to send.");
        }
    });
    
    network.sendToHost(myMessage, new SalutCallback() {
        @Override
        public void call() {
            Log.e(TAG, "Oh no! The data failed to send.");
        }
    });
    
```

### データを受け取る

SalutDataCallbackインターフェースを実装するクラスは`onDataReceived(Object data)`をオーバーライドします。

**データはシリアライズされた文字列で送られ、このメソッドでは`String`として送られます。LoganSquareで実際のオブジェクトに解析する必要があります。**

データは文字列で受け取ります。オブジェクトを解析する処理は次のようになります。

```java
    @Override
    public void onDataReceived(Object data) {
        Log.d(TAG, "Received network data.");
        try
        {
            Message newMessage = LoganSquare.parse((String)data, Message.class);
            Log.d(TAG, newMessage.description);  //See you on the other side!
            //Do other stuff with data.
        }
        catch (IOException ex)
        {
            Log.e(TAG, "Failed to parse network data.");
        }
    }
```

### クリーニング

```java
    @Override
    public void onDestroy() {
        super.onDestroy();
        
        if(MyApp.isHost)
            network.stopNetworkService(true);
        else
            network.unregisterClient(true);
    }
```

**冒頭で説明した、ホストとして動作するかそうでないかを保持する変数を使用しています。**

#### ホスト

**ホスト側では`stopNetworkService`を呼ばなければいけません。**WiFiを無効化するかどうかを指定する論理型の引数を渡します。

#### クライアント

**クライアントサイドでは`unregisterClient`を呼ばなければいけません。**WiFiを無効化するかどうかを指定する論理型の引数を渡します。解除処理の成功・失敗時のコールバックを指定することもできます。

## ライセンス

MIT

---

## あとがき

ここまでがんばって訳して書いてみたんですけど、いざ使ってみるとWi-Fi DirectのAPIは**意味不明に動かないことが多くてつらい**です。同じコードが1回目は動くのに2回目が動かなかったり、Wi-Fiを一度オフにしてオンにすると直った…と思ったら次は動かなかったり……

実用的に使えるようになるまでにはもう少しかかるのかな？と思いました。