initial android support

moved ios specific code into a subfolder
created an android folder
updated the Install instructions on the readme
bumped the version to 0.2.0
This commit is contained in:
Andy Prock
2015-09-25 14:58:32 -07:00
parent 4cb6e77504
commit 3ce4b63536
24 changed files with 874 additions and 26 deletions
@@ -0,0 +1,30 @@
/**
* UdpErrorUtil.java
* react-native-udp
*
* Created by Andy Prock on 9/24/15.
*/
package com.tradle.react;
import javax.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
/**
* Helper class for udp errors.
*/
public class UdpErrorUtil {
/**
* Create Error object to be passed back to the JS callback.
*/
/* package */ static WritableMap getError(@Nullable String key, String errorMessage) {
WritableMap errorMap = Arguments.createMap();
errorMap.putString("message", errorMessage);
if (key != null) {
errorMap.putString("key", key);
}
return errorMap;
}
}
@@ -0,0 +1,116 @@
/**
* UdpReceiverTask.java
* react-native-udp
*
* Created by Andy Prock on 9/24/15.
*/
package com.tradle.react;
import android.os.AsyncTask;
import android.util.Base64;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* This is a specialized AsyncTask that receives data from a socket in the background, and
* notifies it's listener when data is received.
*/
public class UdpReceiverTask extends AsyncTask<Void, UdpReceiverTask.ReceivedPacket, Void> {
private static final String TAG = "UdpReceiverTask";
private static final int MAX_UDP_DATAGRAM_LEN = 1024;
private DatagramSocket mSocket;
private WeakReference<OnDataReceivedListener> mReceiverListener;
/**
* An {@link AsyncTask} that blocks to receive data from a socket.
* Received data is sent via the {@link OnDataReceivedListener}
*/
public UdpReceiverTask(DatagramSocket socket, UdpReceiverTask.OnDataReceivedListener
receiverListener) {
this.mSocket = socket;
this.mReceiverListener = new WeakReference<>(receiverListener);
}
/**
* An infinite loop to block and read data from the socket.
*/
@Override
protected Void doInBackground(Void... a) {
OnDataReceivedListener receiverListener = mReceiverListener.get();
try {
byte[] lMsg = new byte[MAX_UDP_DATAGRAM_LEN];
DatagramPacket dp = new DatagramPacket(lMsg, lMsg.length);
while(!isCancelled()){
mSocket.receive(dp);
publishProgress(new ReceivedPacket(Base64.encodeToString(lMsg, Base64.NO_WRAP),
dp.getAddress().getHostAddress(), dp.getPort()));
}
} catch (IOException ioe) {
if (receiverListener != null) {
receiverListener.didReceiveError(ioe.getMessage());
}
} catch (RuntimeException rte) {
if (receiverListener != null) {
receiverListener.didReceiveRuntimeException(rte);
}
}
return null;
}
/**
* Send data out to the listener.
* @param {@link ReceivedPacket} packet marshalled data
*/
@Override
protected void onProgressUpdate(ReceivedPacket... packet) {
OnDataReceivedListener receiverListener = mReceiverListener.get();
if (receiverListener != null) {
receiverListener.didReceiveData(packet[0].base64String, packet[0].address,
packet[0].port);
}
}
/**
* Close if cancelled.
*/
@Override
protected void onCancelled(){
if (mSocket != null){
mSocket.close();
}
}
/**
* Internal class used to marshall packet data as a progress update.
* base64String the data encoded as a base64 string
* address the address of the sender
* port the port number of the sender
*/
class ReceivedPacket {
String base64String;
String address;
int port;
ReceivedPacket(String base64String, String address, int port) {
this.base64String = base64String;
this.address = address;
this.port = port;
}
}
/**
* Listener interface for receive events.
*/
public interface OnDataReceivedListener {
void didReceiveData(String data, String host, int port);
void didReceiveError(String message);
void didReceiveRuntimeException(RuntimeException exception);
}
}
@@ -0,0 +1,61 @@
/**
* UdpSenderTask.java
* react-native-udp
*
* Created by Andy Prock on 9/24/15.
*/
package com.tradle.react;
import android.os.AsyncTask;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* Specialized AsyncTask that transmits data in the background, and notifies listeners of the result.
*/
public class UdpSenderTask extends AsyncTask<DatagramPacket, Void, Void> {
private static final String TAG = "UdpSenderTask";
private DatagramSocket mSocket;
private WeakReference<OnDataSentListener> mListener;
public UdpSenderTask(DatagramSocket socket, OnDataSentListener listener) {
this.mSocket = socket;
this.mListener = new WeakReference<>(listener);
}
@Override
protected Void doInBackground(DatagramPacket... params) {
OnDataSentListener listener = mListener.get();
try {
mSocket.send(params[0]);
if (listener != null) {
listener.onDataSent(this);
}
} catch (IOException e) {
if (listener != null) {
listener.onDataSentError(this, e.getMessage());
}
} catch (RuntimeException rte) {
if (listener != null) {
listener.onDataSentRuntimeException(this, rte);
}
}
return null;
}
/**
* Callbacks for data send events.
*/
public interface OnDataSentListener {
void onDataSent(UdpSenderTask task);
void onDataSentError(UdpSenderTask task, String error);
void onDataSentRuntimeException(UdpSenderTask task, RuntimeException exception);
}
}
@@ -0,0 +1,226 @@
/**
* UdpSocketClient.java
* react-native-udp
*
* Created by Andy Prock on 9/24/15.
*/
package com.tradle.react;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.util.Base64;
import com.facebook.react.bridge.Callback;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.channels.DatagramChannel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Client class that wraps a sender and a receiver for UDP data.
*/
public final class UdpSocketClient implements UdpReceiverTask.OnDataReceivedListener, UdpSenderTask.OnDataSentListener {
private final OnDataReceivedListener mReceiverListener;
private final OnRuntimeExceptionListener mExceptionListener;
private final boolean mReuseAddress;
private final Map<UdpSenderTask, Callback> mPendingSends;
private DatagramChannel mChannel;
private DatagramSocket mSocket;
private UdpReceiverTask mReceiverTask;
private UdpSocketClient(Builder builder) {
this.mReceiverListener = builder.receiverListener;
this.mExceptionListener = builder.exceptionListener;
this.mReuseAddress = builder.reuse;
this.mPendingSends = new ConcurrentHashMap<>();
}
/**
* Binds to a specific port or address. A random port is used if the address is {@code null}.
*
* @param port local port to bind to
* @param address local address to bind to
* @throws IOException
* @throws IllegalArgumentException
* if the SocketAddress is not supported
* @throws SocketException
* if the socket is already bound or a problem occurs during
* binding.
*/
public void bind(Integer port, @Nullable String address) throws IOException {
mChannel = DatagramChannel.open();
mChannel.configureBlocking(true);
mSocket = mChannel.socket();
mReceiverTask = new UdpReceiverTask(mSocket, this);
SocketAddress socketAddress = null;
if (address != null) {
socketAddress = new InetSocketAddress(address, port);
} else {
socketAddress = new InetSocketAddress(port);
}
mSocket.setReuseAddress(mReuseAddress);
mSocket.bind(socketAddress);
// begin listening for data in the background
mReceiverTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Creates a UdpSenderTask, and transmits udp data in the background.
*
* @param base64String byte array housed in a String.
* @param port destination port
* @param address destination address
* @param callback callback for results
* @throws UnknownHostException
*/
public void send(String base64String, Integer port, String address, @Nullable Callback callback) throws UnknownHostException {
byte[] data = Base64.decode(base64String, Base64.NO_WRAP);
DatagramPacket packet = new DatagramPacket(data, data.length,
InetAddress.getByName(address), port);
UdpSenderTask task = new UdpSenderTask(mSocket, this);
if (callback != null) {
synchronized (mPendingSends) {
mPendingSends.put(task, callback);
}
}
task.execute(packet);
}
/**
* Sets the socket to enable broadcasts.
*/
public void setBroadcast(boolean flag) throws SocketException {
if (mSocket != null) {
mSocket.setBroadcast(flag);
}
}
/**
* Shuts down the receiver task, closing the socket.
*/
public void close() throws IOException {
if (mReceiverTask != null && !mReceiverTask.isCancelled()) {
mReceiverTask.cancel(false);
} else if (mChannel.isOpen()) {
mChannel.close();
}
}
/**
* Retransmits the data back a level, attaching {@code this}
*/
@Override
public void didReceiveData(String data, String host, int port) {
mReceiverListener.didReceiveData(this, data, host, port);
}
/**
* Retransmits the error back a level, attaching {@code this}
*/
@Override
public void didReceiveError(String message) {
mReceiverListener.didReceiveError(this, message);
}
/**
* Retransmits the exception back a level, attaching {@code this}
*/
@Override
public void didReceiveRuntimeException(RuntimeException exception) {
mExceptionListener.didReceiveException(exception);
}
/**
* Transmits success to the javascript layer, if a callback is present.
*/
@Override
public void onDataSent(UdpSenderTask task) {
Callback callback;
synchronized (mPendingSends) {
callback = mPendingSends.get(task);
mPendingSends.remove(task);
}
if (callback != null) {
callback.invoke();
}
}
/**
* Transmits an error to the javascript layer, if a callback is present.
*/
@Override
public void onDataSentError(UdpSenderTask task, String error) {
Callback callback;
synchronized (mPendingSends) {
callback = mPendingSends.get(task);
mPendingSends.remove(task);
}
if (callback != null) {
callback.invoke(UdpErrorUtil.getError(null, error));
}
}
/**
* Retransmits the exception back a level, attaching {@code this}
*/
@Override
public void onDataSentRuntimeException(UdpSenderTask task, RuntimeException exception) {
mExceptionListener.didReceiveException(exception);
synchronized (mPendingSends) {
mPendingSends.remove(task);
}
}
public static class Builder {
private OnDataReceivedListener receiverListener;
private OnRuntimeExceptionListener exceptionListener;
private boolean reuse = true;
public Builder(OnDataReceivedListener receiverListener, OnRuntimeExceptionListener exceptionListener) {
this.receiverListener = receiverListener;
this.exceptionListener = exceptionListener;
}
public Builder reuseAddress(boolean reuse) {
this.reuse = reuse;
return this;
}
public UdpSocketClient build() {
return new UdpSocketClient(this);
}
}
/**
* Callback interface for runtime exceptions.
*/
public interface OnRuntimeExceptionListener {
void didReceiveException(RuntimeException exception);
}
/**
* Callback interface data received events.
*/
public interface OnDataReceivedListener {
void didReceiveData(UdpSocketClient client, String data, String host, int port);
void didReceiveError(UdpSocketClient client, String message);
}
}
@@ -0,0 +1,277 @@
/**
* UdpSockets.java
* react-native-udp
*
* Created by Andy Prock on 9/24/15.
*/
package com.tradle.react;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import java.io.IOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutionException;
/**
* The NativeModule in charge of storing active {@link UdpSocketClient}s, and acting as an api layer.
*/
public final class UdpSockets extends ReactContextBaseJavaModule
implements UdpSocketClient.OnDataReceivedListener, UdpSocketClient.OnRuntimeExceptionListener {
private static final String TAG = "UdpSockets";
private SparseArray<UdpSocketClient> mClients = new SparseArray<>();
private boolean mShuttingDown = false;
public UdpSockets(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return TAG;
}
@Override
public void initialize() {
mShuttingDown = false;
}
@Override
public void onCatalystInstanceDestroy() {
mShuttingDown = true;
// serialize on the AsyncTask thread, and block
try {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
for (int i = 0; i < mClients.size(); i++) {
try {
mClients.valueAt(i).close();
} catch (IOException e) {
FLog.e(TAG, "exception when shutting down", e);
}
}
mClients.clear();
}
}.execute().get();
} catch (InterruptedException e) {
FLog.e(TAG, "onCatalystInstanceDestroy", e);
} catch (ExecutionException e) {
FLog.e(TAG, "onCatalystInstanceDestroy", e);
}
}
/**
* Private method to retrieve clients. Must be called from a GuardedAsyncTask for thread-safety.
*/
private UdpSocketClient findClient(final Integer cId, final Callback callback) {
final UdpSocketClient client = mClients.get(cId);
if (client == null) {
if (callback == null) {
FLog.e(TAG, "missing callback parameter.");
} else {
callback.invoke(UdpErrorUtil.getError(null, "no client found with id " + cId), null);
}
}
return client;
}
/**
* Creates a {@link UdpSocketClient} with the given ID, and options
*/
@ReactMethod
public void createSocket(final Integer cId, final ReadableMap options) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
if (cId == null) {
FLog.e(TAG, "createSocket called with nil id parameter.");
return;
}
UdpSocketClient client = mClients.get(cId);
if (client != null) {
FLog.e(TAG, "createSocket called twice with the same id.");
return;
}
UdpSocketClient.Builder builder = new UdpSocketClient.Builder(UdpSockets.this, UdpSockets.this);
mClients.put(cId, builder.build());
}
}.execute();
}
/**
* Binds to a given port and address, and begins listening for data.
*/
@ReactMethod
public void bind(final Integer cId, final Integer port, final @Nullable String address,
final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
UdpSocketClient client = findClient(cId, callback);
if (client == null) {
return;
}
try {
client.bind(port, address);
WritableMap result = Arguments.createMap();
result.putString("address", address);
result.putInt("port", port);
callback.invoke(null, result);
} catch (SocketException exception) {
// Socket is already bound or a problem occurred during binding
callback.invoke(UdpErrorUtil.getError(null, exception.getMessage()));
} catch (IllegalArgumentException exception) {
// SocketAddress is not supported
callback.invoke(UdpErrorUtil.getError(null, exception.getMessage()));
} catch (IOException exception) {
// an exception occurred
callback.invoke(UdpErrorUtil.getError(null, exception.getMessage()));
}
}
}.execute();
}
/**
* Sends udp data via the {@link UdpSocketClient}
*/
@ReactMethod
public void send(final Integer cId, final String base64String,
final Integer port, final String address, final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
UdpSocketClient client = findClient(cId, callback);
if (client == null) {
return;
}
try {
client.send(base64String, port, address, callback);
} catch (UnknownHostException e) {
callback.invoke(UdpErrorUtil.getError(null, e.getMessage()));
}
}
}.execute();
}
/**
* Closes a specific client's socket, and removes it from the list of known clients.
*/
@ReactMethod
public void close(final Integer cId, final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
UdpSocketClient client = findClient(cId, callback);
if (client == null) {
return;
}
try {
client.close();
callback.invoke();
} catch (IOException e) {
callback.invoke(UdpErrorUtil.getError(null, e.getMessage()));
}
mClients.remove(cId);
}
}.execute();
}
/**
* Sets the broadcast flag for a given client.
*/
@ReactMethod
public void setBroadcast(final Integer cId, final Boolean flag, final Callback callback) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
UdpSocketClient client = findClient(cId, callback);
if (client == null) {
return;
}
try {
client.setBroadcast(flag);
callback.invoke();
} catch (SocketException e) {
callback.invoke(UdpErrorUtil.getError(null, e.getMessage()));
}
}
}.execute();
}
/**
* Notifies the javascript layer upon data receipt.
*/
@Override
public void didReceiveData(final UdpSocketClient socket, final String data, final String host, final int port) {
new GuardedAsyncTask<Void, Void>(getReactApplicationContext()) {
@Override
protected void doInBackgroundGuarded(Void... params) {
int clientID = -1;
for(int i = 0; i < mClients.size(); i++) {
clientID = mClients.keyAt(i);
// get the object by the key.
if (socket.equals(mClients.get(clientID))) {
break;
}
}
if (clientID == -1) {
return;
}
WritableMap eventParams = Arguments.createMap();
eventParams.putString("data", data);
eventParams.putString("address", host);
eventParams.putInt("port", port);
ReactContext reactContext = UdpSockets.this.getReactApplicationContext();
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("udp-" + clientID + "-data", eventParams);
}
}.execute();
}
/**
* Logs an error that happened during or prior to data reception.
*/
@Override
public void didReceiveError(UdpSocketClient client, String message) {
FLog.e(TAG, message);
}
/**
* Sends RuntimeExceptions to the application context handler.
*/
@Override
public void didReceiveException(RuntimeException exception) {
getReactApplicationContext().handleException(exception);
}
}
@@ -0,0 +1,43 @@
/**
* UdpSocketsModule.java
* react-native-udp
*
* Created by Andy Prock on 9/24/15.
*/
package com.tradle.react;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class UdpSocketsModule implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new UdpSockets(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}