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"/>