/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2016 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.catrobat.catroid.scratchconverter;
import android.util.Log;
import com.google.android.gms.common.images.WebImage;
import com.google.common.base.Preconditions;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.http.AsyncHttpClient;
import com.koushikdutta.async.http.WebSocket;
import com.koushikdutta.async.http.WebSocket.StringCallback;
import org.catrobat.catroid.common.Constants;
import org.catrobat.catroid.scratchconverter.protocol.BaseMessageHandler;
import org.catrobat.catroid.scratchconverter.protocol.Job;
import org.catrobat.catroid.scratchconverter.protocol.MessageListener;
import org.catrobat.catroid.scratchconverter.protocol.command.AuthenticateCommand;
import org.catrobat.catroid.scratchconverter.protocol.command.Command;
import org.catrobat.catroid.scratchconverter.protocol.command.RetrieveInfoCommand;
import org.catrobat.catroid.scratchconverter.protocol.command.ScheduleJobCommand;
import org.catrobat.catroid.scratchconverter.protocol.message.base.BaseMessage;
import org.catrobat.catroid.scratchconverter.protocol.message.base.ClientIDMessage;
import org.catrobat.catroid.scratchconverter.protocol.message.base.ErrorMessage;
import org.catrobat.catroid.scratchconverter.protocol.message.base.InfoMessage;
public final class WebSocketClient<T extends MessageListener & StringCallback>
implements Client, BaseMessageHandler, CompletedCallback {
private interface ConnectCallback {
void onSuccess();
void onFailure(ClientException ex);
}
private static final String TAG = WebSocketClient.class.getSimpleName();
private Client.State state;
private long clientID;
private final T messageListener;
private AsyncHttpClient asyncHttpClient = AsyncHttpClient.getDefaultInstance();
private WebSocket webSocket;
private Client.ConnectAuthCallback connectAuthCallback;
private ConvertCallback convertCallback;
public WebSocketClient(final long clientID, final T messageListener) {
this.clientID = clientID;
this.state = State.NOT_CONNECTED;
messageListener.setBaseMessageHandler(this);
this.messageListener = messageListener;
this.webSocket = null;
this.connectAuthCallback = null;
this.convertCallback = null;
}
public boolean isConnected() {
return state == State.CONNECTED || state == State.CONNECTED_AUTHENTICATED;
}
@Override
public boolean isClosed() {
return state == State.NOT_CONNECTED;
}
@Override
public boolean isAuthenticated() {
return state == State.CONNECTED_AUTHENTICATED;
}
public void setAsyncHttpClient(final AsyncHttpClient asyncHttpClient) {
this.asyncHttpClient = asyncHttpClient;
}
public void setConvertCallback(final ConvertCallback callback) {
convertCallback = callback;
}
private void connect(final ConnectCallback connectCallback) {
if (state == State.CONNECTED) {
connectCallback.onSuccess();
return;
}
Preconditions.checkState(webSocket == null);
Preconditions.checkState(asyncHttpClient != null);
final WebSocketClient client = this;
asyncHttpClient.websocket(Constants.SCRATCH_CONVERTER_WEB_SOCKET, null, new
AsyncHttpClient.WebSocketConnectCallback() {
@Override
public void onCompleted(Exception ex, final WebSocket newWebSocket) {
Preconditions.checkState(state != State.CONNECTED && webSocket == null);
if (ex != null) {
connectCallback.onFailure(new ClientException(ex));
return;
}
state = State.CONNECTED;
webSocket = newWebSocket;
// onMessage callback
webSocket.setStringCallback(messageListener);
// onClose callback
webSocket.setClosedCallback(client);
connectCallback.onSuccess();
}
});
}
@Override
public void onCompleted(Exception ex) {
// Note: this is the central connection-closed callback-method
// (called when the server or client closes the connection):
state = State.NOT_CONNECTED;
connectAuthCallback.onConnectionClosed(new ClientException(ex));
}
public void close() {
Preconditions.checkState(state != State.NOT_CONNECTED);
Preconditions.checkState(webSocket != null);
Preconditions.checkState(connectAuthCallback != null);
state = State.NOT_CONNECTED;
webSocket.close();
}
private void authenticate() {
Preconditions.checkState(state == State.CONNECTED);
Preconditions.checkState(webSocket != null);
sendCommand(new AuthenticateCommand(clientID));
}
public void connectAndAuthenticate(final ConnectAuthCallback connectAuthCallback) {
this.connectAuthCallback = connectAuthCallback;
switch (state) {
case NOT_CONNECTED:
connect(new ConnectCallback() {
@Override
public void onSuccess() {
Log.i(TAG, "Successfully connected to WebSocket server");
authenticate();
}
@Override
public void onFailure(ClientException ex) {
connectAuthCallback.onConnectionFailure(ex);
}
});
break;
case CONNECTED:
Log.i(TAG, "Already connected to WebSocket server!");
authenticate();
break;
case CONNECTED_AUTHENTICATED:
Log.i(TAG, "Already authenticated!");
connectAuthCallback.onSuccess(clientID);
break;
}
}
@Override
public void retrieveInfo() {
Preconditions.checkState(state == State.CONNECTED_AUTHENTICATED);
Preconditions.checkState(clientID != INVALID_CLIENT_ID);
sendCommand(new RetrieveInfoCommand());
}
@Override
public boolean isJobInProgress(long jobID) {
return messageListener.isJobInProgress(jobID);
}
@Override
public int getNumberOfJobsInProgress() {
return messageListener.getNumberOfJobsInProgress();
}
@Override
public void convertProgram(final long jobID, final String title, final WebImage image, final boolean verbose,
final boolean force) {
Preconditions.checkState(state == State.CONNECTED_AUTHENTICATED);
Preconditions.checkState(clientID != INVALID_CLIENT_ID);
final Job job = new Job(jobID, title, image);
if (state != State.CONNECTED_AUTHENTICATED || webSocket == null) {
convertCallback.onConversionFailure(job, new ClientException("Not connected!"));
return;
}
if (!messageListener.scheduleJob(job, force, convertCallback)) {
Log.e(TAG, "Cannot schedule job since another job of the same Scratch program is already running (job ID "
+ "is: " + jobID + ")");
convertCallback.onConversionFailure(job, new ClientException("Cannot start this job since the job "
+ "already exists and is in progress! Set force-flag to true to restart the conversion while it "
+ "is running!"));
return;
}
Log.i(TAG, "Scheduling new job with ID: " + jobID);
sendCommand(new ScheduleJobCommand(jobID, force, verbose));
}
@Override
public void onUserCanceledConversion(long jobID) {
Preconditions.checkState(state == State.CONNECTED_AUTHENTICATED);
Preconditions.checkState(clientID != INVALID_CLIENT_ID);
messageListener.onUserCanceledConversion(jobID);
}
@Override
public void onBaseMessage(BaseMessage baseMessage) {
if (baseMessage instanceof InfoMessage) {
final InfoMessage infoMessage = (InfoMessage) baseMessage;
convertCallback.onInfo(infoMessage.getCatrobatLanguageVersion(), infoMessage.getJobList());
final Job[] jobs = infoMessage.getJobList();
for (Job job : jobs) {
DownloadCallback downloadCallback = messageListener.restoreJobIfRunning(job, convertCallback);
if (downloadCallback != null) {
convertCallback.onConversionAlreadyFinished(job, downloadCallback, job.getDownloadURL());
}
}
return;
}
if (baseMessage instanceof ErrorMessage) {
final ErrorMessage errorMessage = (ErrorMessage) baseMessage;
Log.e(TAG, errorMessage.getMessage());
if (state == State.CONNECTED) {
Preconditions.checkState(connectAuthCallback != null);
connectAuthCallback.onAuthenticationFailure(new ClientException(errorMessage.getMessage()));
} else if (state == State.CONNECTED_AUTHENTICATED) {
convertCallback.onConversionFailure(null, new ClientException(errorMessage.getMessage()));
} else {
convertCallback.onError(errorMessage.getMessage());
}
return;
}
if (baseMessage instanceof ClientIDMessage) {
Preconditions.checkState(state == State.CONNECTED);
final ClientIDMessage clientIDMessage = (ClientIDMessage) baseMessage;
long oldClientID = clientID;
clientID = clientIDMessage.getClientID();
if (clientID != oldClientID) {
Log.d(TAG, "New Client ID: " + clientID);
}
state = State.CONNECTED_AUTHENTICATED;
connectAuthCallback.onSuccess(clientID);
return;
}
Log.e(TAG, "No handler implemented for base message: " + baseMessage);
}
private void sendCommand(final Command command) {
Preconditions.checkArgument(command != null);
Preconditions.checkState(state == State.CONNECTED || state == State.CONNECTED_AUTHENTICATED);
Preconditions.checkState(webSocket != null);
final String dataToSend = command.toJson().toString();
Log.d(TAG, "Sending: " + dataToSend);
webSocket.send(dataToSend);
}
}