package com.dappervision.wearscript.managers; import android.util.Base64; import com.dappervision.wearscript.BackgroundService; import com.dappervision.wearscript.HardwareDetector; import com.dappervision.wearscript.Log; import com.dappervision.wearscript.Utils; import com.dappervision.wearscript.WearScriptConnection; import com.dappervision.wearscript.events.ChannelSubscribeEvent; import com.dappervision.wearscript.events.ChannelUnsubscribeEvent; import com.dappervision.wearscript.events.GistSyncEvent; import com.dappervision.wearscript.events.LambdaEvent; import com.dappervision.wearscript.events.SayEvent; import com.dappervision.wearscript.events.ScriptEvent; import com.dappervision.wearscript.events.SendEvent; import com.dappervision.wearscript.events.SendSubEvent; import com.dappervision.wearscript.events.ServerConnectEvent; import com.dappervision.wearscript.events.ShutdownEvent; import com.dappervision.wearscript.events.WarpHEvent; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.msgpack.MessagePack; import org.msgpack.type.Value; import org.msgpack.util.json.JSON; import java.util.List; import java.util.TreeMap; import java.util.TreeSet; public class ConnectionManager extends Manager { public static final String SENSORS_SUBCHAN = "sensors"; public static final String IMAGE_SUBCHAN = "image"; private static final String TAG = "ConnectionManager"; private static final String ONCONNECT = "ONCONNECT"; private static final String LISTEN_CHAN = "subscriptions"; MessagePack msgpack = new MessagePack(); private String GIST_LIST_SYNC_CHAN, GIST_GET_SYNC_CHAN; private WearScriptConnectionImpl connection; private TreeSet<String> testChannels; // NOTE(brandyn): Hack until we normalize the Java lib private int gistSyncPending; private boolean isCustomPackage; public ConnectionManager(BackgroundService bs) { super(bs); connection = new WearScriptConnectionImpl(); GIST_LIST_SYNC_CHAN = connection.channel(connection.groupDevice(), "gistListSync"); GIST_GET_SYNC_CHAN = connection.channel(connection.groupDevice(), "gistGetSync"); isCustomPackage = Utils.getPackageGist(bs) != null; reset(); } public String groupDevice() { if (connection == null) return null; return connection.groupDevice(); } public String group() { if (connection == null) return null; return connection.group(); } public String device() { if (connection == null) return null; return connection.device(); } public void reset() { synchronized (this) { for (String channel : connection.channelsInternal()) { unregisterCallback(channel); } String testChannel = "test:" + connection.groupDevice(); testChannels = new TreeSet<String>(); testChannels.add(testChannel); TreeSet<String> subscribeChannels = new TreeSet<String>(); subscribeChannels.add(testChannel); subscribeChannels.add("android"); subscribeChannels.add(connection.group()); subscribeChannels.add(connection.groupDevice()); subscribeChannels.add(GIST_LIST_SYNC_CHAN); subscribeChannels.add(GIST_GET_SYNC_CHAN); subscribeChannels.add("warph"); // TODO: Specialize it for this group/device TreeSet<String> unsubscribeChannels = new TreeSet<String>(connection.channelsInternal()); unsubscribeChannels.removeAll(subscribeChannels); connection.unsubscribe(unsubscribeChannels); connection.subscribe(subscribeChannels); } super.reset(); } private Object[] stringValuesToObjects(List<Value> data) { Object arr[] = new Object[data.size()]; for (int i = 0; i < data.size(); i++) { // BUG(brandyn): Currently correct but publish may take non string args arr[i] = data.get(i).asRawValue().getString(); } return arr; } private void testHandler(List<Value> data) { Log.d(TAG, "TestHandler"); String command = data.get(1).asRawValue().getString(); String chanArg = data.get(2).asRawValue().getString(); Log.d(TAG, "TestHandler: " + command + " " + chanArg); if (command.equals("subscribe")) { testChannels.add(chanArg); connection.subscribe(chanArg); } else if (command.equals("unsubscribe")) { testChannels.remove(chanArg); connection.unsubscribe(chanArg); } else if (command.equals("channelsInternal")) { connection.publish(chanArg, connection.listValue(connection.channelsInternal())); } else if (command.equals("channelsExternal")) { connection.publish(chanArg, connection.mapValue(connection.channelsExternal())); } else if (command.equals("group")) { connection.publish(chanArg, connection.group()); } else if (command.equals("device")) { connection.publish(chanArg, connection.device()); } else if (command.equals("groupDevice")) { connection.publish(chanArg, connection.groupDevice()); } else if (command.equals("exists")) { connection.publish(chanArg, connection.exists(data.get(3).asRawValue().getString())); } else if (command.equals("publish")) { connection.publish(stringValuesToObjects(data.subList(2, data.size()))); } else if (command.equals("channel")) { String channel = connection.channel(stringValuesToObjects(data.subList(2, data.size()))); connection.publish(chanArg, channel); } else if (command.equals("subchannel")) { connection.publish(chanArg, connection.subchannel(data.get(3).asRawValue().getString())); } else if (command.equals("ackchannel")) { connection.publish(chanArg, connection.ackchannel(data.get(3).asRawValue().getString())); } } public void onEventBackgroundThread(SendEvent e) { String channel = e.getChannel(); Log.d(TAG, "Sending Channel: " + channel); if (!connection.exists(channel)) { Log.d(TAG, "Channel doesn't exist: " + channel); return; } connection.publish(channel, e.getData()); } public void onEventBackgroundThread(ServerConnectEvent e) { registerCallback(ONCONNECT, e.getCallback()); connection.connect(e.getServer()); } public void onEventBackgroundThread(SendSubEvent e) { String channel = connection.subchannel(e.getSubChannel()); Log.d(TAG, "Sending Channel: " + channel); if (!connection.exists(channel)) { Log.d(TAG, "Channel doesn't exist: " + channel); return; } onEventBackgroundThread(new SendEvent(channel, e.getData())); } public void onEvent(ChannelSubscribeEvent e) { synchronized (this) { registerCallback(e.getChannel(), e.getCallback()); connection.subscribe(e.getChannel()); } } public void onEvent(GistSyncEvent e) { Utils.eventBusPost(new SendEvent("gist", "list", GIST_LIST_SYNC_CHAN)); } public void onEvent(ChannelUnsubscribeEvent e) { synchronized (this) { for (String channel : e.getChannels()) { unregisterCallback(channel); // NOTE(brandyn): Ensures a script can stop getting callbacks but won't break the java side if (!channel.equals(connection.groupDevice()) && !channel.equals(connection.group())) connection.unsubscribe(channel); } } } public void shutdown() { synchronized (this) { TreeSet<String> unsubscribeChannels = new TreeSet<String>(connection.channelsInternal()); connection.unsubscribe(unsubscribeChannels); super.shutdown(); connection.shutdown(); } } public String subchannel(String subchan) { return connection.subchannel(subchan); } public boolean exists(String channel) { return connection.exists(channel); } class WearScriptConnectionImpl extends WearScriptConnection { WearScriptConnectionImpl() { super(HardwareDetector.isGlass ? "android:glass" : "android:phone", ((WifiManager) service.getManager(WifiManager.class)).getMacAddress().replace(":", "")); } @Override public void onConnect() { makeCall(ONCONNECT, ""); // NOTE(brandyn): This ensures that we are only calling the function once unregisterCallback(ONCONNECT); } private TreeMap toMap(Value map) { TreeMap<String, Value> mapOut = new TreeMap<String, Value>(); Value[] kv = map.asMapValue().getKeyValueArray(); for (int i = 0; i < kv.length / 2; i++) { mapOut.put(kv[i * 2].asRawValue().getString(), kv[i * 2 + 1]); } return mapOut; } @Override public void onReceive(String channel, byte[] dataRaw, List<Value> data) { // BUG(brandyn): If the channel should go to a subchannel now it won't make it, // we should modify channel name before this call makeCall(channel, "'" + Base64.encodeToString(dataRaw, Base64.NO_WRAP) + "'"); if (isCustomPackage) return; // TODO(brandyn): Clean up gist sync behavior Log.d(TAG, "onReceive: " + channel); if ((channel.equals(this.groupDevice) || channel.equals(this.group) || channel.equals("android")) && !channel.equals(GIST_LIST_SYNC_CHAN)) { String command = data.get(1).asRawValue().getString(); Log.d(TAG, String.format("Got %s %s", channel, command)); if (command.equals("script")) { Value[] files = data.get(2).asMapValue().getKeyValueArray(); // TODO(brandyn): Verify that writing a script here isn't going to break anything while the old script is running String path = null; TreeMap<String, String> filePaths = new TreeMap<String, String>(); JSONObject manifest = null; for (int i = 0; i < files.length / 2; i++) { String name = files[i * 2].asRawValue().getString(); String pathCur = Utils.SaveData(files[i * 2 + 1].asRawValue().getByteArray(), "scripting/", false, name); filePaths.put(name, pathCur); if (name.equals("manifest.json")) { manifest = (JSONObject) JSONValue.parse(files[i * 2 + 1].asRawValue().getString()); } } if (manifest != null && manifest.containsKey("scripts")) { JSONObject scripts = (JSONObject)manifest.get("scripts"); String scriptName = (String)scripts.get(this.groupDevice()); if (scriptName == null) { scriptName = (String)scripts.get(this.group()); } if (scriptName == null) { scriptName = (String)scripts.get("android"); } if (scriptName != null) { Log.d(TAG, "Using script: " + scriptName); path = filePaths.get(scriptName); } } else { path = filePaths.get("glass.html"); } if (path != null) { Utils.eventBusPost(new ScriptEvent(path)); } else { Log.w(TAG, "Got script event but not glass.html, not running"); } } else if (command.equals("lambda")) { Utils.eventBusPost(new LambdaEvent(data.get(2).asRawValue().getString())); } else if (command.equals("error")) { shutdown(); String error = data.get(2).asRawValue().getString(); Log.e(TAG, "Lifecycle: Got server error: " + error); Utils.eventBusPost(new SayEvent(error, true)); } else if (command.equals("version")) { int versionExpected = 1; int version = data.get(2).asIntegerValue().getInt(); if (version != versionExpected) { Utils.eventBusPost(new SayEvent("Version mismatch! Got " + version + " and expected " + versionExpected + ". Visit wear script .com for information.", true)); } } } if (channel.equals(GIST_LIST_SYNC_CHAN)) { /* 1. TODO: Remove old scripts 2. Publish request to get each gist */ Log.d(TAG, "Gist list sync"); gistSyncPending = data.get(1).asArrayValue().size(); for (Value v : data.get(1).asArrayValue()) { Value gistid = (Value) toMap(v).get("id"); if (gistid != null) { Log.d(TAG, "GistID: " + gistid); Utils.eventBusPost(new SendEvent("gist", "get", GIST_GET_SYNC_CHAN, gistid.asRawValue().getString())); } } } Log.d(TAG, "Got Channel: " + channel); // HACK(brandyn): This isn't 100%, just until we normalize lib for (String testChannel : testChannels) { if (channel.startsWith(testChannel)) { testHandler(data); break; } } if (channel.equals(GIST_GET_SYNC_CHAN)) { /* 1. Make directory for script 2. Save script content */ Log.d(TAG, "Gist get sync"); Log.d(TAG, "GistGet:" + data.get(1).toString()); TreeMap<String, Value> gist = toMap(data.get(1)); TreeMap<String, Value> files = toMap(gist.get("files")); String gistid = gist.get("id").asRawValue().getString(); for (Value v : files.values()) { TreeMap<String, Value> file = toMap(v); byte[] content = file.get("content").asRawValue().getByteArray(); String filename = file.get("filename").asRawValue().getString(); String pathCur = Utils.SaveData(content, "gists/" + gistid, false, filename); Log.d(TAG, "File:" + filename + " : " + gistid); } // Shutdown after sync if (--gistSyncPending == 0) Utils.eventBusPost(new ShutdownEvent()); } // TODO: Specialize it for this group/device if (channel.startsWith("warph:")) { double h[] = new double[9]; int cnt = 0; for (Value v : data.get(1).asArrayValue()) { h[cnt++] = v.asFloatValue().getDouble(); } Utils.eventBusPost(new WarpHEvent(h)); } } @Override public void onDisconnect() { } } }