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 @@ + + + +