package com.dappervision.wearscript; import android.util.Base64; import android.util.Log; import com.codebutler.android_websockets.WebSocketClient; import org.apache.http.message.BasicNameValuePair; import org.msgpack.MessagePack; import org.msgpack.type.Value; import org.msgpack.type.ValueFactory; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; import static org.msgpack.template.Templates.TValue; import static org.msgpack.template.Templates.tList; public abstract class WearScriptConnection { private static final String TAG = "WearScriptConnection"; private static final String LISTEN_CHAN = "subscriptions"; private final int sleepTimeMax = 6000; static MessagePack msgpack = new MessagePack(); protected String device, group, groupDevice; private WebSocketClient client; private LocalListener listener; private boolean shutdown; private URI uri; private boolean connected; private boolean callOnConnect; // deviceToChannels is always updated, then externalChannels is rebuilt private TreeMap<String, ArrayList<String>> deviceToChannels; private TreeSet<String> externalChannels; private TreeSet<String> scriptChannels; private boolean reconnecting; public WearScriptConnection(String group, String device) { shutdown = false; callOnConnect = false; reconnecting = false; this.group = group; this.device = device; groupDevice = channel(group, device); scriptChannels = new TreeSet<String>(); resetExternalChannels(); pinger(); } public static byte[] encode(Object... data) { List<Value> out = new ArrayList<Value>(); for (Object i : data) { Class c = i.getClass(); if (c.equals(String.class)) out.add(ValueFactory.createRawValue((String) i)); else if (c.equals(Double.class)) out.add(ValueFactory.createFloatValue((Double) i)); else if (Value.class.isAssignableFrom(c)) out.add((Value) i); else if (c.equals(Boolean.class)) out.add(ValueFactory.createBooleanValue((Boolean) i)); else { Log.e(TAG, "Unhandled class: " + c); return null; } } try { return msgpack.write(out); } catch (IOException e) { Log.e(TAG, "Could not encode msgpack"); } return null; } public static Value listValue(Iterable<String> channels) { ArrayList<Value> channelsArray = new ArrayList<Value>(); for (String c : channels) channelsArray.add(ValueFactory.createRawValue(c)); return ValueFactory.createArrayValue(channelsArray.toArray(new Value[channelsArray.size()])); } public static Value mapValue(Map<String, ArrayList<String>> data) { ArrayList<Value> mapArray = new ArrayList<Value>(); for (String k : data.keySet()) { mapArray.add(ValueFactory.createRawValue(k)); mapArray.add(listValue(data.get(k))); } return ValueFactory.createMapValue(mapArray.toArray(new Value[mapArray.size()])); } public abstract void onConnect(); public abstract void onReceive(String channel, byte[] dataRaw, List<Value> data); public abstract void onDisconnect(); private void onReceiveDispatch(String channel, byte[] dataRaw, List<Value> data) { String channelPart = existsInternal(channel); if (channelPart != null) { Log.i(TAG, "ScriptChannel: " + channelPart); if (dataRaw != null && data == null) { try { data = msgpack.read(dataRaw, tList(TValue)); } catch (IOException e) { Log.e(TAG, "Could not decode msgpack"); return; } } onReceive(channelPart, dataRaw, data); } } private void resetExternalChannels() { deviceToChannels = new TreeMap<String, ArrayList<String>>(); externalChannels = new TreeSet<String>(); } public void publish(Object... data) { String channel = (String) data[0]; if (!exists(channel) && !channel.equals(LISTEN_CHAN)) return; Log.d(TAG, "Publishing...: " + channel); if (channel.equals(LISTEN_CHAN)) { Log.d(TAG, "Channels: " + Base64.encodeToString(encode(data), Base64.NO_WRAP)); } publish(channel, encode(data)); } public void publish(String channel, byte[] outBytes) { if (!exists(channel)) { Log.d(TAG, String.format("Not publishing[%s] Client: %s outBytes: %s", channel, client != null, outBytes != null)); return; } if (outBytes != null) { onReceiveDispatch(channel, outBytes, null); if (client != null && existsExternal(channel)) client.send(outBytes); } } private Value oneStringArray(String channel) { Value[] array = new Value[1]; array[0] = ValueFactory.createRawValue(channel); return ValueFactory.createArrayValue(array); } private Value channelsValue() { synchronized (this) { return listValue(scriptChannels); } } public void subscribe(String channel) { synchronized (this) { if (!scriptChannels.contains(channel)) { scriptChannels.add(channel); publish(LISTEN_CHAN, this.groupDevice, channelsValue()); } } } public void subscribe(Iterable<String> channels) { synchronized (this) { boolean added = false; for (String channel : channels) { if (!scriptChannels.contains(channel)) { added = true; scriptChannels.add(channel); } } if (added) publish(LISTEN_CHAN, this.groupDevice, channelsValue()); } } public String channel(Object... channels) { String out = ""; for (Object c : channels) { if (out.isEmpty()) out += (String) c; else { out += ":" + (String) c; } } return out; } public void unsubscribe(String channel) { synchronized (this) { if (scriptChannels.contains(channel)) { scriptChannels.remove(channel); publish(LISTEN_CHAN, this.groupDevice, channelsValue()); } } } public void unsubscribe(Iterable<String> channels) { synchronized (this) { boolean removed = false; for (String channel : channels) { if (scriptChannels.contains(channel)) { removed = true; scriptChannels.remove(channel); } } if (removed) publish(LISTEN_CHAN, this.groupDevice, channelsValue()); } } private void setDeviceChannels(String device, Value[] channels) { synchronized (this) { ArrayList<String> channelsArray = new ArrayList(); for (Value channel : channels) channelsArray.add(channel.asRawValue().getString()); deviceToChannels.put(device, channelsArray); TreeSet<String> externalChannelsNew = new TreeSet<String>(); for (ArrayList<String> deviceChannels : deviceToChannels.values()) for (String channel : deviceChannels) externalChannelsNew.add(channel); externalChannels = externalChannelsNew; } } public void connect(URI uri) { Log.d(TAG, "Lifecycle: Connect called"); synchronized (this) { if (shutdown) { Log.w(TAG, "Trying to connect while shutdown"); return; } Log.i(TAG, uri.toString()); if (uri.equals(this.uri) && connected) { onConnect(); return; } this.uri = uri; List<BasicNameValuePair> extraHeaders = Arrays.asList(); Log.i(TAG, "Lifecycle: Socket connecting"); if (client != null) client.disconnect(); client = new WebSocketClient(uri, new LocalListener(), extraHeaders); reconnect(); } } public String subchannel(String part) { return channel(part, this.groupDevice); } public TreeSet<String> channelsInternal() { return scriptChannels; } public TreeMap<String, ArrayList<String>> channelsExternal() { return deviceToChannels; } public String group() { return group; } public String groupDevice() { return groupDevice; } public String device() { return device; } public String ackchannel(String c) { return channel(c, "ACK"); } private boolean existsExternal(String channel) { if (channel.equals(LISTEN_CHAN)) return true; String channelPartial = ""; String[] parts = channel.split(":"); if (externalChannels.contains(channelPartial)) return true; for (String part : parts) { if (channelPartial.isEmpty()) channelPartial += part; else channelPartial += ":" + part; if (externalChannels.contains(channelPartial)) return true; } return false; } private String existsInternal(String channel) { String channelPartial = ""; String[] parts = channel.split(":"); String channelPartialExists = null; for (String part : parts) { if (channelPartial.isEmpty()) channelPartial += part; else channelPartial += ":" + part; if (scriptChannels.contains(channelPartial)) channelPartialExists = channelPartial; } return channelPartialExists; } public boolean exists(String channel) { return existsExternal(channel) || existsInternal(channel) != null; } public void disconnect() { if (client != null) client.disconnect(); } public void shutdown() { synchronized (this) { this.shutdown = true; disconnect(); if (client != null) client.getHandlerThread().getLooper().quit(); } } private void pinger() { new Thread(new Runnable() { public void run() { while (true) { if (shutdown) return; Log.w(TAG, "Lifecycle: Pinger..."); publish(LISTEN_CHAN, WearScriptConnection.this.groupDevice, channelsValue()); try { Thread.sleep(60000); } catch (InterruptedException e) { } } } }).start(); } private void publishChannels(int delay) { new Thread(new Runnable() { public void run() { Log.w(TAG, "Lifecycle: Pinger..."); Log.d(TAG, "Channels: " + channelsValue()); publish(LISTEN_CHAN, WearScriptConnection.this.groupDevice, channelsValue()); try { Thread.sleep(500); } catch (InterruptedException e) { } } }).start(); } private void reconnect() { synchronized (this) { if (shutdown) return; if (reconnecting) return; reconnecting = true; } new Thread(new Runnable() { public void run() { try { int sleepTime = 1000; while (true) { Log.w(TAG, "Lifecycle: Trying to reconnect..."); synchronized (WearScriptConnection.this) { if (connected || shutdown) { reconnecting = false; break; } Log.w(TAG, "Lifecycle: Reconnecting..."); // NOTE(brandyn): Reset channelToDevices, server will refresh on connect resetExternalChannels(); client.connect(); } try { Thread.sleep(sleepTime); } catch (InterruptedException e) { } sleepTime *= 2; if (sleepTime > sleepTimeMax) { sleepTime = sleepTimeMax; } } } finally { synchronized (WearScriptConnection.this) { reconnecting = false; // NOTE(brandyn): This ensures that we at least leave this thread with one of... // 1.) socket connected (disconnect after this would trigger a reconnect) // 2.) another thread that will try again if (!connected && !shutdown) reconnect(); } } } }).start(); } private class LocalListener implements WebSocketClient.Listener { LocalListener() { } @Override public void onConnect() { if (shutdown) return; connected = true; callOnConnect = true; } @Override public void onMessage(String s) { // Unused } @Override public void onDisconnect(int i, String s) { Log.w(TAG, "Lifecycle: Underlying socket disconnected: i: " + i + " s: " + s); synchronized (this) { connected = false; callOnConnect = false; if (shutdown) return; } WearScriptConnection.this.onDisconnect(); reconnect(); } @Override public void onError(Exception e) { Log.w(TAG, "Lifecycle: Underlying socket errored: " + e.getLocalizedMessage()); synchronized (this) { connected = false; callOnConnect = false; if (shutdown) return; } WearScriptConnection.this.onDisconnect(); reconnect(); } @Override public void onMessage(byte[] message) { if (shutdown) return; synchronized (this) { try { handleMessage(message); } catch (Exception e) { Log.e(TAG, String.format("onMessage: %s", e.toString())); } } } private void handleMessage(byte[] message) throws IOException { String channel = ""; List<Value> input = msgpack.read(message, tList(TValue)); channel = input.get(0).asRawValue().getString(); Log.d(TAG, String.format("Got %s", channel)); if (channel.equals(LISTEN_CHAN)) { String d = input.get(1).asRawValue().getString(); Value[] channels = input.get(2).asArrayValue().getElementArray(); setDeviceChannels(d, channels); if (callOnConnect) { callOnConnect = false; Log.i(TAG, "Lifecycle: Calling server callback"); //publish(LISTEN_CHAN, groupDevice, channelsValue()); WearScriptConnection.this.onConnect(); publishChannels(500); } } WearScriptConnection.this.onReceiveDispatch(channel, message, input); } } }