package net.osmand.plus.osmo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.widget.Toast;
import net.osmand.PlatformUtil;
import net.osmand.plus.osmo.OsMoService.SessionInfo;
import org.apache.commons.logging.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
public class OsMoThread {
// private static String TRACKER_SERVER = "srv.osmo.mobi";
// private static int TRACKER_PORT = 3245;
private static final String PING_CMD = "P";
protected final static Log log = PlatformUtil.getLog(OsMoThread.class);
private static final long HEARTBEAT_DELAY = 100;
private static final long HEARTBEAT_FAILED_DELAY = 10000;
private static final long TIMEOUT_TO_RECONNECT = 60 * 1000;
private static final int SOCKET_TIMEOUT = 60 * 1000;
private static final long TIMEOUT_TO_PING = 5 * 60 * 1000;
private static final long LIMIT_OF_FAILURES_RECONNECT = 10;
private static final long SELECT_TIMEOUT = 500;
private static int HEARTBEAT_MSG = 3;
private Handler serviceThread;
private int failures = 0;
private int activeConnectionId = 0;
private boolean stopThread;
private boolean reconnect;
private Selector selector;
private int authorized = 0; // 1 - send, 2 - authorized
private OsMoService service;
private SessionInfo sessionInfo = null;
private SocketChannel activeChannel;
private long connectionTime;
private long lastSendCommand = 0;
private long pingSent = 0;
private ByteBuffer pendingSendCommand;
private String readCommand = "";
private ByteBuffer pendingReadCommand = ByteBuffer.allocate(2048);
private LinkedList<String> queueOfMessages = new LinkedList<String>();
private SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss", Locale.US);
private ConcurrentLinkedQueue<String> lastCommands = new ConcurrentLinkedQueue<String>();
private final static int STACK_CMD = 30;
public OsMoThread(OsMoService service) {
this.service = service;
// start thread to receive events from OSMO
HandlerThread h = new HandlerThread("OSMo Service");
h.start();
serviceThread = new Handler(h.getLooper());
scheduleHeartbeat(HEARTBEAT_DELAY);
}
public void stopConnection() {
stopThread = true;
}
protected void initConnection() throws IOException {
// always ask session token
// if (sessionInfo == null) {
sessionInfo = service.prepareSessionToken();
// }
if(sessionInfo == null) {
return;
}
this.activeChannel = null;
authorized = 0;
reconnect = false;
pingSent = 0;
failures = 0;
lastSendCommand = 0;
selector = Selector.open();
SocketChannel activeChannel = SocketChannel.open();
activeChannel.configureBlocking(true);
activeChannel.connect(new InetSocketAddress(sessionInfo.hostName, Integer.parseInt(sessionInfo.port)));
activeChannel.configureBlocking(false);
activeChannel.socket().setSoTimeout(SOCKET_TIMEOUT);
SelectionKey key = activeChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
connectionTime = System.currentTimeMillis();
if (this.activeChannel != null) {
stopChannel();
}
this.activeChannel = activeChannel;
key.attach(new Integer(++activeConnectionId));
for(OsMoReactor sender : getReactors()) {
sender.onConnected();
}
}
private Collection<OsMoReactor> getReactors() {
return service.getListReactors();
}
public String format(String cmd, Map<String, Object> params) {
JSONObject json;
try {
json = new JSONObject();
Iterator<Entry<String, Object>> it = params.entrySet().iterator();
while(it.hasNext()) {
Entry<String, Object> e = it.next();
json.put(e.getKey(), e.getValue());
}
return cmd + "|"+json.toString();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public void scheduleHeartbeat(long delay) {
Message msg = serviceThread.obtainMessage();
msg.what = HEARTBEAT_MSG;
serviceThread.postDelayed(new Runnable() {
@Override
public void run() {
checkAsyncSocket();
}
}, delay);
}
public boolean isConnected() {
return activeChannel != null;
}
public boolean isActive() {
return activeChannel != null && pingSent == 0 && authorized == 2;
}
protected void checkAsyncSocket() {
long delay = HEARTBEAT_DELAY;
try {
// if (selector == null) {
// stopThread = true;
if(activeChannel == null || reconnect) {
initConnection();
if(activeChannel == null) {
delay = HEARTBEAT_FAILED_DELAY;
}
} else {
checkSelectedKeys();
}
} catch (Exception e) {
log.info("Exception selecting socket", e);
exc("ERROR HEARTBEAT : ", e);
e.printStackTrace();
if (activeChannel != null && !activeChannel.isConnected()) {
activeChannel = null;
}
final String msg = e.getMessage();
for(OsMoReactor sender : getReactors()) {
sender.onDisconnected(msg);
}
delay = HEARTBEAT_FAILED_DELAY;
if (e instanceof OsMoConnectionException) {
stopThread = true;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(service.getMyApplication(), msg, Toast.LENGTH_LONG).show();
}
});
} else {
if (lastSendCommand != 0 && System.currentTimeMillis() - lastSendCommand > TIMEOUT_TO_RECONNECT) {
reconnect = true;
} else if (failures++ > LIMIT_OF_FAILURES_RECONNECT) {
reconnect = true;
}
}
}
if (stopThread) {
stopChannel();
for(OsMoReactor sender : getReactors()) {
sender.onDisconnected(null);
}
serviceThread.getLooper().quit();
} else {
scheduleHeartbeat(delay);
}
}
protected void exc(String header, Exception e) {
String eMsg = e.getMessage();
if(e.getStackTrace() != null && e.getStackTrace().length > 0) {
eMsg += " " + e.getStackTrace()[0].toString();
}
cmd(header + eMsg, true);
}
private void stopChannel() {
if (activeChannel != null) {
try {
activeChannel.close();
} catch (IOException e) {
}
}
activeChannel = null;
}
private void checkSelectedKeys() throws IOException {
/* int s = */selector.select(SELECT_TIMEOUT);
Set<SelectionKey> keys = selector.selectedKeys();
if (keys == null) {
return;
}
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
final boolean isActive = new Integer(activeConnectionId).equals(key.attachment());
// final boolean isActive = activeChannel == key.channel();
if (isActive) {
if (key.isWritable()) {
writeCommands();
}
if (key.isReadable()) {
readCommands();
}
} else {
try {
key.channel().close();
} catch (Exception e) {
log.info("Exception closing channel", e);
e.printStackTrace();
}
}
iterator.remove();
}
}
private void readCommands() throws IOException {
boolean hasSomethingToRead = true;
while (hasSomethingToRead) {
pendingReadCommand.clear();
int read = activeChannel.read(pendingReadCommand);
if (!pendingReadCommand.hasRemaining()) {
hasSomethingToRead = true;
} else {
hasSomethingToRead = false;
}
if(read == -1) {
reconnect = true;
} else if (read > 0) {
byte[] ar = pendingReadCommand.array();
String res = new String(ar, 0, read);
readCommand += res;
int i;
while ((i = readCommand.indexOf('\n')) != -1) {
String cmd = readCommand.substring(0, i);
readCommand = readCommand.substring(i + 1);
queueOfMessages.add(cmd.replace("\\n", "\n"));
}
}
}
if (queueOfMessages.size() > 0) {
processReadMessages();
}
}
private void processReadMessages() {
while(!queueOfMessages.isEmpty()){
String cmd = queueOfMessages.poll();
cmd(cmd, false);
int k = cmd.indexOf('|');
String header = cmd;
String id = "";
String data = "";
if(k >= 0) {
header = cmd.substring(0, k);
data = cmd.substring(k + 1);
}
int ks = header.indexOf(':');
if (ks >= 0) {
id = header.substring(ks + 1);
header = header.substring(0, ks);
}
JSONObject obj = null;
if(data.startsWith("{")) {
try {
obj = new JSONObject(data);
} catch (JSONException e) {
e.printStackTrace();
}
}
boolean error = false;
if(obj != null && obj.has("error")) {
error = true;
try {
String s = obj.getString("error");
if(obj.has("error_description")) {
s += " " +obj.getString("error_description");
}
service.showErrorMessage(s);
} catch (JSONException e) {
e.printStackTrace();
}
}
if(header.equalsIgnoreCase("TOKEN")) {
if(!error){
authorized = 2;
try {
parseAuthCommand(data, obj);
} catch (JSONException e) {
service.showErrorMessage(e.getMessage());
}
}
continue;
} else if(header.equalsIgnoreCase(OsMoService.REGENERATE_CMD)) {
reconnect = true;
continue;
} else if(header.equalsIgnoreCase(PING_CMD)) {
pingSent = 0;
continue;
// lastSendCommand = System.currentTimeMillis(); // not needed handled by send
}
boolean processed = false;
for (OsMoReactor o : getReactors()) {
try {
if (o.acceptCommand(header, id, data, obj, this)) {
processed = true;
break;
}
} catch (Exception e) {e.printStackTrace();
exc("ERROR REACTOR:", e);
}
}
if (!processed) {
log.warn("Command not processed '" + cmd + "'");
}
}
lastSendCommand = System.currentTimeMillis();
}
private void parseAuthCommand(String data, JSONObject obj) throws JSONException {
if(sessionInfo != null) {
if(obj.has("protocol")) {
sessionInfo.protocol = obj.getString("protocol");
}
if(obj.has("now")) {
sessionInfo.serverTimeDelta = obj.getLong("now") - System.currentTimeMillis();
}
if(obj.has("name")) {
sessionInfo.username = obj.getString("name");
}
if (obj.has("motd")) {
long l = obj.getLong("motd");
if(l != sessionInfo.motdTimestamp ){
sessionInfo.motdTimestamp = l;
service.pushCommand("MOTD");
}
}
if(obj.has("tracker_id")) {
sessionInfo.trackerId= obj.getString("tracker_id");
}
if(obj.has("group_tracker_id")) {
sessionInfo.groupTrackerId= obj.getString("group_tracker_id");
}
}
}
public long getConnectionTime() {
return connectionTime;
}
private void writeCommands() throws UnsupportedEncodingException, IOException {
if(authorized == 0) {
String auth = "TOKEN|"+ sessionInfo.token;
cmd(auth, true);
authorized = 1;
pendingSendCommand = ByteBuffer.wrap(prepareCommand(auth).toString().getBytes("UTF-8"));
}
if (pendingSendCommand == null) {
pendingSendCommand = getNewPendingSendCommand();
}
while (pendingSendCommand != null) {
activeChannel.write(pendingSendCommand);
if (!pendingSendCommand.hasRemaining()) {
lastSendCommand = System.currentTimeMillis();
pendingSendCommand = getNewPendingSendCommand();
} else {
break;
}
}
}
private ByteBuffer getNewPendingSendCommand() throws UnsupportedEncodingException {
if(authorized == 1) {
return null;
}
for (OsMoReactor s : getReactors()) {
String l = null;
try {
l = s.nextSendCommand(this);
} catch (Exception e) {
exc("ERROR SENDER:", e);
}
if (l != null) {
cmd(l, true);
return ByteBuffer.wrap(prepareCommand(l).toString().getBytes("UTF-8"));
}
}
final long interval = System.currentTimeMillis() - lastSendCommand;
if(interval > TIMEOUT_TO_PING) {
final long pingInterval = System.currentTimeMillis() - pingSent;
if(pingSent == 0 || pingInterval > TIMEOUT_TO_PING) {
pingSent = System.currentTimeMillis();
cmd(PING_CMD, true);
return ByteBuffer.wrap(prepareCommand(PING_CMD).toString().getBytes("UTF-8"));
}
} else if(pingSent != 0) {
pingSent = 0;
}
return null;
}
public ConcurrentLinkedQueue<String> getLastCommands() {
return lastCommands;
}
private void cmd(String cmd, boolean send) {
log.info("OsMO" + (send ? "> " : ">> ") + cmd);
lastCommands.add((send ? "> " : ">> ") + df.format(new Date()) + " " + cmd);
while(lastCommands.size() > STACK_CMD) {
lastCommands.poll();
}
}
public SessionInfo getSessionInfo() {
return sessionInfo;
}
private String prepareCommand(String l) {
StringBuilder res = new StringBuilder(l.length());
for (int i = 0; i < l.length(); i++) {
char c = l.charAt(i);
if (c == '\n' || c == '=' || c == '\\') {
res.append('\\');
}
res.append(c);
}
String finalCmd = res.toString().trim();
return finalCmd + "=\n";
}
public long getLastCommandTime() {
return lastSendCommand;
}
public void reconnect() {
sessionInfo = null;
reconnect = true;
}
}