diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7d2c76f
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..3b31283
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 7aff108..e4c3ec9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -16,11 +16,25 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
+
+ }
+ productFlavors {
+ sandbox {
+ applicationIdSuffix ".sandbox"
+ versionNameSuffix "-SANDBOX"
+ }
+ full { }
+ }
+
+ variantFilter { variant ->
+ // Ignore sandboxRelease since it makes no sense
+ if(variant.name == 'sandboxRelease')
+ setIgnore true
}
}
dependencies {
- compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6717f1a..1b6cc7b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,9 @@
+
+
+
STATIC MEMBERS
+// |===============================
+
+ private static NetAdapter instance;
+
+ public static NetAdapter getInstance()
+ {
+ if(instance == null) instance = new NetAdapter();
+ return instance;
+ }
+
+
+
+// |===============================
+// |==> CLASSES
+// |===============================
+
+ public enum State
+ {
+ IDLE,
+ CONNECTING,
+ CONNECTED,
+ ERROR
+ }
+
+
+ public interface OnNetworkEventListener
+ {
+ void onNetworkStateChanged(State newState);
+
+ void onNetworkFailure();
+ }
+
+
+ NetBridge.OnConnectionStateListener bridgeListener = new NetBridge.OnConnectionStateListener()
+ {
+ @Override
+ public void onConnectionStateChanged(NetBridge.BridgeState newState)
+ {
+ switch (newState)
+ {
+ case IDLE:
+ state = State.IDLE;
+ break;
+ case CONNECTING:
+ state = State.CONNECTING;
+ break;
+ case CONNECTED:
+ state = State.CONNECTED;
+ break;
+ case ERROR:
+ state = State.ERROR;
+ break;
+ }
+
+ notifyNetworkState();
+ }
+ };
+
+
+ private final ConnectDialogFragment.OnConnectDialogEventListener connectionDialogListener =
+ new ConnectDialogFragment.OnConnectDialogEventListener()
+ {
+ @Override
+ public void onDialogConnectRequest(String address)
+ {
+ try
+ {
+ netBridge.startConnection(InetAddress.getByName(address));
+ }
+ catch (UnknownHostException e)
+ {
+ e.printStackTrace();
+ throw new AssertionError("Dialog returned invalid address");
+ }
+ }
+ };
+
+
+// |===============================
+// |==> FIELDS
+// |===============================
+
+ private State state;
+
+ private WifiBridge netBridge;
+
+ private ConnectDialogFragment connectDialogFragment;
+
+ private OnNetworkEventListener listener;
+
+ private Sender sender;
+
+
+// |===============================
+// |==> CONSTRUCTORS
+// |===============================
+
+ private NetAdapter()
+ {
+ netBridge = new WifiBridge(null);
+ netBridge.setConnectionStateListener(bridgeListener);
+
+ connectDialogFragment = new ConnectDialogFragment();
+ connectDialogFragment.setDialogEventListener(connectionDialogListener);
+
+ sender = new Sender(this);
+ }
+
+
+// |===============================
+// |==> METHODS
+// |===============================
+
+ private void notifyNetworkState()
+ {
+ if(listener != null)
+ {
+ if(getNetworkState() == State.ERROR)
+ listener.onNetworkFailure();
+ else
+ listener.onNetworkStateChanged(getNetworkState());
+ }
+ }
+
+ @Override
+ public void sendData(String data, NetBridge.DataReliability reliability) //TODO: move this inside NetBridge?
+ {
+ netBridge.sendData(data, reliability);
+ }
+
+ public void connectDialog(Activity activity)
+ {
+ if(!isConnected())
+ connectDialogFragment.show(activity.getFragmentManager(), null);
+ else
+ Log.e(TAG, "Socket is already connected");
+ }
+
+ public void disconnect()
+ {
+ connectDialogFragment.dismiss();
+
+ netBridge.stopConnection();
+ }
+
+ public boolean isConnected()
+ {
+ return netBridge.getConnectionState() == NetBridge.BridgeState.CONNECTED;
+ }
+
+ public void registerListener(@NonNull OnNetworkEventListener listener)
+ {
+ this.listener = listener;
+ notifyNetworkState();
+ }
+
+ public void unregisterListener()
+ {
+ this.listener = null;
+ }
+
+ public State getNetworkState()
+ {
+ return state;
+ }
+
+ public Sender getSender()
+ {
+ return sender;
+ }
+}
+
+
+
+
+
+// public static boolean isWifiConnected(@NonNull Context context)
+// {
+// ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+// Network networks[] = manager.getAllNetworks();
+//
+// for(Network net : networks)
+// {
+// NetworkInfo info = manager.getNetworkInfo(net);
+// if(info.getType() == ConnectivityManager.TYPE_WIFI) return info.isConnected();
+// }
+//
+// return false;
+// }
\ No newline at end of file
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/NetBridge.java b/app/src/main/java/com/jackss/ag/macroboard/network/NetBridge.java
new file mode 100644
index 0000000..144b640
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/NetBridge.java
@@ -0,0 +1,88 @@
+package com.jackss.ag.macroboard.network;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+/**
+ *
+ */
+abstract public class NetBridge
+{
+ public enum DataReliability
+ {
+ RELIABLE,
+ UNRELIABLE
+ }
+
+ public enum BridgeState
+ {
+ IDLE,
+ CONNECTING,
+ CONNECTED,
+ ERROR
+ }
+
+ public interface OnConnectionStateListener
+ {
+ void onConnectionStateChanged(BridgeState newState);
+ }
+
+ private Context context;
+
+ private OnConnectionStateListener connectionStateListener;
+
+ private BridgeState connectionState;
+
+
+ public NetBridge(Context context)
+ {
+ this.context = context;
+ }
+
+
+ // interface
+ abstract public boolean canStartConnection();
+
+ abstract public void startConnection(T address);
+
+ abstract public void stopConnection();
+
+ abstract public boolean isConnected();
+
+ abstract public boolean sendData(String data, DataReliability reliability);
+
+ public boolean sendData(String data)
+ {
+ return sendData(data, DataReliability.RELIABLE);
+ }
+
+ // final
+ public void setConnectionStateListener(@Nullable OnConnectionStateListener connectionStateListener)
+ {
+ this.connectionStateListener = connectionStateListener;
+
+ if(connectionStateListener != null) connectionStateListener.onConnectionStateChanged(getConnectionState());
+ }
+
+ protected void setConnectionState(BridgeState state)
+ {
+ if(this.connectionState != state)
+ {
+ this.connectionState = state;
+ if(connectionStateListener != null) connectionStateListener.onConnectionStateChanged(this.connectionState);
+ }
+
+ Log.v("NetBridge", "Moving to state: " + state.name());
+ }
+
+ public BridgeState getConnectionState()
+ {
+ return connectionState;
+ }
+
+ public Context getContext()
+ {
+ return context;
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/Packager.java b/app/src/main/java/com/jackss/ag/macroboard/network/Packager.java
new file mode 100644
index 0000000..11c48de
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/Packager.java
@@ -0,0 +1,87 @@
+package com.jackss.ag.macroboard.network;
+
+import com.jackss.ag.macroboard.utils.StaticLibrary;
+
+/**
+ *
+ */
+public class Packager
+{
+ private static final String DIV = ";";
+ private static final String SPEC = ":";
+
+ private static final String HS_HEADER = "MB_HANDSHAKE";
+ private static final String HS_NAME = "N";
+
+ private static final String BE_REQUEST = "MB_REQUEST";
+ private static final String BE_RESPONSE = "MB_RESPONSE";
+
+ private static final String SL_ACTION = "A";
+ private static final String MD_COPY = "c";
+ private static final String MD_CUT = "x";
+ private static final String MD_PASTE = "v";
+
+ private static final String SL_MOUSE = "M";
+ private static final String MD_CLICK_1 = "1";
+ private static final String MD_CLICK_2 = "2";
+
+
+// |==============================
+// |==> HANDSHAKE
+// |===============================
+
+ public static String packHandShake()
+ {
+ return HS_HEADER + DIV + HS_NAME + SPEC + StaticLibrary.getDeviceName();
+ }
+
+ public static boolean unpackHandShake(String handshake)
+ {
+ return handshake.equals(HS_HEADER);
+ }
+
+
+// |==============================
+// |==> BEACON
+// |===============================
+
+ public static String packBroadcastMessage()
+ {
+ return BE_REQUEST;
+ }
+
+ public static boolean validateBeaconResponse(String response)
+ {
+ return response.equals(BE_RESPONSE);
+ }
+
+
+// |==============================
+// |==> ACTIONS
+// |===============================
+
+ public static String packActionCopy()
+ {
+ return SL_ACTION + DIV + MD_COPY;
+ }
+
+ public static String packActionCut()
+ {
+ return SL_ACTION + DIV + MD_CUT;
+ }
+
+ public static String packActionPaste()
+ {
+ return SL_ACTION + DIV + MD_PASTE;
+ }
+
+ private static String div(String... k)
+ {
+ String f = "";
+
+ for(String item : k)
+ f += item + DIV;
+
+ return f;
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/Sender.java b/app/src/main/java/com/jackss/ag/macroboard/network/Sender.java
new file mode 100644
index 0000000..f850e4e
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/Sender.java
@@ -0,0 +1,34 @@
+package com.jackss.ag.macroboard.network;
+
+import android.support.annotation.NonNull;
+
+/**
+ *
+ */
+public class Sender
+{
+ interface OnSendListener
+ {
+ void sendData(String data, NetBridge.DataReliability reliability);
+ }
+
+
+ private OnSendListener sendListener;
+
+
+ Sender(@NonNull OnSendListener sendListener)
+ {
+ this.sendListener = sendListener;
+ }
+
+
+ public void sendTest(NetBridge.DataReliability reliability)
+ {
+ sendListener.sendData("Test from Sender", reliability);
+ }
+
+ public void sendActionCopy()
+ {
+ sendListener.sendData(Packager.packActionCopy(), NetBridge.DataReliability.RELIABLE);
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/wifi/Beacon.java b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/Beacon.java
new file mode 100644
index 0000000..00eb1d6
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/Beacon.java
@@ -0,0 +1,294 @@
+package com.jackss.ag.macroboard.network.wifi;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import com.jackss.ag.macroboard.network.Packager;
+import com.jackss.ag.macroboard.settings.StaticSettings;
+import com.jackss.ag.macroboard.utils.ExpiringList;
+
+import java.io.IOException;
+import java.net.*;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ *
+ */
+public class Beacon
+{
+ private static final String TAG = "Beacon";
+
+ private static final int MSG_WHAT_ADDRESS = 1200;
+ private static final int MSG_WHAT_ERROR = 1400;
+
+ private ExecutorService multicastExecutor = Executors.newSingleThreadExecutor();
+ private Future multicastFuture;
+
+ private Thread receiverThread;
+ private ReceiverTask receiverTask;
+
+ private OnEventListener eventListener;
+
+ private ExpiringList deviceList;
+
+
+
+// |==============================
+// |==> CLASSES
+// |===============================
+
+ /** Beacon callbacks */
+ public interface OnEventListener
+ {
+ /** Called when a new device respond to the beacon multicast */
+ void onDeviceFound(SocketInfo info);
+
+ /**
+ * Called after updateDevices() if at least one device is removed.
+ * @param infoSet a Set containing the removed devices
+ */
+ void onDevicesTimeout(Set infoSet);
+
+ /** Called if an error occurs */
+ void onBeaconFailure();
+ }
+
+
+ /** Multicast UDP packets over the network */
+ private static class MulticastTask implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ Log.i(TAG, "Starting broadcasting thread");
+
+ try(MulticastSocket multicastSocket = new MulticastSocket())
+ {
+ InetAddress group = InetAddress.getByName(StaticSettings.BEACON_MULTICAST_ADDRESS);
+ multicastSocket.joinGroup(group);
+
+ while(true)
+ {
+ String request = Packager.packBroadcastMessage();
+
+ byte sending[] = request.getBytes(StandardCharsets.UTF_8);
+ DatagramPacket packet = new DatagramPacket(sending, sending.length, group, StaticSettings.NET_PORT);
+
+ multicastSocket.send(packet);
+
+ try{ Thread.sleep(StaticSettings.BEACON_MULTICAST_INTERVAL); } catch (InterruptedException e) { break; }
+ }
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+
+ /** Receive packets sent as response from listening devices */
+ private class ReceiverTask implements Runnable
+ {
+ private Handler mainHandler;
+
+ private DatagramSocket receiverSocket;
+
+
+ private void createMainHandler() //TODO: use parent class maybe
+ {
+ mainHandler = new Handler(Looper.getMainLooper())
+ {
+ @Override
+ public void handleMessage(Message msg)
+ {
+ switch (msg.what)
+ {
+ case MSG_WHAT_ADDRESS:
+ onDeviceFound((SocketInfo) msg.obj);
+ break;
+
+ case MSG_WHAT_ERROR:
+ onFailure();
+ break;
+
+ default:
+ throw new AssertionError("Unknown msg.what");
+ }
+ }
+ };
+ }
+
+ // Close receiver socket. Needed to interrupt DatagramSocket.receive(packet)
+ void shutdown()
+ {
+ if(receiverSocket != null) receiverSocket.close();
+ }
+
+ @Override
+ public void run()
+ {
+ Log.i(TAG, "Starting receiverSocket thread");
+
+ createMainHandler();
+
+ try
+ {
+ receiverSocket = new DatagramSocket(StaticSettings.NET_PORT);
+
+ while(true)
+ {
+ byte buff[] = new byte[256];
+ DatagramPacket responsePacket = new DatagramPacket(buff, buff.length);
+ receiverSocket.receive(responsePacket);
+
+ InetAddress requestAddress = responsePacket.getAddress();
+ if(requestAddress != null)
+ {
+ String response = new String(
+ responsePacket.getData(),
+ responsePacket.getOffset(),
+ responsePacket.getLength(),
+ StandardCharsets.UTF_8 );
+
+ if(Packager.validateBeaconResponse(response))
+ {
+ SocketInfo info = new SocketInfo(requestAddress.getHostAddress(), requestAddress.getHostName());
+ mainHandler.obtainMessage(MSG_WHAT_ADDRESS, info).sendToTarget();
+ }
+ else
+ {
+ Log.d(TAG, "Invalid response: " + response);
+ }
+ }
+ else throw new AssertionError("Unexpected error"); //TODO: bad throw
+
+ if(Thread.interrupted()) break;
+ }
+
+ Log.i(TAG, "Quitting receiverSocket thread");
+ }
+ catch (Exception e)
+ {
+ if(receiverSocket != null && !receiverSocket.isClosed())
+ {
+ mainHandler.sendEmptyMessage(MSG_WHAT_ERROR);
+ e.printStackTrace();
+ }
+ }
+ finally
+ {
+ shutdown();
+ }
+ }
+ }
+
+
+// |==============================
+// |==> CONSTRUCTOR
+// |===============================
+
+ public Beacon()
+ {
+ deviceList = new ExpiringList<>(StaticSettings.BEACON_DEVICE_TIMEOUT);
+ }
+
+
+
+// |==============================
+// |==> METHODS
+// |==============================
+
+ private void onDeviceFound(SocketInfo info)
+ {
+ if(deviceList.add(info))
+ {
+ Log.i(TAG, "Found new address");
+
+ if(eventListener != null) eventListener.onDeviceFound(info);
+ }
+ }
+
+ private void onFailure()
+ {
+ Log.i(TAG, "Unknown error occurred");
+
+ stopBroadcast();
+ if(eventListener != null) eventListener.onBeaconFailure();
+ }
+
+ /** Set listener for beacon events */
+ public void setBeaconListener(OnEventListener listener)
+ {
+ this.eventListener = listener;
+ }
+
+ /** Update the devices in the list. OnEventListener.onDevicesTimeout(infoSet) is called if any device is removed. */
+ public void updateDevices()
+ {
+ Set removed = deviceList.update();
+
+ if(removed != null && eventListener != null) eventListener.onDevicesTimeout(removed);
+ }
+
+ public Set getDevicesList()
+ {
+ return new HashSet<>(deviceList.getList());
+ }
+
+ /** Is currently sending packets? */
+ public boolean isRunning() //TODO: too many checks?
+ {
+ return multicastExecutor != null && !multicastExecutor.isShutdown() && !multicastExecutor.isTerminated()
+ && multicastFuture != null && !multicastFuture.isCancelled() && !multicastFuture.isDone();
+ }
+
+ /** Start sending packet */
+ public void startBroadcast()
+ {
+ if(!isRunning())
+ {
+ deviceList.clear();
+
+ receiverTask = new ReceiverTask();
+ receiverThread = new Thread(receiverTask);
+ receiverThread.setDaemon(true);
+ receiverThread.start();
+
+ multicastFuture = multicastExecutor.submit(new MulticastTask());
+ }
+ else Log.e(TAG, "Beacon is already running");
+ }
+
+ /** Stop sending packets */
+ public void stopBroadcast()
+ {
+ deviceList.clear();
+
+ if(multicastFuture != null)
+ {
+ multicastFuture.cancel(true);
+ multicastFuture = null;
+ }
+
+ if(receiverThread != null)
+ {
+ receiverThread.interrupt(); //TODO: maybe not necessary
+ receiverThread = null;
+ }
+
+ if(receiverTask != null)
+ {
+ receiverTask.shutdown();
+ receiverTask = null;
+ }
+
+ Log.i(TAG, "Beacon future has been shutdown");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/wifi/SocketInfo.java b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/SocketInfo.java
new file mode 100644
index 0000000..3cf8a13
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/SocketInfo.java
@@ -0,0 +1,46 @@
+package com.jackss.ag.macroboard.network.wifi;
+
+import android.support.annotation.NonNull;
+import java.net.InetAddress;
+
+
+/**
+ *
+ */
+public class SocketInfo
+{
+ public String address;
+ public String hostName;
+
+ public SocketInfo(String address, String hostName)
+ {
+ this.address = address;
+ this.hostName = hostName;
+ }
+
+ /** Shouldn't be called on main thread. The name fetch is a network operation */
+ public SocketInfo(@NonNull InetAddress address)
+ {
+ this.address = address.getHostAddress();
+ this.hostName = address.getHostName(); // Net operation
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ SocketInfo that = (SocketInfo) o;
+
+ return address != null ? address.equals(that.address) : that.address == null;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = address != null ? address.hashCode() : 0;
+ result = 31 * result + (hostName != null ? hostName.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/wifi/TcpConnection.java b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/TcpConnection.java
new file mode 100644
index 0000000..86ff075
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/TcpConnection.java
@@ -0,0 +1,449 @@
+package com.jackss.ag.macroboard.network.wifi;
+
+import android.os.*;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.jackss.ag.macroboard.settings.StaticSettings;
+
+import java.io.*;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+
+/**
+ * Manages a TCP connection.
+ *
+ */
+public class TcpConnection
+{
+ private static final String TAG = "TcpConnection";
+
+
+ // Used in handler messages what field
+ private static final int MSG_WHAT_DATA = 200;
+ private static final int MSG_WHAT_ERROR = 400;
+
+
+ private Socket clientSocket;
+
+ private ClientConnectionTask connectionTask; // AsyncTask used to produce a connected socket
+
+ private Thread inputThread; // Thread listening for input_stream data
+
+ private PrintWriter outputPrinter; // Printer used to send data to the output_stream
+
+
+ private TcpState tcpState = TcpState.IDLE;
+
+ private int port;
+
+ private OnTcpListener tcpListener;
+
+
+
+// |==============================
+// |==> CONSTRUCTORS
+// |===============================
+
+ public TcpConnection(int port)
+ {
+ this.port = port;
+ }
+
+
+// |==============================
+// |==> CLASSES
+// |===============================
+
+ enum TcpState
+ {
+ IDLE,
+ CONNECTING,
+ CONNECTED,
+ ERROR
+ }
+
+
+ interface OnTcpListener
+ {
+ void onData(String data);
+
+ void onConnectionStateChanged(TcpState newState);
+ }
+
+
+ /** Struct containing address and port */
+ private static class ConnectionInfo
+ {
+ InetAddress address;
+ int port;
+
+ ConnectionInfo(InetAddress address, int port)
+ {
+ this.address = address;
+ this.port = port;
+ }
+ }
+
+
+ /**
+ * AsyncTask used to produce a connected TCP socket.
+ */
+ private class ConnectionTask extends AsyncTask
+ {
+ @Override
+ protected Socket doInBackground(Integer... portArgs)
+ {
+ int port = portArgs[0];
+
+ try(ServerSocket serverSocket = new ServerSocket(port))
+ {
+ return serverSocket.accept();
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Socket socket)
+ {
+ // only if the task is not cancelled
+ if(!isCancelled()) onConnectionResult(socket);
+ }
+ }
+
+
+ private class ClientConnectionTask extends AsyncTask
+ {
+ @Override
+ protected Socket doInBackground(ConnectionInfo... args)
+ {
+ ConnectionInfo info = args[0];
+
+ try
+ {
+ return new Socket(info.address, info.port);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Socket socket)
+ {
+ // only if the task is not cancelled
+ if(!isCancelled()) onConnectionResult(socket);
+ }
+ }
+
+
+ /**
+ * Runnable running on a separate thread listening for TCP input stream data.
+ * Data is sent to main_thread via Handler(main_looper).
+ */
+ private class InputHandler implements Runnable
+ {
+ private static final String TAG = "InputHandler";
+
+ private Handler mainHandler;
+ private final Socket clientSocket;
+
+ InputHandler(Socket socket)
+ {
+ this.clientSocket = socket;
+ }
+
+ private void createMainHandler()
+ {
+ mainHandler = new Handler(Looper.getMainLooper()) //TODO: this may cause leaks?
+ {
+ @Override
+ public void handleMessage(Message msg)
+ {
+ // RUNNING ON MAIN THREAD
+ switch(msg.what)
+ {
+ case MSG_WHAT_DATA:
+ onDataReceived((String) msg.obj);
+ break;
+
+ case MSG_WHAT_ERROR:
+ onInputThreadError();
+ break;
+ }
+ }
+ };
+ }
+
+ private void sendErrorMessage()
+ {
+ mainHandler.sendEmptyMessage(MSG_WHAT_ERROR);
+ }
+
+ @Override
+ public void run()
+ {
+ Log.i(TAG, "Started input_thread");
+
+ createMainHandler();
+
+ try
+ {
+ BufferedReader br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+ if(Thread.interrupted()) return;
+
+ while(true)
+ {
+ String readData = br.readLine();
+ if(Thread.interrupted()) break;
+
+ if(readData != null)
+ mainHandler.obtainMessage(MSG_WHAT_DATA, readData).sendToTarget();
+ else
+ sendErrorMessage();
+ }
+ }
+ catch (Exception e)
+ {
+ if(!clientSocket.isClosed()) e.printStackTrace();
+ sendErrorMessage();
+ }
+ }
+ }
+
+
+
+// |==============================
+// |==> METHODS
+// |===============================
+
+ // Called from ConnectionTask.onPostExecute(socket) when connection is finished
+ private void onConnectionResult(Socket socket)
+ {
+ clientSocket = socket;
+ connectionTask = null;
+
+ if(isSocketConnected())
+ {
+ onConnected();
+ }
+ else
+ {
+ Log.e(TAG, "Connection result failed");
+ onError();
+ }
+
+ }
+
+ // Called if a connected socket is found
+ private void onConnected()
+ {
+ Log.i(TAG, "Connected to: " + clientSocket.getInetAddress().getHostAddress());
+
+ try
+ {
+ clientSocket.setTcpNoDelay(true);
+
+ outputPrinter =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8)), true);
+
+ if(inputThread != null) inputThread.interrupt();
+ inputThread = new Thread(new InputHandler(clientSocket));
+ inputThread.setDaemon(true);
+ inputThread.start();
+
+ setTcpState(TcpState.CONNECTED);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ onError();
+ }
+ }
+
+ // Called from input_thread if an error occurs using the main_handler (i.e. running on main_thread)
+ private void onInputThreadError()
+ {
+ // "throw" an error only if the socket wasn't intentionally closed using Socket.close()
+ // since it means it is not an unexpected event
+ if(isSocketConnected()) onError();
+ }
+
+ // Called internally when an error occurs
+ private void onError() //TODO: maybe reset
+ {
+ setTcpState(TcpState.ERROR);
+ }
+
+ // Running on main thread. Called by input thread when data is read from the input buffer
+ private void onDataReceived(String data)
+ {
+ if(tcpListener != null) tcpListener.onData(data);
+ }
+
+ // Internal set tcp state. Call the listener if a change occurred
+ private void setTcpState(TcpState newState)
+ {
+ if(getTcpState() != newState)
+ {
+ this.tcpState = newState;
+ if(tcpListener != null) tcpListener.onConnectionStateChanged(getTcpState());
+
+ Log.v(TAG, "Moving to state: " + newState.name());
+ }
+ }
+
+ /** Get the state of this TCP connection. */
+ public TcpState getTcpState()
+ {
+ return tcpState;
+ }
+
+ /** Set the listener notified of data receiving and connection state change. */
+ public void setTcpListener(@Nullable OnTcpListener tcpListener)
+ {
+ this.tcpListener = tcpListener;
+
+ if(tcpListener != null) tcpListener.onConnectionStateChanged(getTcpState());
+ }
+
+ /** If isSocketConnected() equals true return the socket address, return null otherwise. */
+ public InetAddress getConnectedAddress()
+ {
+ return isSocketConnected() ? clientSocket.getInetAddress() : null;
+ }
+
+ /** Get the port used by this connection. */
+ public int getPort()
+ {
+ return port;
+ }
+
+ /** Return true if the connection is valid */
+ public boolean isConnected()
+ {
+ return getTcpState() == TcpState.CONNECTED;
+ }
+
+ /** Return true if is currently try to connect, false otherwise. */
+ private boolean isConnecting() //TODO: can use TcpState
+ {
+ return connectionTask != null // valid ref
+ && connectionTask.getStatus() == AsyncTask.Status.RUNNING // task running
+ && !connectionTask.isCancelled(); // task not cancelled
+ }
+
+ /**
+ * Return true if is the socket is connected, false otherwise.
+ *
+ * NOTE: The socket is considered connected even if is actually disconnected from the network.
+ * Only writing or reading from its streams determine if a socket is actually connected or not.
+ * TcpListener.onConnectionStateChange(TcpState.ERROR) is called (on the main thread) when such operations fail.
+ */
+ public boolean isSocketConnected()
+ {
+ return clientSocket != null && clientSocket.isConnected();
+ }
+
+ /** Check whenever calling startConnection will actually start a connection */
+ public boolean canStartConnection()
+ {
+ return getTcpState() == TcpState.IDLE;
+ }
+
+ /**
+ * Try to produce a connected Socket. State is moved to TcpState.CONNECTING.
+ *
+ * @return return true if the connection is successfully started
+ */
+ public boolean startConnection(InetAddress address)
+ {
+ if(canStartConnection())
+ {
+ Log.i(TAG, "Connection in progress");
+
+ connectionTask = new ClientConnectionTask();
+ setTcpState(TcpState.CONNECTING);
+ connectionTask.execute(new ConnectionInfo(address, StaticSettings.NET_PORT));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Free every resource allowing to start a new connection. */
+ public void reset()
+ {
+ Log.i(TAG, "Connection reset");
+
+ // connection task
+ if(connectionTask != null)
+ {
+ connectionTask.cancel(true);
+ connectionTask = null;
+ }
+
+ // input thread
+ if(inputThread != null)
+ {
+ inputThread.interrupt();
+ inputThread = null;
+ }
+
+ // output printer
+ if(outputPrinter != null)
+ {
+ outputPrinter.close();
+ outputPrinter = null;
+ }
+
+ // client socket
+ if(clientSocket != null) try
+ {
+ clientSocket.close();
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ finally
+ {
+ clientSocket = null;
+ }
+
+ setTcpState(TcpState.IDLE);
+ }
+
+ /**
+ * If the connection is open send a data string, do nothing otherwise.
+ *
+ * @param data Data string to send
+ */
+ public void sendData(String data)
+ {
+ if(isSocketConnected()) //TODO: change this
+ {
+ if(outputPrinter != null)
+ outputPrinter.println(data);
+ else
+ throw new AssertionError("outputPrinter is null while the socket is connected!");
+ }
+ else
+ {
+ Log.e(TAG, "SendData() called when Socket is not connected");
+ onError();
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/wifi/UdpSender.java b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/UdpSender.java
new file mode 100644
index 0000000..93c6f35
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/UdpSender.java
@@ -0,0 +1,57 @@
+package com.jackss.ag.macroboard.network.wifi;
+
+import android.support.annotation.NonNull;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Send udp packets to a specific address and port.
+ */
+public class UdpSender
+{
+ private static final String TAG = "UdpSender";
+
+ private ExecutorService executorService;
+
+
+ // Runnable used to send strings to udp using: host, port
+ private class SendingTask implements Runnable
+ {
+ private final int port;
+ private final InetAddress address;
+ private final String data;
+
+ SendingTask(InetAddress address, int port, String data)
+ {
+ this.address = address;
+ this.port = port;
+ this.data = data;
+ }
+
+ @Override
+ public void run()
+ {
+ try (DatagramSocket socket = new DatagramSocket())
+ {
+ byte buff[] = data.getBytes(StandardCharsets.UTF_8);
+ DatagramPacket packet = new DatagramPacket(buff, buff.length, address, port);
+ socket.send(packet);
+ }
+ catch (Exception e) { e.printStackTrace(); }
+ }
+ }
+
+ public UdpSender()
+ {
+ executorService = Executors.newSingleThreadExecutor();
+ }
+
+ public void sendData(@NonNull InetAddress address, int port, String data)
+ {
+ executorService.execute(new SendingTask(address, port, data));
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/network/wifi/WifiBridge.java b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/WifiBridge.java
new file mode 100644
index 0000000..ae3c526
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/network/wifi/WifiBridge.java
@@ -0,0 +1,149 @@
+package com.jackss.ag.macroboard.network.wifi;
+
+import android.content.Context;
+import android.util.Log;
+import com.jackss.ag.macroboard.network.NetBridge;
+import com.jackss.ag.macroboard.network.Packager;
+import com.jackss.ag.macroboard.settings.StaticSettings;
+
+import java.io.PrintWriter;
+import java.net.InetAddress;
+
+
+/**
+ *
+ */
+public class WifiBridge extends NetBridge
+{
+ private static final String TAG = "WifiBridge";
+
+ private TcpConnection tcpConnection;
+
+ private UdpSender udpSender;
+
+ private boolean handShakeComplete = false;
+
+
+ private TcpConnection.OnTcpListener tcpListener= new TcpConnection.OnTcpListener()
+ {
+ @Override
+ public void onData(String data)
+ {
+ Log.v(TAG, "Data received: " + data);
+
+ if(isHandShakeComplete())
+ {
+ Log.v(TAG, "Valid data"); //TODO: manage data here
+ }
+ else
+ {
+ if(Packager.unpackHandShake(data))
+ {
+ Log.i(TAG, "Valid handshake");
+ handShakeComplete = true;
+ setConnectionState(BridgeState.CONNECTED);
+ }
+ else
+ Log.i(TAG, "Invalid handshake");
+ }
+ }
+
+ @Override
+ public void onConnectionStateChanged(TcpConnection.TcpState newState)
+ {
+ switch (newState)
+ {
+ case IDLE:
+ setConnectionState(BridgeState.IDLE);
+ break;
+
+ case CONNECTING:
+ setConnectionState(BridgeState.CONNECTING);
+ break;
+
+ case CONNECTED:
+ sendHandShake();
+ break;
+
+ case ERROR:
+ setConnectionState(BridgeState.ERROR);
+ break;
+ }
+ }
+ };
+
+
+ public WifiBridge(Context context)
+ {
+ super(context);
+
+ tcpConnection = new TcpConnection(StaticSettings.NET_PORT);
+ tcpConnection.setTcpListener(tcpListener);
+ udpSender = new UdpSender();
+ }
+
+ @Override
+ public boolean canStartConnection()
+ {
+ return tcpConnection.canStartConnection();
+ }
+
+ @Override
+ public void startConnection(InetAddress address)
+ {
+ if(canStartConnection())
+ tcpConnection.startConnection(address);
+ }
+
+ @Override
+ public void stopConnection()
+ {
+ tcpConnection.reset();
+ invalidateHandShake();
+ }
+
+ @Override
+ public boolean isConnected()
+ {
+ return getConnectionState() == BridgeState.CONNECTED;
+ }
+
+ @Override
+ public boolean sendData(String data, DataReliability reliability)
+ {
+ if(data == null) throw new AssertionError("sending null data string");
+
+ if(!isConnected())
+ {
+ Log.v(TAG, "sending data from non-connected wifi_bridge");
+ return false;
+ }
+
+ if(reliability == DataReliability.RELIABLE)
+ {
+ tcpConnection.sendData(data);
+ }
+ else
+ {
+ udpSender.sendData(tcpConnection.getConnectedAddress(), tcpConnection.getPort(), data);
+ }
+
+ return true;
+ }
+
+ private void sendHandShake()
+ {
+ if(getConnectionState() == BridgeState.CONNECTING)
+ tcpConnection.sendData(Packager.packHandShake());
+ }
+
+ private boolean isHandShakeComplete()
+ {
+ return handShakeComplete;
+ }
+
+ private void invalidateHandShake()
+ {
+ handShakeComplete = false;
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/settings/StaticSettings.java b/app/src/main/java/com/jackss/ag/macroboard/settings/StaticSettings.java
new file mode 100644
index 0000000..e3fcd18
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/settings/StaticSettings.java
@@ -0,0 +1,15 @@
+package com.jackss.ag.macroboard.settings;
+
+/**
+ *
+ */
+public class StaticSettings
+{
+ public static final int NET_PORT = 4545;
+
+ public static final String BEACON_MULTICAST_ADDRESS = "228.5.6.7";
+ public static final int BEACON_MULTICAST_INTERVAL = 1000;
+ public static final int BEACON_DEVICE_TIMEOUT = 4;
+
+ public static final int DEVICES_UPDATE_INTERVAL = 3;
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/ui/fragments/ConnectDialogFragment.java b/app/src/main/java/com/jackss/ag/macroboard/ui/fragments/ConnectDialogFragment.java
new file mode 100644
index 0000000..1a70f45
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/ui/fragments/ConnectDialogFragment.java
@@ -0,0 +1,185 @@
+package com.jackss.ag.macroboard.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import com.jackss.ag.macroboard.R;
+import com.jackss.ag.macroboard.network.wifi.Beacon;
+import com.jackss.ag.macroboard.network.wifi.SocketInfo;
+import com.jackss.ag.macroboard.settings.StaticSettings;
+
+import java.util.Set;
+
+/**
+ *
+ */
+public class ConnectDialogFragment extends DialogFragment implements Beacon.OnEventListener
+{
+ private static final String TAG = "ConnectDialogFragment";
+
+ private Beacon beacon;
+
+ private ListView deviceList;
+ private SocketAddressAdapter adapter;
+
+ private OnConnectDialogEventListener dialogEventListener;
+
+ private Handler mHandler;
+ private Runnable updateDevicesTask;
+
+
+// |==============================
+// |==> CLASSES
+// |===============================
+
+ public interface OnConnectDialogEventListener
+ {
+ void onDialogConnectRequest(String address);
+ }
+
+ private class UpdateDevicesTask implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ updateDevices();
+ postDeviceUpdate();
+ }
+ }
+
+
+// |==============================
+// |==> METHODS
+// |===============================
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ beacon = new Beacon();
+ beacon.setBeaconListener(this);
+
+ adapter = new SocketAddressAdapter(getActivity());
+
+ mHandler = new Handler();
+ updateDevicesTask = new UpdateDevicesTask();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState)
+ {
+ ViewGroup view = (ViewGroup) getActivity().getLayoutInflater().inflate(R.layout.fragment_dialog_connect, null);
+
+ deviceList = (ListView) view.findViewById(R.id.device_list);
+ deviceList.setAdapter(adapter);
+ deviceList.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
+ deviceList.setOnItemClickListener(new AdapterView.OnItemClickListener()
+ {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id)
+ {
+ deviceClicked(adapter.getItem(position));
+ }
+ });
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder .setView(view)
+ .setNegativeButton(R.string.cancel, null)
+ .setTitle(R.string.connect_to_device);
+
+ return builder.create();
+ }
+
+ @Override
+ public void onStart()
+ {
+ super.onStart();
+
+ beacon.startBroadcast();
+ postDeviceUpdate();
+ updateUI();
+ }
+
+ @Override
+ public void onStop()
+ {
+ super.onStop();
+
+ beacon.stopBroadcast();
+ adapter.clear();
+ stopDeviceUpdate();
+ }
+
+ @Override
+ public void onDeviceFound(SocketInfo socketInfo)
+ {
+ adapter.add(socketInfo);
+ updateUI();
+ postDeviceUpdate();
+ }
+
+ @Override
+ public void onDevicesTimeout(Set infoSet)
+ {
+ for(SocketInfo info : infoSet)
+ {
+ adapter.remove(info);
+ }
+ updateUI();
+ postDeviceUpdate();
+ }
+
+ @Override
+ public void onBeaconFailure()
+ {
+ throw new AssertionError("Beacon failed");
+ }
+
+ // Schedule an update_task in the handler and remove any previous one.
+ private void postDeviceUpdate()
+ {
+ stopDeviceUpdate();
+ mHandler.postDelayed(updateDevicesTask, StaticSettings.DEVICES_UPDATE_INTERVAL * 1000);
+ }
+
+ // Remove any update callback from the handler
+ private void stopDeviceUpdate()
+ {
+ mHandler.removeCallbacks(updateDevicesTask);
+ }
+
+ // Called when a device int the list is clicked
+ private void deviceClicked(SocketInfo info)
+ {
+ if(info == null) throw new AssertionError("Selected null address");
+
+ if(dialogEventListener != null) dialogEventListener.onDialogConnectRequest(info.address);
+ dismiss();
+ }
+
+ // Fresh UI update
+ private void updateUI()
+ {
+ adapter.notifyDataSetChanged();
+ }
+
+ // Fetch devices from the beacon and add them to the adapter
+ private void updateDevices()
+ {
+ beacon.updateDevices();
+ }
+
+ /** Set an event used to list for connect requests */
+ public void setDialogEventListener(OnConnectDialogEventListener dialogEventListener)
+ {
+ this.dialogEventListener = dialogEventListener;
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/ui/fragments/SocketAddressAdapter.java b/app/src/main/java/com/jackss/ag/macroboard/ui/fragments/SocketAddressAdapter.java
new file mode 100644
index 0000000..78e80ba
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/ui/fragments/SocketAddressAdapter.java
@@ -0,0 +1,40 @@
+package com.jackss.ag.macroboard.ui.fragments;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import com.jackss.ag.macroboard.R;
+import com.jackss.ag.macroboard.network.wifi.SocketInfo;
+import com.jackss.ag.macroboard.utils.StaticLibrary;
+
+
+/**
+ *
+ */
+class SocketAddressAdapter extends ArrayAdapter
+{
+ SocketAddressAdapter(@NonNull Context context)//, @LayoutRes int resource)
+ {
+ super(context, 0);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)
+ {
+ View view = convertView == null
+ ? LayoutInflater.from(getContext()).inflate(R.layout.row_connect_device, parent, false)
+ : convertView;
+
+ TextView label = (TextView) view.findViewById(R.id.connect_device_text);
+ SocketInfo item = getItem(position);
+ label.setText(item != null ? StaticLibrary.sanitizeHostName(item.hostName) : "Error");
+
+ return view;
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/ui/views/BottomNavigationItem.java b/app/src/main/java/com/jackss/ag/macroboard/ui/views/BottomNavigationItem.java
index 80b9e56..60e893c 100644
--- a/app/src/main/java/com/jackss/ag/macroboard/ui/views/BottomNavigationItem.java
+++ b/app/src/main/java/com/jackss/ag/macroboard/ui/views/BottomNavigationItem.java
@@ -18,7 +18,7 @@
import com.jackss.ag.macroboard.R;
import com.jackss.ag.macroboard.utils.BubbleGenerator;
import com.jackss.ag.macroboard.utils.ButtonDetector;
-import com.jackss.ag.macroboard.utils.MBUtils;
+import com.jackss.ag.macroboard.utils.StaticLibrary;
/**
@@ -91,7 +91,7 @@ private void initUI()
{
layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
- layout.setPadding(0, MBUtils.dp2px(TOP_PADDING_DP), 0, MBUtils.dp2px(BOTTOM_PADDING_DP));
+ layout.setPadding(0, StaticLibrary.dp2px(TOP_PADDING_DP), 0, StaticLibrary.dp2px(BOTTOM_PADDING_DP));
icon = new ImageView(getContext());
@@ -100,7 +100,7 @@ private void initUI()
label.setTextAlignment(TEXT_ALIGNMENT_CENTER);
label.setTextSize(TypedValue.COMPLEX_UNIT_SP, TEXT_SIZE_SP);
- layout.addView(icon, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, MBUtils.dp2px(ICON_SIZE_DP)));
+ layout.addView(icon, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, StaticLibrary.dp2px(ICON_SIZE_DP)));
layout.addView(label, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
addView(layout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
diff --git a/app/src/main/java/com/jackss/ag/macroboard/ui/views/MaterialButton.java b/app/src/main/java/com/jackss/ag/macroboard/ui/views/MaterialButton.java
index e5f6343..4ba00b4 100644
--- a/app/src/main/java/com/jackss/ag/macroboard/ui/views/MaterialButton.java
+++ b/app/src/main/java/com/jackss/ag/macroboard/ui/views/MaterialButton.java
@@ -30,8 +30,8 @@ public class MaterialButton extends View
// XML attrs
int backgroundColor = Color.GRAY;
- float cornerRadius = MBUtils.dp2px(2);
- int iconSize = MBUtils.dp2px(24);
+ float cornerRadius = StaticLibrary.dp2px(2);
+ int iconSize = StaticLibrary.dp2px(24);
float backgroundSaturationMultiplier = 0.85f;
// Background press effects
@@ -113,7 +113,7 @@ public MaterialButton(Context context, @Nullable AttributeSet attrs, int defStyl
if(icon == null) icon = getResources().getDrawable(R.drawable.ic_test_icon, null);
- backgroundPressedColor = MBUtils.saturateColor(backgroundColor, backgroundSaturationMultiplier);
+ backgroundPressedColor = StaticLibrary.saturateColor(backgroundColor, backgroundSaturationMultiplier);
backgroundColorEvaluator = new CachedArgbEvaluator(backgroundColor, backgroundPressedColor);
initGraphics();
@@ -176,13 +176,13 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- final int desiredSize = MBUtils.dp2px(DESIRED_BACKGROUND_DP);
+ final int desiredSize = StaticLibrary.dp2px(DESIRED_BACKGROUND_DP);
final int desiredW = desiredSize + getPaddingLeft() + getPaddingRight();
final int desiredH = desiredSize + getPaddingTop() + getPaddingBottom();
- int measuredW = MBUtils.resolveDesiredMeasure(widthMeasureSpec, desiredW);
- int measuredH = MBUtils.resolveDesiredMeasure(heightMeasureSpec, desiredH);
+ int measuredW = StaticLibrary.resolveDesiredMeasure(widthMeasureSpec, desiredW);
+ int measuredH = StaticLibrary.resolveDesiredMeasure(heightMeasureSpec, desiredH);
setMeasuredDimension(measuredW, measuredH);
}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/utils/ExpiringList.java b/app/src/main/java/com/jackss/ag/macroboard/utils/ExpiringList.java
new file mode 100644
index 0000000..ae7e7ea
--- /dev/null
+++ b/app/src/main/java/com/jackss/ag/macroboard/utils/ExpiringList.java
@@ -0,0 +1,95 @@
+package com.jackss.ag.macroboard.utils;
+
+import android.os.SystemClock;
+
+import java.util.*;
+
+
+/**
+ *
+ */
+public class ExpiringList implements Iterable
+{
+ private HashMap map = new HashMap<>();
+
+ private int defaultTimeout;
+
+
+ public ExpiringList(int defaultTimeout)
+ {
+ this.defaultTimeout = defaultTimeout;
+ }
+
+
+ /**
+ * Add an item to the map. If the item is already in it only the timeout is updated.
+ * @param item the item to add
+ * @param timeout the time in seconds the item will remain on the map
+ * @return return true if the item wasn't already in the map
+ */
+ public boolean add(T item, int timeout)
+ {
+ if(timeout <= 0) throw new IllegalArgumentException("timeout is minor than 0");
+
+ long expireTime = getAbsoluteTime() + timeout * 1000;
+ return map.put(item, expireTime) == null;
+ }
+
+ /**
+ * Add an item to the map and using default timeout passed in the constructor.
+ * @param item the item to add
+ * @return return true if the item wasn't already in the map
+ */
+ public boolean add(T item)
+ {
+ return add(item, defaultTimeout);
+ }
+
+ /** Return true if the item is in the map. update() should be called before this. */
+ public boolean contains(T item)
+ {
+ return map.containsKey(item);
+ }
+
+ /** Remove every expired entry, returning a Set containing removed objects. */
+ public Set update()
+ {
+ final long systemTime = getAbsoluteTime();
+ Set removed = new HashSet<>();
+
+ for(Iterator> it = map.entrySet().iterator(); it.hasNext(); )
+ {
+ Map.Entry entry = it.next();
+ if(entry.getValue() <= systemTime)
+ {
+ it.remove();
+ removed.add(entry.getKey());
+ }
+ }
+
+ return removed.size() > 0 ? removed : null;
+ }
+
+ /** Remove all entries */
+ public void clear()
+ {
+ map.clear();
+ }
+
+ /** Get a {@link Set} of the contained values. update() should be called before. */
+ public Set getList()
+ {
+ return map.keySet();
+ }
+
+ @Override
+ public Iterator iterator()
+ {
+ return map.keySet().iterator();
+ }
+
+ private long getAbsoluteTime()
+ {
+ return SystemClock.uptimeMillis();
+ }
+}
diff --git a/app/src/main/java/com/jackss/ag/macroboard/utils/MBUtils.java b/app/src/main/java/com/jackss/ag/macroboard/utils/StaticLibrary.java
similarity index 57%
rename from app/src/main/java/com/jackss/ag/macroboard/utils/MBUtils.java
rename to app/src/main/java/com/jackss/ag/macroboard/utils/StaticLibrary.java
index d37a7a0..56fcf40 100644
--- a/app/src/main/java/com/jackss/ag/macroboard/utils/MBUtils.java
+++ b/app/src/main/java/com/jackss/ag/macroboard/utils/StaticLibrary.java
@@ -1,14 +1,18 @@
package com.jackss.ag.macroboard.utils;
+import android.bluetooth.BluetoothAdapter;
import android.content.res.Resources;
import android.graphics.Color;
+import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.view.View;
+import java.util.concurrent.ThreadFactory;
+
/**
* Static library for generic methods
*/
-public class MBUtils
+public class StaticLibrary
{
/** Convert dp to px */
public static int dp2px(float dp)
@@ -43,4 +47,32 @@ public static int saturateColor(int color, float amount)
return Color.HSVToColor(hsv);
}
+
+ /** Create a thread factory for network threads */
+ public static ThreadFactory buildNetworkThreadFactory()
+ {
+ return new ThreadFactory()
+ {
+ @Override
+ public Thread newThread(@NonNull Runnable r)
+ {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ return t;
+ }
+ };
+ }
+
+ /** Remove every char after the first dot. Prevent wifi modems to modify socket names. */
+ public static String sanitizeHostName(String hostName)
+ {
+ String res[] = hostName.split("\\.");
+ return res.length > 0 ? res[0] : hostName;
+ }
+
+ /** Get a user friendly device name */
+ public static String getDeviceName()
+ {
+ return BluetoothAdapter.getDefaultAdapter().getName();
+ }
}
diff --git a/app/src/main/res/drawable/ic_computer_black_24dp.xml b/app/src/main/res/drawable/ic_computer_black_24dp.xml
new file mode 100644
index 0000000..4599f98
--- /dev/null
+++ b/app/src/main/res/drawable/ic_computer_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_dialog_connect.xml b/app/src/main/res/layout/fragment_dialog_connect.xml
new file mode 100644
index 0000000..4d516f0
--- /dev/null
+++ b/app/src/main/res/layout/fragment_dialog_connect.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/row_connect_device.xml b/app/src/main/res/layout/row_connect_device.xml
new file mode 100644
index 0000000..107a873
--- /dev/null
+++ b/app/src/main/res/layout/row_connect_device.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 1fb63ff..1dd4818 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -5,7 +5,9 @@
#303f9f
#ff4081
- #4333
+ #1D000000
+
+ #9999
#f6f6f6
#777
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 5a6c2ed..892471b 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,8 +2,9 @@
14dp
14dp
-
4dp
1dp
+ 32dp
+ 24dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2617f32..1e2e203 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,7 +2,11 @@
MacroBoard
- DummyText
+ Dummy Text
No custom buttons
+ Scanning for devices…
+ PC
+ Cancel
+ Connect to device
diff --git a/app/src/sandbox/AndroidManifest.xml b/app/src/sandbox/AndroidManifest.xml
new file mode 100644
index 0000000..b0099d7
--- /dev/null
+++ b/app/src/sandbox/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/sandbox/java/com/jackss/ag/macroboard/sandbox/NetworkTestsActivity.java b/app/src/sandbox/java/com/jackss/ag/macroboard/sandbox/NetworkTestsActivity.java
new file mode 100644
index 0000000..95838e1
--- /dev/null
+++ b/app/src/sandbox/java/com/jackss/ag/macroboard/sandbox/NetworkTestsActivity.java
@@ -0,0 +1,93 @@
+package com.jackss.ag.macroboard.sandbox;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import com.jackss.ag.macroboard.R;
+import com.jackss.ag.macroboard.network.NetAdapter;
+import com.jackss.ag.macroboard.network.NetBridge;
+
+
+public class NetworkTestsActivity extends AppCompatActivity implements NetAdapter.OnNetworkEventListener
+{
+ private static final String TAG = "NetworkTestsActivity";
+
+ private Button start;
+ private Button stop;
+ private Button send;
+ private TextView result;
+
+ NetAdapter netAdapter;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_network_tests);
+
+ start = (Button) findViewById(R.id.sandbox_start);
+ stop = (Button) findViewById(R.id.sandbox_stop);
+ send = (Button) findViewById(R.id.sandbox_send);
+ result = (TextView) findViewById(R.id.sandbox_result);
+
+ netAdapter = NetAdapter.getInstance();
+ }
+
+ public void onClick(View view)
+ {
+ if(view.equals(start))
+ {
+ netAdapter.connectDialog(this);
+ }
+ else if(view.equals(stop))
+ {
+ netAdapter.disconnect();
+ }
+ else if(view.equals(send))
+ {
+ netAdapter.getSender().sendTest(NetBridge.DataReliability.RELIABLE);
+ }
+ }
+
+
+ @Override
+ protected void onStart()
+ {
+ super.onStart();
+
+ netAdapter.registerListener(this);
+ }
+
+ @Override
+ protected void onStop()
+ {
+ super.onStop();
+
+ netAdapter.unregisterListener();
+ }
+
+ @Override
+ protected void onDestroy()
+ {
+ super.onDestroy();
+
+ netAdapter.disconnect();
+ }
+
+ @Override
+ public void onNetworkStateChanged(NetAdapter.State newState)
+ {
+ result.setText(newState.name());
+ }
+
+ @Override
+ public void onNetworkFailure()
+ {
+ Log.e(TAG, "Net failure");
+ result.setText(String.valueOf("Error"));
+ }
+}
\ No newline at end of file
diff --git a/app/src/sandbox/res/layout/activity_network_tests.xml b/app/src/sandbox/res/layout/activity_network_tests.xml
new file mode 100644
index 0000000..0a6c4b7
--- /dev/null
+++ b/app/src/sandbox/res/layout/activity_network_tests.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file