Web sockets and the Lichess API (on Android)

91 Views Asked by At

As a learning project, I tried to develop an Android app (in Java) that can play games on Lichess.org using the Lichess API. Using OkHttp3, I managed to extract data from from the API (see the commented section in the code below). However, when actually playing games, you have to get Lichess to send you updates when a move has been made, and I got the impression that web sockets were the right technology for this. However, I keep getting the response

Response{protocol=http/1.1, code=200, message=OK, url=https://lichess.org/api/board/game/stream/<ID of my game>}

and the exception

java.net.ProtocolException: Expected HTTP 101 response but was '200 OK'

To use the Board API, you need an access token for Lichess with a specific mandate to use the Board API. This can be generated from the preferences on the Lichess website. Obviously, I should not share mine here, but it takes a couple of seconds to set up if you have a Lichess account.

EDIT: The Lichess API seems to indicate that 200 means that the response was actually successful. The “OK” part seems to indicate the same thing. So why was the method onFailure activated?

Here is my code. It is based on a simple “Hello World” application generated by Android Studio:

package com.example.test;

import android.os.Bundle;

import com.google.android.material.snackbar.Snackbar;

import androidx.appcompat.app.AppCompatActivity;

import android.view.View;

import androidx.core.view.WindowCompat;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.example.test.databinding.ActivityMainBinding;

import android.view.Menu;
import android.view.MenuItem;

import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

import okhttp3.*;

public class MainActivity extends AppCompatActivity {

    private AppBarConfiguration appBarConfiguration;
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        setSupportActionBar(binding.toolbar);

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        binding.fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAnchorView(R.id.fab)
                        .setAction("Action", null).show();
            }
        });

        try {
            run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void run() throws IOException {

        OkHttpClient client = new OkHttpClient();

        String access_token = "<my access token (not sharing this here)>";


        /*
        // The following works fine:

        Request request = new Request.Builder()
                .url("https://lichess.org/api/account/playing")
                .header("Authorization", "Bearer " + access_token)
                .header("Accept", "application/json")
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                call.cancel();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                final String myResponse = response.body().string();
                System.out.println(myResponse);
            }
        });

        */

        String gameId = "<game ID of the game being played>";

        Request gameRequest = new Request.Builder()
                .url("wss://lichess.org/api/board/game/stream/" + gameId)
                .header("Authorization", "Bearer " + access_token)
                .header("Accept", "application/x-ndjson")
                .build();

        WebSocketListener listener = new WebSocketListener(){
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                webSocket.send("{\"action\":\"subscribe\",\"type\":\"live\"}");
            }

            @Override
            public void onMessage(WebSocket webSocket, String text) {
                // Do whatever I want to do if successful
            }

            @Override
            public void onClosing(WebSocket webSocket, int code, String reason) {
                webSocket.close(1000, null);
            }

            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                t.printStackTrace();
            }
        };

        WebSocket websocket = client.newWebSocket(gameRequest,listener);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onSupportNavigateUp() {
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        return NavigationUI.navigateUp(navController, appBarConfiguration)
                || super.onSupportNavigateUp();
    }
}

In order to use web sockets, I gave my app the following permissions in the file AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
0

There are 0 best solutions below