diff --git a/graphenej/build.gradle b/graphenej/build.gradle index ee89cb7..56b1946 100644 --- a/graphenej/build.gradle +++ b/graphenej/build.gradle @@ -23,6 +23,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } } + defaultConfig { + multiDexEnabled true + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } } dependencies { @@ -32,6 +36,10 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.0' implementation group: "org.tukaani", name: "xz", version: "1.6" + androidTestImplementation 'com.android.support:support-annotations:27.1.1' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test:rules:1.0.2' + // Rx dependencies api 'io.reactivex.rxjava2:rxandroid:2.0.2' api 'io.reactivex.rxjava2:rxjava:2.1.16' diff --git a/graphenej/src/androidTest/java/cy/agorise/graphenej/NodeLatencyVerifierTest.java b/graphenej/src/androidTest/java/cy/agorise/graphenej/NodeLatencyVerifierTest.java new file mode 100644 index 0000000..bbe80ff --- /dev/null +++ b/graphenej/src/androidTest/java/cy/agorise/graphenej/NodeLatencyVerifierTest.java @@ -0,0 +1,83 @@ +package cy.agorise.graphenej; + +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import cy.agorise.graphenej.api.bitshares.Nodes; +import cy.agorise.graphenej.network.FullNode; +import cy.agorise.graphenej.network.LatencyNodeProvider; +import cy.agorise.graphenej.network.NodeLatencyVerifier; +import cy.agorise.graphenej.network.NodeProvider; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; + +@RunWith(AndroidJUnit4.class) +public class NodeLatencyVerifierTest { + private final String TAG = this.getClass().getName(); + + @Test + public void testNodeLatencyTest() throws Exception { + ArrayList nodeList = new ArrayList<>(); + nodeList.add(new FullNode(Nodes.NODE_URLS[0])); + nodeList.add(new FullNode(Nodes.NODE_URLS[1])); + nodeList.add(new FullNode(Nodes.NODE_URLS[2])); + final NodeLatencyVerifier nodeLatencyVerifier = new NodeLatencyVerifier(nodeList); + PublishSubject subject = nodeLatencyVerifier.start(); + final NodeProvider nodeProvider = new LatencyNodeProvider(); + subject.subscribe(new Observer() { + int counter = 0; + + @Override + public void onSubscribe(Disposable d) {} + + @Override + public void onNext(FullNode fullNode) { + Log.i(TAG,String.format("Avg latency: %.2f, url: %s", fullNode.getLatencyValue(), fullNode.getUrl())); + + // Updating node provider + nodeProvider.updateNode(fullNode); + List sortedNodes = nodeProvider.getSortedNodes(); + for(FullNode node : sortedNodes){ + Log.d(TAG,String.format("> %.2f, url: %s", node.getLatencyValue(), node.getUrl())); + } + + // Finish test after certain amount of rounds + if(counter > 3){ + synchronized (NodeLatencyVerifierTest.this){ + nodeLatencyVerifier.stop(); + NodeLatencyVerifierTest.this.notifyAll(); + } + } + + counter++; + } + + @Override + public void onError(Throwable e) { + Log.e(TAG,"onError.Msg: "+e.getMessage()); + synchronized (NodeLatencyVerifierTest.this){ + NodeLatencyVerifierTest.this.notifyAll(); + } + } + + @Override + public void onComplete() { + Log.d(TAG,"onComplete"); + } + }); + try { + synchronized(this) { + wait(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/graphenej/src/main/AndroidManifest.xml b/graphenej/src/main/AndroidManifest.xml index 1531629..6a1e466 100644 --- a/graphenej/src/main/AndroidManifest.xml +++ b/graphenej/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="cy.agorise.graphenej"> + getSortedNodes(){ + public List getNodes(){ return nodeProvider.getSortedNodes(); } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java b/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java index a201bbf..67befb2 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/LatencyNodeProvider.java @@ -1,10 +1,15 @@ package cy.agorise.graphenej.network; +import android.util.Log; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.PriorityQueue; public class LatencyNodeProvider implements NodeProvider { + private final String TAG = this.getClass().getName(); private PriorityQueue mFullNodeHeap; public LatencyNodeProvider(){ @@ -23,11 +28,8 @@ public class LatencyNodeProvider implements NodeProvider { @Override public boolean updateNode(FullNode fullNode) { - if(mFullNodeHeap.remove(fullNode)){ - return mFullNodeHeap.offer(fullNode); - }else{ - return false; - } + mFullNodeHeap.remove(fullNode); + return mFullNodeHeap.offer(fullNode); } /** @@ -49,7 +51,15 @@ public class LatencyNodeProvider implements NodeProvider { @Override public List getSortedNodes() { FullNode[] nodeArray = mFullNodeHeap.toArray(new FullNode[mFullNodeHeap.size()]); - Arrays.sort(nodeArray); + ArrayList nodeList = new ArrayList<>(); + for(FullNode fullNode : nodeList){ + if(fullNode != null){ + nodeList.add(fullNode); + }else{ + Log.d(TAG,"Found a null node in getSortedNodes"); + } + } + Collections.sort(nodeList); return Arrays.asList(nodeArray); } } diff --git a/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java new file mode 100644 index 0000000..5835f63 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/network/NodeLatencyVerifier.java @@ -0,0 +1,139 @@ +package cy.agorise.graphenej.network; + +import android.os.Handler; +import android.os.Looper; + +import java.util.HashMap; +import java.util.List; + +import cy.agorise.graphenej.api.android.NetworkService; +import io.reactivex.subjects.PublishSubject; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +/** + * Class that encapsulates the node latency verification task + */ +public class NodeLatencyVerifier { + + public static final int DEFAULT_LATENCY_VERIFICATION_PERIOD = 5 * 1000; + + // Variable used to store the list of nodes that should be verified + private List mNodeList; + + // Variable used to store the desired verification period + private long verificationPeriod; + + // Subject used to publish the result to interested parties + private PublishSubject subject = PublishSubject.create(); + + private HashMap nodeURLMap = new HashMap<>(); + +// private WebSocket webSocket; + + // Map used to store the first timestamp required for a RTT (Round Trip Time) measurement. + // If: + // RTT = t2 - t1 + // This map will hold the value of t1 for each one of the nodes to be measured. + private HashMap timestamps = new HashMap<>(); + + private HashMap requestMap = new HashMap<>(); + + private Handler mHandler = new Handler(Looper.getMainLooper()); + + private OkHttpClient client; + + public NodeLatencyVerifier(List nodes){ + this(nodes, DEFAULT_LATENCY_VERIFICATION_PERIOD); + } + + public NodeLatencyVerifier(List nodes, long period){ + mNodeList = nodes; + verificationPeriod = period; + } + + /** + * Method used to start the latency verification task. + *

+ * The returning object can be used for interested parties to receive constant updates + * regarding new latency measurements for every full node. + *

+ * @return A {@link PublishSubject} class instance. + */ + public PublishSubject start(){ + mHandler.post(mVerificationTask); + return subject; + } + + /** + * Method used to cancel the verification task. + */ + public void stop(){ + mHandler.removeCallbacks(mVerificationTask); + } + + /** + * Node latency verification task. + */ + private final Runnable mVerificationTask = new Runnable() { + @Override + public void run() { + for(FullNode fullNode : mNodeList){ + long before = System.currentTimeMillis(); + timestamps.put(fullNode, before); + + // We want to reuse the same OkHttoClient instance if possible + if(client == null) client = new OkHttpClient(); + + // Same thing with the Request instance, we want to reuse them. But since + // we might have one request per node, we keep them in a map. + Request request; + if(requestMap.containsKey(fullNode.getUrl())){ + request = requestMap.get(fullNode.getUrl()); + }else{ + // If the map had no entry for the request we want, we create one + // and add it to the map. + request = new Request.Builder().url(fullNode.getUrl()).build(); + requestMap.put(fullNode.getUrl(), request); + } + + client.newWebSocket(request, mWebSocketListener); + if(!nodeURLMap.containsKey(fullNode.getUrl())){ + nodeURLMap.put(fullNode.getUrl(), fullNode); + } + } + mHandler.postDelayed(this, verificationPeriod); + } + }; + + /** + * Listener that will be called upon a server response. + */ + private WebSocketListener mWebSocketListener = new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + handleResponse(webSocket, response); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + handleResponse(webSocket, response); + } + + private void handleResponse(WebSocket webSocket, Response response){ + String url = "wss://" + webSocket.request().url().host() + webSocket.request().url().encodedPath(); + FullNode fullNode = nodeURLMap.get(url); + long after = System.currentTimeMillis(); + long before = timestamps.get(fullNode); + long delay = after - before; + fullNode.addLatencyValue(delay); + subject.onNext(fullNode); + webSocket.close(NetworkService.NORMAL_CLOSURE_STATUS, null); + } + }; +} diff --git a/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java b/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java index 9b859ad..6f72eb6 100644 --- a/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java +++ b/sample/src/androidTest/java/cy/sample/labs/sample/ExampleInstrumentedTest.java @@ -21,6 +21,6 @@ public class ExampleInstrumentedTest { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("com.luminiasoft.labs.sample", appContext.getPackageName()); + assertEquals("cy.agorise.labs.sample", appContext.getPackageName()); } } diff --git a/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java b/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java index eea9aff..805f850 100644 --- a/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java +++ b/sample/src/main/java/cy/agorise/labs/sample/ConnectedActivity.java @@ -9,6 +9,7 @@ import android.support.v7.app.AppCompatActivity; import android.util.Log; import cy.agorise.graphenej.api.android.NetworkService; +import cy.agorise.graphenej.network.NodeLatencyVerifier; public abstract class ConnectedActivity extends AppCompatActivity implements ServiceConnection { private final String TAG = this.getClass().getName();