package org.sagemath.droid.fragments; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.Log; import android.util.Pair; import android.webkit.CookieSyncManager; import com.google.gson.Gson; import com.squareup.okhttp.FormEncodingBuilder; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.otto.Subscribe; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.message.BasicNameValuePair; import org.sagemath.droid.constants.ExecutionState; import org.sagemath.droid.constants.StringConstants; import org.sagemath.droid.events.InteractUpdateEvent; import org.sagemath.droid.events.ProgressEvent; import org.sagemath.droid.events.ServerDisconnectEvent; import org.sagemath.droid.events.ShareAvailableEvent; import org.sagemath.droid.models.gson.*; import org.sagemath.droid.utils.BusProvider; import org.sagemath.droid.utils.CookieManagerProvider; import org.sagemath.droid.utils.UrlUtils; import org.sagemath.droid.websocket.WebSocketClient; import java.io.IOException; import java.net.CookieManager; import java.net.HttpCookie; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.sagemath.droid.events.ServerDisconnectEvent.DisconnectType.*; /** * <p>The Headless Fragment which performs all computations</p> * * @author Nikhil Peter Raj */ public class AsyncTaskFragment extends Fragment { private static final String TAG = "SageDroid:AsyncTaskFragment"; public static interface ServerCallbacks { public void onReply(BaseReply reply); public void onComputationFinished(); } private ServerCallbacks callBacks; private static final String HEADER_ACCEPT_ENCODING = "Accept_Encoding"; private static final String HEADER_TOS = "accepted_tos"; private static final String VALUE_IDENTITY = "identity"; private static final String VALUE_CODE = "code"; private OkHttpClient httpClient; private CookieManager cookieManager; private Gson gson; private SageAsyncTask asyncTask; private WebSocketClient shellClient, ioPubClient; private String queryCode; private String kernelID; private String permalinkURL; private boolean isInteractInput = false; Request currentExecuteRequest; public static AsyncTaskFragment getInstance() { return new AsyncTaskFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); init(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); BusProvider.getInstance().register(this); callBacks = (ServerCallbacks) activity; } @Override public void onDetach() { super.onDetach(); BusProvider.getInstance().unregister(this); callBacks = null; } public void query(String sageInput) { //Initialize a new ExecuteRequest object currentExecuteRequest = new Request(sageInput); Log.i(TAG, "Creating new ExecuteRequest: " + gson.toJson(currentExecuteRequest)); queryCode = gson.toJson(currentExecuteRequest); asyncTask = new SageAsyncTask(); asyncTask.execute(currentExecuteRequest); } private void init() { httpClient = new OkHttpClient(); cookieManager = CookieManagerProvider.getInstance(); httpClient.setCookieHandler(cookieManager); gson = new Gson(); } public void closeWebSockets() { if (shellClient != null && ioPubClient != null) { shellClient.disconnect(); ioPubClient.disconnect(); } Log.i(TAG, "Sockets closed"); } public void cancel() { if (asyncTask != null) { asyncTask.cancel(true); } closeWebSockets(); } public URI getShareURI() { URI shareURI = URI.create(permalinkURL); if (shareURI != null) return shareURI; //TODO Error Handler msg here return null; } private PermalinkResponse sendPermalinkRequest(Request request) throws IOException { PermalinkResponse permalinkResponse; String url = UrlUtils.getPermalinkURL(); RequestBody formBody = new FormEncodingBuilder() .add(VALUE_CODE, request.getContent().getCode()) .build(); com.squareup.okhttp.Request permalinkRequest = new com.squareup.okhttp.Request.Builder() .url(url) .post(formBody) .build(); Response response = httpClient.newCall(permalinkRequest).execute(); if (!response.isSuccessful()) BusProvider.getInstance().post(new ServerDisconnectEvent(DISCONNECT_HTTP_ERROR)); permalinkResponse = gson.fromJson(response.body().charStream(), PermalinkResponse.class); Log.i(TAG, "Permalink Response" + gson.toJson(permalinkResponse)); return permalinkResponse; } private WebSocketResponse sendInitialRequest() throws IOException { WebSocketResponse webSocketResponse; String url = UrlUtils.getInitialKernelURL(); RequestBody formBody = new FormEncodingBuilder() .add(HEADER_ACCEPT_ENCODING, VALUE_IDENTITY) .add(HEADER_TOS, "true") .build(); com.squareup.okhttp.Request initialRequest = new com.squareup.okhttp.Request.Builder() .url(url) .post(formBody) .build(); Response response = httpClient.newCall(initialRequest).execute(); if(!response.isSuccessful()) BusProvider.getInstance().post(new ServerDisconnectEvent(DISCONNECT_HTTP_ERROR)); webSocketResponse = gson.fromJson(response.body().charStream(), WebSocketResponse.class); Log.i(TAG, "Received Websocket Response: " + gson.toJson(webSocketResponse)); Log.i(TAG, "Cookies" + Arrays.asList(cookieManager.getCookieStore().getCookies())); return webSocketResponse; } public void addReply(BaseReply reply) { Log.i(TAG, "Adding Reply:" + reply.getStringMessageType()); if (reply instanceof ImageReply) { ImageReply imageReply = (ImageReply) reply; imageReply.setKernelID(kernelID); if (callBacks != null) callBacks.onReply(imageReply); } else if (reply instanceof InteractReply) { Log.i(TAG, "Reply is Interact, calling onSageInteractListener"); isInteractInput = true; if (callBacks != null) callBacks.onReply(reply); } else if (reply instanceof StatusReply) { //If the reply is a status with idle/dead execution state, a computation //has finished or terminated, inform the SageActivity StatusReply statusReply = (StatusReply) reply; if (statusReply.getContent().getExecutionState() == ExecutionState.IDLE || statusReply.getContent().getExecutionState() == ExecutionState.DEAD) { if (callBacks != null) { callBacks.onReply(reply); callBacks.onComputationFinished(); } } } else if (reply instanceof PythonInputReply) { if (((PythonInputReply) reply).isInteractUpdateReply()) { BusProvider.getInstance().post(new InteractUpdateEvent(null, null, null)); } } else { Log.i(TAG, "Reply to current execute request"); callBacks.onReply(reply); } } public String formatInteractUpdate(String interactID, String name, String value) { String template = "sys._sage_.update_interact(\"%s\",\"%s\",%s)"; return String.format(template, interactID, name, value); } @SuppressWarnings("unused") @Subscribe public void onInteractUpdate(InteractUpdateEvent event) { if (event.getReply() != null) { Log.i(TAG, "UPDATING INTERACT VARIABLE: " + event.getVarName()); Log.i(TAG, "UPDATED INTERACT VALUE: " + event.getValue().toString()); String interactID = event.getReply().getContent().getData().getInteract().getNewInteractID(); String sageInput = formatInteractUpdate(interactID, event.getVarName(), event.getValue().toString()); Log.i(TAG, "Updating Interact: " + sageInput); currentExecuteRequest = new Request(sageInput, event.getReply().getHeader().getSession()); Log.i(TAG, "Sending Interact Update Request:" + gson.toJson(currentExecuteRequest)); shellClient.send(gson.toJson(currentExecuteRequest)); } } private void parseResponses(Pair<BaseResponse, BaseResponse> responses) { BaseResponse firstResponse = responses.first; BaseResponse secondResponse = responses.second; if (firstResponse instanceof PermalinkResponse) { permalinkURL = ((PermalinkResponse) firstResponse).getQueryURL(); BusProvider.getInstance().post(new ShareAvailableEvent(permalinkURL)); } if (secondResponse instanceof WebSocketResponse) { if (((WebSocketResponse) secondResponse).isValidResponse()) { Log.d(TAG, "Response is valid"); //Cache to avoid huge lengths WebSocketResponse response = (WebSocketResponse) secondResponse; kernelID = response.getKernelID(); String shellURL, ioPubURL; shellURL = UrlUtils.getShellURL(response.getKernelID(), response.getWebSocketURL()); ioPubURL = UrlUtils.getIoPubURL(response.getKernelID(), response.getWebSocketURL()); setupWebSockets(shellURL, ioPubURL, shellCallback, ioPubCallback); } } } private void setupWebSockets(String shellURL, String ioPubURL , WebSocketClient.Listener shellCallback , WebSocketClient.Listener ioPubCallback) { List<HttpCookie> cookies = cookieManager.getCookieStore().getCookies(); ArrayList<BasicNameValuePair> headers = new ArrayList<>(); StringBuilder builder = new StringBuilder(); //Add the cookies to websocket request CookieSyncManager.createInstance(getActivity()); android.webkit.CookieManager manager = android.webkit.CookieManager.getInstance(); manager.setAcceptCookie(true); manager.removeSessionCookie(); CookieSyncManager.getInstance().sync(); for (int i = 0; i < cookies.size(); i++) { //Add the same cookies to WebKit so WebViews can use it manager.setCookie(cookies.get(i).getDomain(),cookies.get(i).getName() + "=" + cookies.get(i).getValue()); if (i < cookies.size() - 1) builder.append(cookies.get(i).getName() + "=" + cookies.get(i).getValue() + ";"); else builder.append(cookies.get(i).getName() + "=" + cookies.get(i).getValue()); } //Sync the Cookies for WebViews CookieSyncManager.getInstance().sync(); Log.i(TAG, "Adding Cookies: " + builder.toString()); headers.add(new BasicNameValuePair("Cookie", builder.toString())); shellClient = new WebSocketClient(URI.create(shellURL), shellCallback, headers); ioPubClient = new WebSocketClient(URI.create(ioPubURL), ioPubCallback, headers); shellClient.connect(); ioPubClient.connect(); } private WebSocketClient.Listener shellCallback = new WebSocketClient.Listener() { @Override public void onConnect() { Log.i(TAG, "Shell Connected, Sending " + queryCode); shellClient.send(queryCode); } @Override public void onMessage(String message) { Log.i(TAG, "Shell Received Message" + message); } @Override public void onMessage(byte[] data) { } @Override public void onDisconnect(int code, String reason) { Log.i(TAG, "Shell Closed" + reason); } @Override public void onError(Exception error) { Log.i(TAG, "Shell Error: " + error.getMessage()); } }; private WebSocketClient.Listener ioPubCallback = new WebSocketClient.Listener() { @Override public void onConnect() { } @Override public void onMessage(String message) { try { Log.i(TAG, "Got Shell Message: " + message); final BaseReply reply = BaseReply.parse(message); getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (reply != null) addReply(reply); } }); } catch (Exception e) { Log.i(TAG, e.getMessage()); e.printStackTrace(); } } @Override public void onMessage(byte[] data) { } @Override public void onDisconnect(int code, String reason) { //If Activity does not exist, i.e Fragment is detached, early breakout if(getActivity()==null) return; if (isInteractInput) { Log.i(TAG, "Executing Disconnect Callback"); getActivity().runOnUiThread(new Runnable() { @Override public void run() { BusProvider.getInstance().post(new ServerDisconnectEvent(DISCONNECT_INTERACT)); } }); } } @Override public void onError(Exception error) { Log.i(TAG, "IOPub Error:" + error); } }; private class SageAsyncTask extends AsyncTask<Request, Void, Pair<BaseResponse, BaseResponse>> { @Override protected void onPreExecute() { BusProvider.getInstance().post(new ProgressEvent(StringConstants.ARG_PROGRESS_START)); } @Override protected Pair<BaseResponse, BaseResponse> doInBackground(Request... params) { try { BaseResponse permalinkResponse, webSocketResponse; permalinkResponse = sendPermalinkRequest(params[0]); webSocketResponse = sendInitialRequest(); return Pair.create(permalinkResponse, webSocketResponse); } catch (ConnectTimeoutException timeoutException) { BusProvider.getInstance().post(new ServerDisconnectEvent(DISCONNECT_TIMEOUT)); } catch (Exception e) { Log.e(TAG, "Could not send request: " + e.getMessage()); } return null; } @Override protected void onPostExecute(Pair<BaseResponse, BaseResponse> responses) { parseResponses(responses); } } }