ActionCableをAndroidからOkHttpで使ってみる

## 動機

RailsのWebアプリケーションを作る際に、ActionCableでお手軽にWebSocketを行いたい。
けど、今後の拡張性を考えると、Android/iOSともWebSocketでつなげる必要がある。
とりあえず、Androidで出来ればiOSでも出来るだろう。

## 先に結論

とりあえず、JSONのフォーマットさえちゃんとしたら、Androidでも動くっぽい。
ただ、 `disable_request_forgery_protection`してるので、本番では問題あるかも？

クライアント側でもある程度実装が必要っぽいので、本番では使わないかなぁ。
（素のWebSocketでの実装・その他の実装も比較する必要はありますが。）

## 前提

### サーバサイド

Ruby: 2.3.1
Rails: 5.0.0.beta1 （バージョン上げようとしたらいくつかエラーが出たので断念。）

https://github.com/JunichiIto/campfire を一部改変して利用しました。（heroku readyなのがありがたい）

### クライアント

Android: 4.2.2
okhttp: 3.4.2

## サーバサイドの準備

### サンプルから改変

https://github.com/JunichiIto/campfire をcloneしてくる。

`.ruby-version` と `Gemfile`に記載されているRubyのバージョンを、2.3.1に変更。

`config/environments/production.rb` でrequest originが `https?`で始まるものに限定していたので、その指定すら無くすようにしました。

```diff
-  config.action_cable.allowed_request_origins = [ /https?:\/\/.*/ ]
+  # config.action_cable.allowed_request_origins = [ /https?:\/\/.*/ ]
+  config.action_cable.disable_request_forgery_protection = true
```

（これって、http headerとかで見ているのでしょうか？本来であれば、何かしら制限しておく必要がある？）

### herokuにデプロイ

READMEに書いてある通り、herokuにアップロードする。
`heroku open` するとURLが分かるので、後でAndroidの実装に組み込む。

## クライアントの実装

### ライブラリのインポート

```
compile 'com.squareup.okhttp3:okhttp:3.4.2'
compile 'com.squareup.okhttp3:okhttp-ws:3.4.2'
```

（ただ、 https://github.com/square/okhttp/pull/2852 を見ると、次のバージョンとかでいろいろ変わるかも？）

### 送受信の実装

今回は、ActivityのonCreateで接続するようにしました。
結果はログに出るだけです。

```java:MainActivity.java
package hm.orz.chaos114.websocketsample;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

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

        Request request = new Request.Builder()
                .url("wss://INPUT_YOUR_DOMEIN.herokuapp.com/cable") // INPUT_YOUR_DOMEINは、heroku openしたときのドメインを指定してください。
                .build();

        OkHttpClient client = new OkHttpClient.Builder()
                .build();
        WebSocketCall call = WebSocketCall.create(client, request);
        call.enqueue(new WebSocketListener() {
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                Log.d(TAG, "onOpen");

                try {
                    webSocket.sendMessage(RequestBody.create(WebSocket.TEXT, "{\"command\":\"subscribe\",\"identifier\":\"{\\\"channel\\\":\\\"RoomChannel\\\"}\"}"));
                    webSocket.sendMessage(RequestBody.create(WebSocket.TEXT, "{\"command\":\"message\",\"identifier\": \"{\\\"channel\\\":\\\"RoomChannel\\\"}\",\"data\":\"{\\\"message\\\":\\\"sample message!!!\\\",\\\"action\\\":\\\"speak\\\"}\"}"));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            @Override
            public void onFailure(IOException e, Response response) {
                Log.d(TAG, "onFailure " + response, e);
            }

            @Override
            public void onMessage(ResponseBody message) throws IOException {
                Log.d(TAG, "onMessage " + message.string());
            }

            @Override
            public void onPong(Buffer payload) {
                Log.d(TAG, "onPong");
            }

            @Override
            public void onClose(int code, String reason) {
                Log.d(TAG, "onClose");
            }
        });
    }
}
```

起動すると、`RoomChannel`に接続し、`sample message!!!`というテキストを送信します。
heroku openしたブラウザを確認すると、メッセージが追加されていくはずです。

## 仕組みの考察メモ

- `ApplicationCable::Channel` を継承したクラス名が、Channel名となる。
- Channel名を指定して、`subscribe`コマンドを発行すると、受信可能状態になる。
- `ApplicationCable::Channel` を継承したクラスのメソッドが、Action名になる。
- Action名を指定して、`message`コマンドを発行すると、メッセージを送信できる。
- 部屋を分ける場合は、 https://github.com/rails/rails/tree/master/actioncable#passing-parameters-to-channel このへんを参考にしたら出来るかも？

## 参考

いろいろ実装したあとで、 https://github.com/hosopy/actioncable-client-java を見つけました。
コレ使えばよかったのかも。。