Removed most (but not yet all) of the android-related dependencies from the NetworkService class

- The NetworkService no longer extends the Service class
- The NetworkService is now a singleton class, which is instantiated by a static method in a thread-safe manner
- Replaced Android Log.d() functions by System.out.println()
- Renamed the 'onStop' method to simply 'stop' since it will no longer work as a callback
- Renamed the 'bootstrapService' to start, since this is how this class inner workings are started
- Lots of configurable parameters were removed, we now focus on the default values, like empty strings for usernames and passwords
- Node latency verification is done by default, instead of being an optional feature enabled by parameters
- Removed the 'LocalBinder' inner class
- The sample app was modified accordingly
This commit is contained in:
Nelson R. Perez 2019-09-13 18:14:47 -05:00
parent 1e8a1e0346
commit d019acca6a
9 changed files with 153 additions and 662 deletions

View file

@ -1,13 +1,7 @@
package cy.agorise.graphenej.api.android; package cy.agorise.graphenej.api.android;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
@ -62,10 +56,8 @@ import cy.agorise.graphenej.network.NodeProvider;
import cy.agorise.graphenej.operations.CustomOperation; import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation; import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation; import cy.agorise.graphenej.operations.TransferOperation;
import cy.agorise.graphenej.stats.ExponentialMovingAverage;
import io.reactivex.Observer; import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.Nullable;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -75,12 +67,10 @@ import okhttp3.WebSocket;
import okhttp3.WebSocketListener; import okhttp3.WebSocketListener;
/** /**
* Service in charge of maintaining a connection to the full node. * Class in charge of maintaining a connection to the full node.
*/ */
public class NetworkService extends Service { public class NetworkService {
private final String TAG = this.getClass().getName();
public static final int NORMAL_CLOSURE_STATUS = 1000; public static final int NORMAL_CLOSURE_STATUS = 1000;
private static final int GOING_AWAY_STATUS = 1001; private static final int GOING_AWAY_STATUS = 1001;
@ -92,72 +82,6 @@ public class NetworkService extends Service {
// which we can choose from. // which we can choose from.
private final int DEFAULT_INITIAL_DELAY = 500; private final int DEFAULT_INITIAL_DELAY = 500;
/**
* Constant to be used as a key in order to pass the user name information, in case the
* provided API nodes might require this information.
*/
public static final String KEY_USERNAME = "key_username";
/**
* Constant to be used as a key in order to pass the password information, in case the
* provided API nodes might require this information.
* <p>
* This information should be passed as an intent extra when calling the bindService
* or startService methods.
*/
public static final String KEY_PASSWORD = "key_password";
/**
* Constant used as a key in order to specify which APIs the application will be requiring.
* <p>
* This information should be passed as an intent extra when calling the bindService
* or startService methods.
*/
public static final String KEY_REQUESTED_APIS = "key_requested_apis";
/**
* Constant used as a key in order to let the NetworkService know whether or not it should
* start a recurring node latency verification task.
* <p>
* This information should be passed as an intent extra when calling the bindService
* or startService methods.
*/
public static final String KEY_ENABLE_LATENCY_VERIFIER = "key_enable_latency_verifier";
/**
* Constant used as a key in order to specify the alpha (or smoothing) factor to be used in
* the exponential moving average calculated from the different latency samples. This only
* makes sense if the latency verification feature is enabled of course.
* <p>
* This information should be passed as an intent extra when calling the bindService
* or startService methods.
*/
public static final String KEY_NODE_LATENCY_SMOOTHING_FACTOR = "key_node_latency_smoothing_factor";
/**
* Key used to pass via intent a boolean extra to specify whether the connection should
* be automatically established.
* <p>
* This information should be passed as an intent extra when calling the bindService
* or startService methods.
*/
public static final String KEY_AUTO_CONNECT = "key_auto_connect";
/**
* Key used to pass via intent a list of node URLs. The value passed should be a String
* containing a simple comma separated list of URLs.
* <p>
* For example:
*
* wss://domain1.com/ws,wss://domain2.com/ws,wss://domain3.com/ws
* <p>
* This information should be passed as an intent extra when calling the bindService
* or startService methods.
*/
public static final String KEY_NODE_URLS = "key_node_urls";
private final IBinder mBinder = new LocalBinder();
private WebSocket mWebSocket; private WebSocket mWebSocket;
// Username and password used to connect to a specific node // Username and password used to connect to a specific node
@ -170,7 +94,7 @@ public class NetworkService extends Service {
private long mCurrentId = 0; private long mCurrentId = 0;
// Requested APIs passed to this service // Requested APIs passed to this service
private int mRequestedApis; private int mRequestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
// Variable used to keep track of the currently obtained API accesses // Variable used to keep track of the currently obtained API accesses
private HashMap<Integer, Integer> mApiIds = new HashMap<Integer, Integer>(); private HashMap<Integer, Integer> mApiIds = new HashMap<Integer, Integer>();
@ -216,6 +140,31 @@ public class NetworkService extends Service {
// suited for every response type. // suited for every response type.
private DeserializationMap mDeserializationMap = new DeserializationMap(); private DeserializationMap mDeserializationMap = new DeserializationMap();
/**
* Singleton reference
*/
private static NetworkService instance;
/**
* Private constructor
*/
private NetworkService(){}
/**
* Thread-safe singleton getter.
* @return A NetworkService instance.
*/
public static NetworkService getInstance(){
if(instance == null) {
synchronized (NetworkService.class) {
if(instance == null) {
instance = new NetworkService();
}
}
}
return instance;
}
/** /**
* Actually establishes a connection from this Service to one of the full nodes. * Actually establishes a connection from this Service to one of the full nodes.
*/ */
@ -230,11 +179,11 @@ public class NetworkService extends Service {
synchronized (mWebSocketListener){ synchronized (mWebSocketListener){
mSelectedNode = nodeProvider.getBestNode(); mSelectedNode = nodeProvider.getBestNode();
if(mSelectedNode != null){ if(mSelectedNode != null){
Log.d(TAG,"Trying to connect to: "+ mSelectedNode.getUrl()); System.out.println("Trying to connect to: "+ mSelectedNode.getUrl());
Request request = new Request.Builder().url(mSelectedNode.getUrl()).build(); Request request = new Request.Builder().url(mSelectedNode.getUrl()).build();
mWebSocket = client.newWebSocket(request, mWebSocketListener); mWebSocket = client.newWebSocket(request, mWebSocketListener);
}else{ }else{
Log.d(TAG,"Could not find best node, reescheduling"); System.out.println("Could not find best node, reescheduling");
// If no node could be found yet, schedule a new attempt in DEFAULT_INITIAL_DELAY ms // If no node could be found yet, schedule a new attempt in DEFAULT_INITIAL_DELAY ms
mHandler.postDelayed(mConnectAttempt, DEFAULT_INITIAL_DELAY); mHandler.postDelayed(mConnectAttempt, DEFAULT_INITIAL_DELAY);
} }
@ -244,7 +193,7 @@ public class NetworkService extends Service {
public long sendMessage(String message){ public long sendMessage(String message){
if(mWebSocket != null){ if(mWebSocket != null){
if(mWebSocket.send(message)){ if(mWebSocket.send(message)){
Log.v(TAG,"-> " + message); System.out.println("-> " + message);
return mCurrentId; return mCurrentId;
} }
}else{ }else{
@ -270,7 +219,7 @@ public class NetworkService extends Service {
ApiCall call = apiCallable.toApiCall(apiId, ++mCurrentId); ApiCall call = apiCallable.toApiCall(apiId, ++mCurrentId);
mRequestClassMap.put(mCurrentId, apiCallable.getClass()); mRequestClassMap.put(mCurrentId, apiCallable.getClass());
if(mWebSocket != null && mWebSocket.send(call.toJsonString())){ if(mWebSocket != null && mWebSocket.send(call.toJsonString())){
Log.v(TAG,"-> "+call.toJsonString()); System.out.println("-> "+call.toJsonString());
return mCurrentId; return mCurrentId;
} }
} }
@ -285,8 +234,10 @@ public class NetworkService extends Service {
return mWebSocket != null && isLoggedIn; return mWebSocket != null && isLoggedIn;
} }
@Override /**
public void onDestroy() { * Stops the service by closing the connection and stopping the latency verifier.
*/
public void stop() {
if(mWebSocket != null) if(mWebSocket != null)
mWebSocket.close(NORMAL_CLOSURE_STATUS, null); mWebSocket.close(NORMAL_CLOSURE_STATUS, null);
@ -294,48 +245,23 @@ public class NetworkService extends Service {
nodeLatencyVerifier.stop(); nodeLatencyVerifier.stop();
} }
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
/** /**
* Initialize information and try to connect to a node accordingly. This methods were moved * Starts the connection
* from onBind to avoid crashes due to components other than {@link NetworkServiceManager}
* binding to the service without submitting the proper information.
*
* @param extras Bundle that contains all required information for a proper initialization
*/ */
public void bootstrapService(Bundle extras) { public void start(String[] urls, double alpha) {
// Retrieving credentials and requested API data from the shared preferences // Retrieving credentials and requested API data from the shared preferences
mUsername = extras.getString(NetworkService.KEY_USERNAME, ""); mUsername = "";
mPassword = extras.getString(NetworkService.KEY_PASSWORD, ""); mPassword = "";
mRequestedApis = extras.getInt(NetworkService.KEY_REQUESTED_APIS, 0);
boolean mAutoConnect = extras.getBoolean(NetworkService.KEY_AUTO_CONNECT, true);
boolean verifyNodeLatency = extras.getBoolean(NetworkService.KEY_ENABLE_LATENCY_VERIFIER, false);
// If the user of the library desires, a custom list of node URLs can if(urls == null || urls.length == 0){
// be passed using the KEY_NODE_URLS constant throw new MissingResourceException("Expecting at least a node URL to be provided", String.class.getName(), "urls");
String nodeURLStr = extras.getString(NetworkService.KEY_NODE_URLS, "");
if(nodeURLStr.equals("")){
throw new MissingResourceException("A comma-separated list of node URLs must be provided as an intent extra", String.class.getName(), NetworkService.KEY_NODE_URLS);
} }
// Adding user-provided list of node URLs
String[] urls = nodeURLStr.split(",");
// Feeding all node information to the NodeProvider instance // Feeding all node information to the NodeProvider instance
for(String nodeUrl : urls){ for(String nodeUrl : urls){
nodeProvider.addNode(new FullNode(nodeUrl)); nodeProvider.addNode(new FullNode(nodeUrl));
} }
if (!mAutoConnect && !verifyNodeLatency) {
throw new IllegalArgumentException("NetworkService$bootstrapService: verifyNodeLatency cannot be false when autoConnect is false too.");
}
if (verifyNodeLatency) {
double alpha = extras.getDouble(KEY_NODE_LATENCY_SMOOTHING_FACTOR, ExponentialMovingAverage.DEFAULT_ALPHA);
ArrayList<FullNode> fullNodes = new ArrayList<>(); ArrayList<FullNode> fullNodes = new ArrayList<>();
for(String url : urls){ for(String url : urls){
fullNodes.add(new FullNode(url, alpha)); fullNodes.add(new FullNode(url, alpha));
@ -343,14 +269,8 @@ public class NetworkService extends Service {
nodeLatencyVerifier = new NodeLatencyVerifier(fullNodes); nodeLatencyVerifier = new NodeLatencyVerifier(fullNodes);
fullNodePublishSubject = nodeLatencyVerifier.start(); fullNodePublishSubject = nodeLatencyVerifier.start();
fullNodePublishSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(nodeLatencyObserver); fullNodePublishSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(nodeLatencyObserver);
}
if (mAutoConnect)
connect();
else
mHandler.postDelayed(mConnectAttempt, DEFAULT_INITIAL_DELAY); mHandler.postDelayed(mConnectAttempt, DEFAULT_INITIAL_DELAY);
// TODO make sure (verifyNodeLatency==false && mAutoConnect==true) is a valid/useful combination, else simplify and use only one of those arguments
} }
/** /**
@ -374,7 +294,7 @@ public class NetworkService extends Service {
public void run() { public void run() {
FullNode fullNode = nodeProvider.getBestNode(); FullNode fullNode = nodeProvider.getBestNode();
if(fullNode != null){ if(fullNode != null){
Log.i(TAG, String.format("Connected with %d latency results", latencyUpdateCounter)); System.out.println( String.format("Connected with %d latency results", latencyUpdateCounter));
connect(); connect();
}else{ }else{
mHandler.postDelayed(this, DEFAULT_INITIAL_DELAY); mHandler.postDelayed(this, DEFAULT_INITIAL_DELAY);
@ -398,24 +318,13 @@ public class NetworkService extends Service {
@Override @Override
public void onError(Throwable e) { public void onError(Throwable e) {
Log.e(TAG,"nodeLatencyObserver.onError.Msg: "+e.getMessage()); System.out.println("nodeLatencyObserver.onError.Msg: "+e.getMessage());
} }
@Override @Override
public void onComplete() { } public void onComplete() { }
}; };
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
public class LocalBinder extends Binder {
public NetworkService getService() {
// Return this instance of LocalService so clients can call public methods
return NetworkService.this;
}
}
private WebSocketListener mWebSocketListener = new WebSocketListener() { private WebSocketListener mWebSocketListener = new WebSocketListener() {
@Override @Override
@ -446,7 +355,7 @@ public class NetworkService extends Service {
@Override @Override
public synchronized void onMessage(WebSocket webSocket, String text) { public synchronized void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text); super.onMessage(webSocket, text);
Log.v(TAG,"<- "+text); System.out.println("<- "+text);
JsonRpcNotification notification = gson.fromJson(text, JsonRpcNotification.class); JsonRpcNotification notification = gson.fromJson(text, JsonRpcNotification.class);
if(notification.method != null){ if(notification.method != null){
@ -514,7 +423,7 @@ public class NetworkService extends Service {
} }
if(response.error != null && response.error.message != null){ if(response.error != null && response.error.message != null){
// We could not make sense of this incoming message, just log a warning // We could not make sense of this incoming message, just log a warning
Log.w(TAG,"Error.Msg: "+response.error.message); System.out.println("Error.Msg: "+response.error.message);
} }
// Properly de-serialize all other fields and broadcasts to the event bus // Properly de-serialize all other fields and broadcasts to the event bus
handleJsonRpcResponse(response, text); handleJsonRpcResponse(response, text);
@ -597,10 +506,10 @@ public class NetworkService extends Service {
Type GetAssetsResponse = new TypeToken<JsonRpcResponse<List<Asset>>>(){}.getType(); Type GetAssetsResponse = new TypeToken<JsonRpcResponse<List<Asset>>>(){}.getType();
parsedResponse = gson.fromJson(text, GetAssetsResponse); parsedResponse = gson.fromJson(text, GetAssetsResponse);
}else { }else {
Log.w(TAG,"Unknown request class"); System.out.println("Unknown request class");
} }
}else{ }else{
Log.w(TAG,"Unhandled situation"); System.out.println("Unhandled situation");
} }
} }
@ -680,15 +589,15 @@ public class NetworkService extends Service {
@Override @Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) { public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response); super.onFailure(webSocket, t, response);
Log.e(TAG,"onFailure. Exception: "+t.getClass().getName()+", Msg: "+t.getMessage()); System.out.println("onFailure. Exception: "+t.getClass().getName()+", Msg: "+t.getMessage());
// Logging error stack trace // Logging error stack trace
for(StackTraceElement element : t.getStackTrace()){ for(StackTraceElement element : t.getStackTrace()){
Log.v(TAG,String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber())); System.out.println(String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber()));
} }
// If there is a response, we print it // If there is a response, we print it
if(response != null){ if(response != null){
Log.e(TAG,"Response: "+response.message()); System.out.println("Response: "+response.message());
} }
handleWebSocketDisconnection(true, true); handleWebSocketDisconnection(true, true);
@ -702,7 +611,7 @@ public class NetworkService extends Service {
* @param penalizeNode Whether or not to penalize the current node with a very high latency reading. * @param penalizeNode Whether or not to penalize the current node with a very high latency reading.
*/ */
private synchronized void handleWebSocketDisconnection(boolean tryReconnection, boolean penalizeNode) { private synchronized void handleWebSocketDisconnection(boolean tryReconnection, boolean penalizeNode) {
Log.d(TAG,"handleWebSocketDisconnection. try reconnection: " + tryReconnection + ", penalizeNode: " + penalizeNode); System.out.println("handleWebSocketDisconnection. try reconnection: " + tryReconnection + ", penalizeNode: " + penalizeNode);
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE)); RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
isLoggedIn = false; isLoggedIn = false;
@ -733,8 +642,7 @@ public class NetworkService extends Service {
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE)); RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
if (nodeProvider.getBestNode() == null) { if (nodeProvider.getBestNode() == null) {
Log.e(TAG, "Giving up on connections"); System.out.println( "Giving up on connections");
stopSelf();
} else { } else {
mHandler.postDelayed(new Runnable() { mHandler.postDelayed(new Runnable() {
@Override @Override

View file

@ -1,325 +0,0 @@
package cy.agorise.graphenej.api.android;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import cy.agorise.graphenej.stats.ExponentialMovingAverage;
/**
* This class should be instantiated at the application level of the android app.
*
* It will monitor the interaction between the different activities of an app and help us decide
* when the connection to the full node should be interrupted.
*/
public class NetworkServiceManager implements Application.ActivityLifecycleCallbacks {
private final String TAG = this.getClass().getName();
/**
* Constant used to specify how long will the app wait for another activity to go through its starting life
* cycle events before running the teardownConnectionTask task.
*
* This is used as a means to detect whether or not the user has left the app.
*/
private static final int DISCONNECT_DELAY = 1500;
/**
* Handler instance used to schedule tasks back to the main thread
*/
private Handler mHandler = new Handler();
/**
* Weak reference to the application context
*/
private WeakReference<Context> mContextReference;
// In case we want to interact directly with the service
private NetworkService mService;
// Attributes that might need to be passed to the NetworkService
private String mUserName = "";
private String mPassword = "";
private int mRequestedApis;
private List<String> mCustomNodeUrls = new ArrayList<>();
private boolean mAutoConnect;
private boolean mVerifyLatency;
// Flag used to make sure we only call 'bindService' once.
// private boolean mStartingService;
/**
* Runnable used to schedule a service disconnection once the app is not visible to the user for
* more than DISCONNECT_DELAY milliseconds.
*/
private final Runnable mDisconnectRunnable = new Runnable() {
@Override
public void run() {
Context context = mContextReference.get();
if(mService != null){
context.unbindService(mServiceConnection);
mService = null;
}
context.stopService(new Intent(context, NetworkService.class));
}
};
private NetworkServiceManager(Context context){
mContextReference = new WeakReference<>(context);
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) { }
@Override
public void onActivityStarted(Activity activity) { }
@Override
public void onActivityResumed(Activity activity) {
mHandler.removeCallbacks(mDisconnectRunnable);
if(mService == null){
// Creating a new Intent that will be used to start the NetworkService
Context context = mContextReference.get();
Intent intent = new Intent(context, NetworkService.class);
context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
}
/**
* This method passes all the required information to the NetworkService to properly
* initialize itself
*/
private void passRequiredInfoToConfigureService() {
// Adding user-provided node URLs
StringBuilder stringBuilder = new StringBuilder();
Iterator<String> it = mCustomNodeUrls.iterator();
while(it.hasNext()){
stringBuilder.append(it.next());
if(it.hasNext()) stringBuilder.append(",");
}
String customNodes = stringBuilder.toString();
Bundle b = new Bundle();
// Adding all
b.putString(NetworkService.KEY_USERNAME, mUserName);
b.putString(NetworkService.KEY_PASSWORD, mPassword);
b.putInt(NetworkService.KEY_REQUESTED_APIS, mRequestedApis);
b.putString(NetworkService.KEY_NODE_URLS, customNodes);
b.putBoolean(NetworkService.KEY_AUTO_CONNECT, mAutoConnect);
b.putBoolean(NetworkService.KEY_ENABLE_LATENCY_VERIFIER, mVerifyLatency);
mService.bootstrapService(b);
}
@Override
public void onActivityPaused(Activity activity) {
mHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY);
}
@Override
public void onActivityStopped(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {}
@Override
public void onActivityDestroyed(Activity activity) {}
/** Defines callbacks for backend binding, passed to bindService() */
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
boolean passInfo = false;
if(mService == null){
mService = binder.getService();
// We only pass the required information in case this is the first time we get a reference
// to the NetworkService instance.
passInfo = true;
}
if(passInfo)
passRequiredInfoToConfigureService();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
};
public String getUserName() {
return mUserName;
}
public void setUserName(String userName) {
this.mUserName = userName;
}
public String getPassword() {
return mPassword;
}
public void setPassword(String mPassword) {
this.mPassword = mPassword;
}
public int getRequestedApis() {
return mRequestedApis;
}
public void setRequestedApis(int mRequestedApis) {
this.mRequestedApis = mRequestedApis;
}
public List<String> getCustomNodeUrls() {
return mCustomNodeUrls;
}
public void setCustomNodeUrls(List<String> mCustomNodeUrls) {
this.mCustomNodeUrls = mCustomNodeUrls;
}
public boolean isAutoConnect() {
return mAutoConnect;
}
public void setAutoConnect(boolean mAutoConnect) {
this.mAutoConnect = mAutoConnect;
}
public boolean isVerifyLatency() {
return mVerifyLatency;
}
public void setVerifyLatency(boolean mVerifyLatency) {
this.mVerifyLatency = mVerifyLatency;
}
/**
* Class used to create a {@link NetworkServiceManager} with specific attributes.
*/
public static class Builder {
private String username;
private String password;
private int requestedApis;
private List<String> customNodeUrls;
private boolean autoconnect = true;
private boolean verifyNodeLatency;
private double alpha = ExponentialMovingAverage.DEFAULT_ALPHA;
/**
* Sets the user name, if required to connect to a node.
* @param name User name
* @return The Builder instance
*/
public Builder setUserName(String name){
this.username = name;
return this;
}
/**
* Sets the password, if required to connect to a node.
* @param password Password
* @return The Builder instance
*/
public Builder setPassword(String password){
this.password = password;
return this;
}
/**
* Sets an integer with the requested APIs encoded as binary flags.
* @param apis Integer representing the different APIs we require from the node.
* @return The Builder instance
*/
public Builder setRequestedApis(int apis){
this.requestedApis = apis;
return this;
}
/**
* Adds a list of custom node URLs.
* @param nodeUrls List of custom full node URLs.
* @return The Builder instance
*/
public Builder setCustomNodeUrls(List<String> nodeUrls){
this.customNodeUrls = nodeUrls;
return this;
}
/**
* Adds a list of custom node URLs.
* @param nodeUrls List of custom full node URLs.
* @return The Builder instance
*/
public Builder setCustomNodeUrls(String nodeUrls){
String[] urls = nodeUrls.split(",");
for(String url : urls){
if(customNodeUrls == null) customNodeUrls = new ArrayList<>();
customNodeUrls.add(url);
}
return this;
}
/**
* Sets the autoconnect flag. This is true by default.
* @param autoConnect True if we want the service to connect automatically, false otherwise.
* @return The Builder instance
*/
public Builder setAutoConnect(boolean autoConnect){
this.autoconnect = autoConnect;
return this;
}
/**
* Sets the node-verification flag. This is false by default.
* @param verifyLatency True if we want the service to perform a latency analysis before connecting.
* @return The Builder instance.
*/
public Builder setNodeLatencyVerification(boolean verifyLatency){
this.verifyNodeLatency = verifyLatency;
return this;
}
/**
* Sets the node latency verification's exponential moving average alpha parameter.
* @param alpha The alpha parameter to use when computing the exponential moving average of the
* measured latencies.
* @return The Builder instance.
*/
public Builder setLatencyAverageAlpha(double alpha){
this.alpha = alpha;
return this;
}
/**
* Method used to build a {@link NetworkServiceManager} instance with all of the characteristics
* passed as parameters.
* @param context A Context of the application package implementing
* this class.
* @return Instance of the NetworkServiceManager class.
*/
public NetworkServiceManager build(Context context){
NetworkServiceManager manager = new NetworkServiceManager(context);
if(username != null) manager.setUserName(username); else manager.setUserName("");
if(password != null) manager.setPassword(password); else manager.setPassword("");
if(customNodeUrls != null) manager.setCustomNodeUrls(customNodeUrls);
manager.setRequestedApis(requestedApis);
manager.setAutoConnect(autoconnect);
manager.setVerifyLatency(verifyNodeLatency);
return manager;
}
}
}

View file

@ -1,62 +1,20 @@
package cy.agorise.labs.sample; package cy.agorise.labs.sample;
import android.content.ComponentName; import android.os.Bundle;
import android.content.Context; import android.support.annotation.Nullable;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import cy.agorise.graphenej.api.android.NetworkService; import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.network.NodeLatencyVerifier;
public abstract class ConnectedActivity extends AppCompatActivity implements ServiceConnection { public abstract class ConnectedActivity extends AppCompatActivity {
private final String TAG = this.getClass().getName(); private final String TAG = this.getClass().getName();
/* Network service connection */ /* Network service connection */
protected NetworkService mNetworkService; protected NetworkService mNetworkService;
/**
* Flag used to keep track of the NetworkService binding state
*/
private boolean mShouldUnbindNetwork;
private ServiceConnection mNetworkServiceConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName className, protected void onCreate(@Nullable Bundle savedInstanceState) {
IBinder service) { super.onCreate(savedInstanceState);
// We've bound to LocalService, cast the IBinder and get LocalService instance mNetworkService = NetworkService.getInstance();
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mNetworkService = binder.getService();
ConnectedActivity.this.onServiceConnected(className, service);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
ConnectedActivity.this.onServiceDisconnected(componentName);
}
};
@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, NetworkService.class);
// Binding to NetworkService
if(bindService(intent, mNetworkServiceConnection, Context.BIND_AUTO_CREATE)){
mShouldUnbindNetwork = true;
}else{
Log.e(TAG,"Binding to the network service failed.");
}
}
@Override
protected void onPause() {
super.onPause();
// Unbinding from network service
if(mShouldUnbindNetwork){
unbindService(mNetworkServiceConnection);
mShouldUnbindNetwork = false;
}
} }
} }

View file

@ -1,8 +1,6 @@
package cy.agorise.labs.sample; package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
@ -239,10 +237,4 @@ public class HtlcActivity extends ConnectedActivity implements
// Return a newly built transaction // Return a newly built transaction
return new Transaction(privKey, blockData, operations); return new Transaction(privKey, blockData, operations);
} }
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) { }
@Override
public void onServiceDisconnected(ComponentName componentName) { }
} }

View file

@ -1,9 +1,7 @@
package cy.agorise.labs.sample; package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.support.design.widget.TextInputEditText; import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout; import android.support.design.widget.TextInputLayout;
import android.text.InputType; import android.text.InputType;
@ -418,12 +416,14 @@ public class PerformCallActivity extends ConnectedActivity {
} }
private void sendGetObjectsRequest(){ private void sendGetObjectsRequest(){
Log.d(TAG,"sendGetObjectsRequest");
String objectId = param1.getText().toString(); String objectId = param1.getText().toString();
if(objectId.matches("\\d\\.\\d{1,3}\\.\\d{1,10}")){ if(objectId.matches("\\d\\.\\d{1,3}\\.\\d{1,10}")){
ArrayList<String> array = new ArrayList<>(); ArrayList<String> array = new ArrayList<>();
array.add(objectId); array.add(objectId);
GetObjects getObjects = new GetObjects(array); GetObjects getObjects = new GetObjects(array);
long id = mNetworkService.sendMessage(getObjects, GetObjects.REQUIRED_API); long id = mNetworkService.sendMessage(getObjects, GetObjects.REQUIRED_API);
Log.d(TAG,"sendGetObnjetcsRequest id: "+id);
responseMap.put(id, mRPC); responseMap.put(id, mRPC);
}else{ }else{
param1.setError(getResources().getString(R.string.error_input_id)); param1.setError(getResources().getString(R.string.error_input_id));
@ -639,14 +639,4 @@ public class PerformCallActivity extends ConnectedActivity {
if(!mDisposable.isDisposed()) if(!mDisposable.isDisposed())
mDisposable.dispose(); mDisposable.dispose();
} }
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
// Called upon NetworkService connection
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
// Called upon NetworkService disconnection
}
} }

View file

@ -1,17 +1,12 @@
package cy.agorise.labs.sample; package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.util.SortedList; import android.support.v7.util.SortedList;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -40,7 +35,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.PublishSubject;
public class RemoveNodeActivity extends AppCompatActivity implements ServiceConnection { public class RemoveNodeActivity extends ConnectedActivity {
private final String TAG = this.getClass().getName(); private final String TAG = this.getClass().getName();
@ -66,18 +61,6 @@ public class RemoveNodeActivity extends AppCompatActivity implements ServiceConn
rvNodes.setLayoutManager(new LinearLayoutManager(this)); rvNodes.setLayoutManager(new LinearLayoutManager(this));
nodesAdapter = new FullNodesAdapter(this, LATENCY_COMPARATOR); nodesAdapter = new FullNodesAdapter(this, LATENCY_COMPARATOR);
rvNodes.setAdapter(nodesAdapter); rvNodes.setAdapter(nodesAdapter);
}
@OnClick(R.id.btnReconnectNode)
public void removeCurrentNode() {
mNetworkService.reconnectNode();
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) iBinder;
mNetworkService = binder.getService();
if(mNetworkService != null){ if(mNetworkService != null){
// PublishSubject used to announce full node latencies updates // PublishSubject used to announce full node latencies updates
@ -90,9 +73,9 @@ public class RemoveNodeActivity extends AppCompatActivity implements ServiceConn
} }
} }
@Override @OnClick(R.id.btnReconnectNode)
public void onServiceDisconnected(ComponentName componentName) { public void removeCurrentNode() {
mNetworkService = null; mNetworkService.reconnectNode();
} }
/** /**
@ -119,20 +102,6 @@ public class RemoveNodeActivity extends AppCompatActivity implements ServiceConn
public void onComplete() { } public void onComplete() { }
}; };
@Override
protected void onStart() {
super.onStart();
// Bind to LocalService
Intent intent = new Intent(this, NetworkService.class);
bindService(intent, this, Context.BIND_AUTO_CREATE);
}
@Override
protected void onPause() {
super.onPause();
unbindService(this);
}
class FullNodesAdapter extends RecyclerView.Adapter<FullNodesAdapter.ViewHolder> { class FullNodesAdapter extends RecyclerView.Adapter<FullNodesAdapter.ViewHolder> {
class ViewHolder extends RecyclerView.ViewHolder { class ViewHolder extends RecyclerView.ViewHolder {

View file

@ -2,12 +2,7 @@ package cy.agorise.labs.sample;
import android.app.Application; import android.app.Application;
import java.util.Arrays; import cy.agorise.labs.sample.network.NetworkServiceManager;
import java.util.Iterator;
import java.util.List;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.android.NetworkServiceManager;
/** /**
* Sample application class * Sample application class
@ -18,9 +13,6 @@ public class SampleApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
// Specifying some important information regarding the connection, such as the
// credentials and the requested API accesses
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
String[] nodeURLs = new String[]{ String[] nodeURLs = new String[]{
"wss://bitshares.openledger.info/ws", "wss://bitshares.openledger.info/ws",
@ -29,38 +21,11 @@ public class SampleApplication extends Application {
"wss://citadel.li/node", "wss://citadel.li/node",
"wss://api.bts.mobi/ws" "wss://api.bts.mobi/ws"
}; };
List<String> nodeList = Arrays.asList(nodeURLs);
String nodes = join(nodeList, ",");
NetworkServiceManager networkManager = new NetworkServiceManager.Builder() NetworkServiceManager networkManager = new NetworkServiceManager(nodeURLs);
.setUserName("username")
.setPassword("secret")
.setRequestedApis(requestedApis)
.setCustomNodeUrls(nodes)
.setAutoConnect(true)
.setNodeLatencyVerification(true)
.setLatencyAverageAlpha(0.1f)
.build(this);
// Registering this class as a listener to all activity's callback cycle events, in order to // Registering this class as a listener to all activity's callback cycle events, in order to
// better estimate when the user has left the app and it is safe to disconnect the websocket connection // better estimate when the user has left the app and it is safe to disconnect the websocket connection
registerActivityLifecycleCallbacks(networkManager); registerActivityLifecycleCallbacks(networkManager);
} }
/**
* Private method used to join a sequence of Strings given a iterable representation
* and a delimiter.
*
*
* @param s Any collection of CharSequence that implements the Iterable interface.
* @param delimiter The delimiter which will be used to join the different strings together.
* @return A single string combining all the iterable pieces with the delimiter.
*/
private String join(Iterable<? extends CharSequence> s, String delimiter) {
Iterator<? extends CharSequence> iter = s.iterator();
if (!iter.hasNext()) return "";
StringBuilder buffer = new StringBuilder(iter.next());
while (iter.hasNext()) buffer.append(delimiter).append(iter.next());
return buffer.toString();
}
} }

View file

@ -1,12 +1,6 @@
package cy.agorise.labs.sample; package cy.agorise.labs.sample;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
@ -14,7 +8,6 @@ import android.widget.TextView;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.api.android.RxBus; import cy.agorise.graphenej.api.android.RxBus;
import cy.agorise.graphenej.api.calls.CancelAllSubscriptions; import cy.agorise.graphenej.api.calls.CancelAllSubscriptions;
import cy.agorise.graphenej.api.calls.SetSubscribeCallback; import cy.agorise.graphenej.api.calls.SetSubscribeCallback;
@ -23,16 +16,13 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer; import io.reactivex.functions.Consumer;
public class SubscriptionActivity extends AppCompatActivity { public class SubscriptionActivity extends ConnectedActivity {
private final String TAG = this.getClass().getName(); private final String TAG = this.getClass().getName();
@BindView(R.id.text_field) @BindView(R.id.text_field)
TextView mTextField; TextView mTextField;
// In case we want to interact directly with the service
private NetworkService mService;
private Disposable mDisposable; private Disposable mDisposable;
// Notification counter // Notification counter
@ -63,51 +53,19 @@ public class SubscriptionActivity extends AppCompatActivity {
}); });
} }
@Override
protected void onStart() {
super.onStart();
// Bind to LocalService
Intent intent = new Intent(this, NetworkService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onPause() {
super.onPause();
unbindService(mConnection);
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
mDisposable.dispose(); mDisposable.dispose();
} }
/** Defines callbacks for backend binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
Log.d(TAG,"onServiceConnected");
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG,"onServiceDisconnected");
}
};
@OnClick(R.id.subscribe) @OnClick(R.id.subscribe)
public void onTransferFeeUsdClicked(View v){ public void onTransferFeeUsdClicked(View v){
mService.sendMessage(new SetSubscribeCallback(true), SetSubscribeCallback.REQUIRED_API); mNetworkService.sendMessage(new SetSubscribeCallback(true), SetSubscribeCallback.REQUIRED_API);
} }
@OnClick(R.id.unsubscribe) @OnClick(R.id.unsubscribe)
public void onTransferFeeBtsClicked(View v){ public void onTransferFeeBtsClicked(View v){
mService.sendMessage(new CancelAllSubscriptions(), CancelAllSubscriptions.REQUIRED_API); mNetworkService.sendMessage(new CancelAllSubscriptions(), CancelAllSubscriptions.REQUIRED_API);
} }
} }

View file

@ -0,0 +1,76 @@
package cy.agorise.labs.sample.network;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import cy.agorise.graphenej.api.android.NetworkService;
import cy.agorise.graphenej.stats.ExponentialMovingAverage;
public class NetworkServiceManager implements Application.ActivityLifecycleCallbacks {
private final String TAG = this.getClass().getName();
/**
* Constant used to specify how long will the app wait for another activity to go through its starting life
* cycle events before running the teardownConnectionTask task.
*
* This is used as a means to detect whether or not the user has left the app.
*/
private static final int DISCONNECT_DELAY = 1500;
/**
* Handler instance used to schedule tasks back to the main thread
*/
private Handler mHandler = new Handler();
private NetworkService mNetworkService;
private String[] mNodeUrls;
public NetworkServiceManager(String[] nodes){
this.mNodeUrls = nodes;
}
/**
* Runnable used to schedule a service disconnection once the app is not visible to the user for
* more than DISCONNECT_DELAY milliseconds.
*/
private final Runnable mDisconnectRunnable = new Runnable() {
@Override
public void run() {
mNetworkService.stop();
mNetworkService = null;
}
};
@Override
public void onActivityCreated(Activity activity, Bundle bundle) { }
@Override
public void onActivityStarted(Activity activity) { }
@Override
public void onActivityResumed(Activity activity) {
mHandler.removeCallbacks(mDisconnectRunnable);
if(mNetworkService == null) {
mNetworkService = NetworkService.getInstance();
mNetworkService.start(this.mNodeUrls, ExponentialMovingAverage.DEFAULT_ALPHA);
}
}
@Override
public void onActivityPaused(Activity activity) {
mHandler.postDelayed(mDisconnectRunnable, DISCONNECT_DELAY);
}
@Override
public void onActivityStopped(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {}
@Override
public void onActivityDestroyed(Activity activity) {}
}