2018-04-05 02:21:13 +00:00
package cy.agorise.graphenej.api.android ;
2018-02-20 22:13:21 +00:00
import android.app.Service ;
import android.content.Intent ;
import android.os.Binder ;
2018-10-18 17:14:05 +00:00
import android.os.Bundle ;
2018-09-24 01:50:39 +00:00
import android.os.Handler ;
2018-02-20 22:13:21 +00:00
import android.os.IBinder ;
2018-09-24 01:50:39 +00:00
import android.os.Looper ;
2018-02-20 22:13:21 +00:00
import android.util.Log ;
2018-04-05 02:07:09 +00:00
import com.google.gson.Gson ;
2018-08-31 03:32:50 +00:00
import com.google.gson.GsonBuilder ;
2018-04-05 02:07:09 +00:00
import com.google.gson.reflect.TypeToken ;
import java.io.Serializable ;
import java.lang.reflect.Type ;
import java.util.ArrayList ;
import java.util.HashMap ;
2018-06-06 04:11:28 +00:00
import java.util.List ;
2018-10-18 17:14:05 +00:00
import java.util.MissingResourceException ;
2018-10-19 05:10:11 +00:00
import java.util.concurrent.TimeUnit ;
2018-04-05 02:07:09 +00:00
2018-07-25 18:08:09 +00:00
import cy.agorise.graphenej.Asset ;
2018-06-06 15:50:14 +00:00
import cy.agorise.graphenej.AssetAmount ;
2018-09-07 04:30:59 +00:00
import cy.agorise.graphenej.BaseOperation ;
2018-08-17 04:59:14 +00:00
import cy.agorise.graphenej.LimitOrder ;
2018-09-19 21:44:26 +00:00
import cy.agorise.graphenej.Memo ;
2018-04-05 02:07:09 +00:00
import cy.agorise.graphenej.RPC ;
2018-08-31 03:32:50 +00:00
import cy.agorise.graphenej.Transaction ;
import cy.agorise.graphenej.UserAccount ;
2018-04-05 02:07:09 +00:00
import cy.agorise.graphenej.api.ApiAccess ;
2018-02-20 22:13:21 +00:00
import cy.agorise.graphenej.api.ConnectionStatusUpdate ;
2018-04-05 02:07:09 +00:00
import cy.agorise.graphenej.api.calls.ApiCallable ;
2018-11-01 21:54:03 +00:00
import cy.agorise.graphenej.api.calls.GetAccountBalances ;
2018-06-06 04:11:28 +00:00
import cy.agorise.graphenej.api.calls.GetAccounts ;
2018-12-19 17:49:04 +00:00
import cy.agorise.graphenej.api.calls.GetAssets ;
2018-09-06 23:09:18 +00:00
import cy.agorise.graphenej.api.calls.GetFullAccounts ;
2018-10-09 00:55:49 +00:00
import cy.agorise.graphenej.api.calls.GetKeyReferences ;
2018-08-17 04:59:14 +00:00
import cy.agorise.graphenej.api.calls.GetLimitOrders ;
2018-06-14 03:18:36 +00:00
import cy.agorise.graphenej.api.calls.GetMarketHistory ;
2018-06-29 04:42:40 +00:00
import cy.agorise.graphenej.api.calls.GetObjects ;
2018-06-11 20:40:01 +00:00
import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory ;
2018-06-06 15:50:14 +00:00
import cy.agorise.graphenej.api.calls.GetRequiredFees ;
2018-07-25 18:08:09 +00:00
import cy.agorise.graphenej.api.calls.ListAssets ;
2018-06-06 04:11:28 +00:00
import cy.agorise.graphenej.models.AccountProperties ;
2018-04-05 02:07:09 +00:00
import cy.agorise.graphenej.models.ApiCall ;
2019-04-04 20:12:09 +00:00
import cy.agorise.graphenej.models.BitAssetData ;
2018-06-06 04:11:28 +00:00
import cy.agorise.graphenej.models.Block ;
2018-06-12 18:34:42 +00:00
import cy.agorise.graphenej.models.BlockHeader ;
2018-06-14 03:18:36 +00:00
import cy.agorise.graphenej.models.BucketObject ;
2018-08-31 03:32:50 +00:00
import cy.agorise.graphenej.models.DynamicGlobalProperties ;
2018-09-06 23:09:18 +00:00
import cy.agorise.graphenej.models.FullAccountDetails ;
2018-09-06 02:04:46 +00:00
import cy.agorise.graphenej.models.HistoryOperationDetail ;
2018-08-31 03:32:50 +00:00
import cy.agorise.graphenej.models.JsonRpcNotification ;
2018-04-05 02:07:09 +00:00
import cy.agorise.graphenej.models.JsonRpcResponse ;
2018-06-11 20:40:01 +00:00
import cy.agorise.graphenej.models.OperationHistory ;
2018-09-19 21:44:26 +00:00
import cy.agorise.graphenej.network.FullNode ;
import cy.agorise.graphenej.network.LatencyNodeProvider ;
2018-09-21 02:51:14 +00:00
import cy.agorise.graphenej.network.NodeLatencyVerifier ;
2018-09-19 21:44:26 +00:00
import cy.agorise.graphenej.network.NodeProvider ;
2018-08-31 03:32:50 +00:00
import cy.agorise.graphenej.operations.CustomOperation ;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation ;
import cy.agorise.graphenej.operations.TransferOperation ;
2018-10-18 22:36:34 +00:00
import cy.agorise.graphenej.stats.ExponentialMovingAverage ;
2018-09-21 02:51:14 +00:00
import io.reactivex.Observer ;
import io.reactivex.android.schedulers.AndroidSchedulers ;
2018-04-05 02:21:13 +00:00
import io.reactivex.annotations.Nullable ;
2018-09-21 02:51:14 +00:00
import io.reactivex.disposables.Disposable ;
import io.reactivex.subjects.PublishSubject ;
2018-02-20 22:13:21 +00:00
import okhttp3.OkHttpClient ;
import okhttp3.Request ;
import okhttp3.Response ;
import okhttp3.WebSocket ;
import okhttp3.WebSocketListener ;
/ * *
2018-04-05 02:21:13 +00:00
* Service in charge of maintaining a connection to the full node .
2018-02-20 22:13:21 +00:00
* /
public class NetworkService extends Service {
private final String TAG = this . getClass ( ) . getName ( ) ;
2018-09-20 21:21:25 +00:00
public static final int NORMAL_CLOSURE_STATUS = 1000 ;
2018-11-08 03:20:36 +00:00
private static final int GOING_AWAY_STATUS = 1001 ;
2018-02-20 22:13:21 +00:00
2018-09-24 01:50:39 +00:00
// Time to wait before retrying a connection attempt
2018-11-06 17:36:11 +00:00
private static final int DEFAULT_RETRY_DELAY = 500 ;
2018-09-24 01:50:39 +00:00
2018-10-19 05:10:11 +00:00
// Default connection delay when using the node latency verification strategy. This initial
// delay is required in order ot make sure we have a fair selection of node latencies from
// which we can choose from.
2019-06-22 02:02:55 +00:00
private final int DEFAULT_INITIAL_DELAY = 500 ;
2018-10-19 05:10:11 +00:00
2018-10-18 22:36:34 +00:00
/ * *
* 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 .
* /
2018-04-05 02:07:09 +00:00
public static final String KEY_USERNAME = "key_username" ;
2018-10-18 22:36:34 +00:00
/ * *
* 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 .
* /
2018-04-05 02:07:09 +00:00
public static final String KEY_PASSWORD = "key_password" ;
2018-10-18 22:36:34 +00:00
/ * *
* 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 .
* /
2018-04-05 02:07:09 +00:00
public static final String KEY_REQUESTED_APIS = "key_requested_apis" ;
2018-10-18 22:36:34 +00:00
/ * *
* 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 .
* /
2018-09-21 02:51:14 +00:00
public static final String KEY_ENABLE_LATENCY_VERIFIER = "key_enable_latency_verifier" ;
2018-10-18 22:36:34 +00:00
/ * *
* 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" ;
2018-09-19 21:44:26 +00:00
/ * *
2018-10-18 17:14:05 +00:00
* Key used to pass via intent a boolean extra to specify whether the connection should
* be automatically established .
2018-10-18 22:36:34 +00:00
* < p >
* This information should be passed as an intent extra when calling the bindService
* or startService methods .
2018-09-19 21:44:26 +00:00
* /
public static final String KEY_AUTO_CONNECT = "key_auto_connect" ;
2018-05-03 01:08:02 +00:00
/ * *
2018-10-18 17:14:05 +00:00
* 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 .
2018-10-18 22:36:34 +00:00
* < p >
2018-05-03 01:08:02 +00:00
* For example :
*
* wss : //domain1.com/ws,wss://domain2.com/ws,wss://domain3.com/ws
2018-10-18 22:36:34 +00:00
* < p >
* This information should be passed as an intent extra when calling the bindService
* or startService methods .
2018-05-03 01:08:02 +00:00
* /
2018-10-18 17:14:05 +00:00
public static final String KEY_NODE_URLS = "key_node_urls" ;
2018-05-03 01:08:02 +00:00
2018-02-20 22:13:21 +00:00
private final IBinder mBinder = new LocalBinder ( ) ;
private WebSocket mWebSocket ;
2018-04-05 02:07:09 +00:00
// Username and password used to connect to a specific node
private String mUsername ;
private String mPassword ;
private boolean isLoggedIn = false ;
private String mLastCall ;
2018-06-06 04:11:28 +00:00
private long mCurrentId = 0 ;
2018-04-05 02:07:09 +00:00
// Requested APIs passed to this service
private int mRequestedApis ;
// Variable used to keep track of the currently obtained API accesses
2018-07-26 22:13:55 +00:00
private HashMap < Integer , Integer > mApiIds = new HashMap < Integer , Integer > ( ) ;
2018-04-05 02:07:09 +00:00
2018-09-21 02:51:14 +00:00
// Variable used as a source of node information
private NodeProvider nodeProvider = new LatencyNodeProvider ( ) ;
// Class used to obtain frequent node latency updates
private NodeLatencyVerifier nodeLatencyVerifier ;
// PublishSubject used to announce full node latencies updates
private PublishSubject < FullNode > fullNodePublishSubject ;
// Counter used to trigger the connection only after we've received enough node latency updates
private long latencyUpdateCounter ;
2018-05-03 01:08:02 +00:00
2018-09-26 22:17:31 +00:00
// Property used to keep track of the currently active node
private FullNode mSelectedNode ;
2019-02-14 04:30:49 +00:00
private Handler mHandler = new Handler ( Looper . getMainLooper ( ) ) ;
2018-10-19 05:10:11 +00:00
2018-08-31 03:32:50 +00:00
private Gson gson = new GsonBuilder ( )
. registerTypeAdapter ( Transaction . class , new Transaction . TransactionDeserializer ( ) )
. registerTypeAdapter ( TransferOperation . class , new TransferOperation . TransferDeserializer ( ) )
. registerTypeAdapter ( LimitOrderCreateOperation . class , new LimitOrderCreateOperation . LimitOrderCreateDeserializer ( ) )
. registerTypeAdapter ( CustomOperation . class , new CustomOperation . CustomOperationDeserializer ( ) )
. registerTypeAdapter ( AssetAmount . class , new AssetAmount . AssetAmountDeserializer ( ) )
. registerTypeAdapter ( UserAccount . class , new UserAccount . UserAccountSimpleDeserializer ( ) )
. registerTypeAdapter ( DynamicGlobalProperties . class , new DynamicGlobalProperties . DynamicGlobalPropertiesDeserializer ( ) )
. registerTypeAdapter ( Memo . class , new Memo . MemoDeserializer ( ) )
2018-09-07 04:30:59 +00:00
. registerTypeAdapter ( BaseOperation . class , new BaseOperation . OperationDeserializer ( ) )
2018-08-31 03:32:50 +00:00
. registerTypeAdapter ( OperationHistory . class , new OperationHistory . OperationHistoryDeserializer ( ) )
. registerTypeAdapter ( JsonRpcNotification . class , new JsonRpcNotification . JsonRpcNotificationDeserializer ( ) )
. create ( ) ;
2018-04-05 02:07:09 +00:00
2018-06-06 04:11:28 +00:00
// Map used to keep track of outgoing request ids and its request types. This is just
// one of two required mappings. The second one is implemented by the DeserializationMap
// class.
private HashMap < Long , Class > mRequestClassMap = new HashMap < > ( ) ;
// This class is used to keep track of the mapping between request classes and response
// payload classes. It also provides a handy method that returns a Gson deserializer instance
// suited for every response type.
private DeserializationMap mDeserializationMap = new DeserializationMap ( ) ;
2018-09-19 21:44:26 +00:00
/ * *
* Actually establishes a connection from this Service to one of the full nodes .
* /
public void connect ( ) {
2018-10-12 23:36:11 +00:00
OkHttpClient client = new OkHttpClient
. Builder ( )
. connectTimeout ( 2 , TimeUnit . SECONDS )
2018-12-26 22:47:59 +00:00
. readTimeout ( 5 , TimeUnit . SECONDS )
. writeTimeout ( 5 , TimeUnit . SECONDS )
2018-10-12 23:36:11 +00:00
. build ( ) ;
2019-07-12 22:51:50 +00:00
synchronized ( mWebSocketListener ) {
mSelectedNode = nodeProvider . getBestNode ( ) ;
if ( mSelectedNode ! = null ) {
Log . d ( TAG , "Trying to connect to: " + mSelectedNode . getUrl ( ) ) ;
Request request = new Request . Builder ( ) . url ( mSelectedNode . getUrl ( ) ) . build ( ) ;
mWebSocket = client . newWebSocket ( request , mWebSocketListener ) ;
} else {
Log . d ( TAG , "Could not find best node, reescheduling" ) ;
// If no node could be found yet, schedule a new attempt in DEFAULT_INITIAL_DELAY ms
mHandler . postDelayed ( mConnectAttempt , DEFAULT_INITIAL_DELAY ) ;
}
2019-06-06 20:59:55 +00:00
}
2018-04-05 02:21:13 +00:00
}
2018-06-06 04:11:28 +00:00
public long sendMessage ( String message ) {
2018-05-03 00:03:17 +00:00
if ( mWebSocket ! = null ) {
if ( mWebSocket . send ( message ) ) {
2018-05-03 01:08:02 +00:00
Log . v ( TAG , "-> " + message ) ;
2018-06-14 05:02:10 +00:00
return mCurrentId ;
2018-05-03 00:03:17 +00:00
}
2018-04-05 02:21:13 +00:00
} else {
2018-05-03 00:03:17 +00:00
throw new RuntimeException ( "Websocket connection has not yet been established" ) ;
2018-04-05 02:21:13 +00:00
}
2018-06-14 05:02:10 +00:00
return - 1 ;
2018-04-05 02:21:13 +00:00
}
2018-06-15 23:26:59 +00:00
/ * *
* Method that will send a message to the full node , and takes as an argument one of the
* API call wrapper classes . This is the preferred method of sending blockchain API calls .
*
* @param apiCallable The object that will get serialized into a request
* @param requiredApi The required APIs for this specific request . Should be one of the
* constants specified in the ApiAccess class .
* @return The id of the message that was just sent , or - 1 if no message was sent .
* /
2019-05-22 18:48:49 +00:00
public synchronized long sendMessage ( ApiCallable apiCallable , int requiredApi ) {
2018-06-15 23:26:59 +00:00
if ( requiredApi ! = - 1 & & mApiIds . containsKey ( requiredApi ) | | requiredApi = = ApiAccess . API_NONE ) {
int apiId = 0 ;
if ( requiredApi ! = ApiAccess . API_NONE )
apiId = mApiIds . get ( requiredApi ) ;
2018-06-14 16:50:35 +00:00
ApiCall call = apiCallable . toApiCall ( apiId , + + mCurrentId ) ;
mRequestClassMap . put ( mCurrentId , apiCallable . getClass ( ) ) ;
if ( mWebSocket ! = null & & mWebSocket . send ( call . toJsonString ( ) ) ) {
Log . v ( TAG , "-> " + call . toJsonString ( ) ) ;
return mCurrentId ;
}
2018-04-05 02:21:13 +00:00
}
2018-06-14 05:02:10 +00:00
return - 1 ;
2018-04-05 02:21:13 +00:00
}
2018-05-03 00:03:17 +00:00
/ * *
* Method used to inform any external party a clue about the current connectivity status
* @return True if the service is currently connected and logged in , false otherwise .
* /
public boolean isConnected ( ) {
return mWebSocket ! = null & & isLoggedIn ;
}
2018-04-05 02:21:13 +00:00
@Override
public void onDestroy ( ) {
2018-04-05 22:35:34 +00:00
if ( mWebSocket ! = null )
mWebSocket . close ( NORMAL_CLOSURE_STATUS , null ) ;
2018-09-21 02:51:14 +00:00
2018-09-21 18:29:47 +00:00
if ( nodeLatencyVerifier ! = null )
nodeLatencyVerifier . stop ( ) ;
2018-04-05 02:21:13 +00:00
}
@Nullable
@Override
public IBinder onBind ( Intent intent ) {
2018-11-06 17:36:11 +00:00
return mBinder ;
}
/ * *
* Initialize information and try to connect to a node accordingly . This methods were moved
* 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 ) {
2018-09-19 21:44:26 +00:00
// Retrieving credentials and requested API data from the shared preferences
2018-10-18 17:14:05 +00:00
mUsername = extras . getString ( NetworkService . KEY_USERNAME , "" ) ;
mPassword = extras . getString ( NetworkService . KEY_PASSWORD , "" ) ;
mRequestedApis = extras . getInt ( NetworkService . KEY_REQUESTED_APIS , 0 ) ;
2019-06-17 21:53:35 +00:00
boolean mAutoConnect = extras . getBoolean ( NetworkService . KEY_AUTO_CONNECT , true ) ;
2018-10-18 17:14:05 +00:00
boolean verifyNodeLatency = extras . getBoolean ( NetworkService . KEY_ENABLE_LATENCY_VERIFIER , false ) ;
2018-09-19 21:44:26 +00:00
// If the user of the library desires, a custom list of node URLs can
2018-10-18 17:14:05 +00:00
// be passed using the KEY_NODE_URLS constant
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 ) ;
2018-09-19 21:44:26 +00:00
}
2018-10-18 17:14:05 +00:00
// Adding user-provided list of node URLs
String [ ] urls = nodeURLStr . split ( "," ) ;
2018-09-19 21:44:26 +00:00
2018-09-21 02:51:14 +00:00
// Feeding all node information to the NodeProvider instance
2018-10-18 17:14:05 +00:00
for ( String nodeUrl : urls ) {
2018-09-19 21:44:26 +00:00
nodeProvider . addNode ( new FullNode ( nodeUrl ) ) ;
}
2019-06-17 21:53:35 +00:00
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 < > ( ) ;
for ( String url : urls ) {
fullNodes . add ( new FullNode ( url , alpha ) ) ;
2018-09-21 02:51:14 +00:00
}
2019-06-17 21:53:35 +00:00
nodeLatencyVerifier = new NodeLatencyVerifier ( fullNodes ) ;
fullNodePublishSubject = nodeLatencyVerifier . start ( ) ;
fullNodePublishSubject . observeOn ( AndroidSchedulers . mainThread ( ) ) . subscribe ( nodeLatencyObserver ) ;
2018-09-21 02:51:14 +00:00
}
2019-06-17 21:53:35 +00:00
if ( mAutoConnect )
connect ( ) ;
else
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
2018-04-05 02:21:13 +00:00
}
2018-11-08 03:20:36 +00:00
/ * *
2019-06-24 21:07:11 +00:00
* Used to close the current connection and cause the service to attempt a reconnection .
2018-11-08 03:20:36 +00:00
* /
2019-06-24 21:07:11 +00:00
public void reconnectNode ( ) {
2018-11-08 03:20:36 +00:00
mWebSocket . close ( GOING_AWAY_STATUS , null ) ;
}
2018-10-19 05:10:11 +00:00
/ * *
* Runnable that will perform a connection attempt with the best node after DEFAULT_INITIAL_DELAY
* milliseconds . This is used only if the node latency verification is activated .
*
* The reason to delay the initial connection is that we want to ideally connect to the best node ,
* meaning the one that offers the lowest latency value . But we have to give some time for the
* first node latency measurement round to finish in order to have at least a partial result set
* that could be used .
* /
private Runnable mConnectAttempt = new Runnable ( ) {
@Override
public void run ( ) {
FullNode fullNode = nodeProvider . getBestNode ( ) ;
if ( fullNode ! = null ) {
Log . i ( TAG , String . format ( "Connected with %d latency results" , latencyUpdateCounter ) ) ;
connect ( ) ;
} else {
mHandler . postDelayed ( this , DEFAULT_INITIAL_DELAY ) ;
}
}
} ;
2018-09-21 02:51:14 +00:00
/ * *
* Observer used to be notified about node latency measurement updates .
* /
private Observer < FullNode > nodeLatencyObserver = new Observer < FullNode > ( ) {
@Override
public void onSubscribe ( Disposable d ) { }
@Override
public void onNext ( FullNode fullNode ) {
latencyUpdateCounter + + ;
// Updating the node with the new latency measurement
nodeProvider . updateNode ( fullNode ) ;
}
@Override
public void onError ( Throwable e ) {
Log . e ( TAG , "nodeLatencyObserver.onError.Msg: " + e . getMessage ( ) ) ;
}
@Override
public void onComplete ( ) { }
} ;
2018-04-05 02:21:13 +00:00
/ * *
* 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 ;
}
}
2018-02-20 22:13:21 +00:00
private WebSocketListener mWebSocketListener = new WebSocketListener ( ) {
@Override
2019-07-12 22:51:50 +00:00
public synchronized void onOpen ( WebSocket webSocket , Response response ) {
2018-02-20 22:13:21 +00:00
super . onOpen ( webSocket , response ) ;
2018-04-05 02:21:13 +00:00
2018-09-26 22:17:31 +00:00
// Marking the selected node as connected
mSelectedNode . setConnected ( true ) ;
// Updating the selected node's 'connected' status on the NodeLatencyVerifier instance
2018-10-02 04:58:03 +00:00
if ( nodeLatencyVerifier ! = null )
nodeLatencyVerifier . updateActiveNodeInformation ( mSelectedNode ) ;
2018-09-26 22:17:31 +00:00
2018-04-05 02:21:13 +00:00
// Notifying all listeners about the new connection status
2018-07-26 22:13:55 +00:00
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . CONNECTED , ApiAccess . API_NONE ) ) ;
2018-04-05 02:07:09 +00:00
2018-04-05 02:21:13 +00:00
// If we're not yet logged in, we should do it now
2018-04-05 02:07:09 +00:00
if ( ! isLoggedIn ) {
ArrayList < Serializable > loginParams = new ArrayList < > ( ) ;
loginParams . add ( mUsername ) ;
loginParams . add ( mPassword ) ;
ApiCall loginCall = new ApiCall ( 1 , RPC . CALL_LOGIN , loginParams , RPC . VERSION , + + mCurrentId ) ;
mLastCall = RPC . CALL_LOGIN ;
sendMessage ( loginCall . toJsonString ( ) ) ;
}
2018-02-20 22:13:21 +00:00
}
@Override
2019-07-12 22:51:50 +00:00
public synchronized void onMessage ( WebSocket webSocket , String text ) {
2018-02-20 22:13:21 +00:00
super . onMessage ( webSocket , text ) ;
2018-04-06 04:16:10 +00:00
Log . v ( TAG , "<- " + text ) ;
2018-08-31 23:11:57 +00:00
JsonRpcNotification notification = gson . fromJson ( text , JsonRpcNotification . class ) ;
2018-04-05 02:07:09 +00:00
2018-08-31 23:11:57 +00:00
if ( notification . method ! = null ) {
// If we are dealing with a notification
handleJsonRpcNotification ( notification ) ;
} else {
// If we are dealing with a response
JsonRpcResponse < ? > response = gson . fromJson ( text , JsonRpcResponse . class ) ;
if ( response . result ! = null ) {
// Handling initial handshake with the full node (authentication and API access checks)
if ( response . result instanceof Double | | response . result instanceof Boolean ) {
switch ( mLastCall ) {
case RPC . CALL_LOGIN :
isLoggedIn = true ;
// Broadcasting result
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . AUTHENTICATED , ApiAccess . API_NONE ) ) ;
checkNextRequestedApiAccess ( ) ;
break ;
case RPC . CALL_DATABASE : {
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken < JsonRpcResponse < Integer > > ( ) { } . getType ( ) ;
JsonRpcResponse < Integer > apiIdResponse = gson . fromJson ( text , IntegerJsonResponse ) ;
// Storing the "database" api id
mApiIds . put ( ApiAccess . API_DATABASE , apiIdResponse . result ) ;
// Broadcasting result
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . API_UPDATE , ApiAccess . API_DATABASE ) ) ;
checkNextRequestedApiAccess ( ) ;
break ;
}
case RPC . CALL_HISTORY : {
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken < JsonRpcResponse < Integer > > ( ) { } . getType ( ) ;
JsonRpcResponse < Integer > apiIdResponse = gson . fromJson ( text , IntegerJsonResponse ) ;
// Broadcasting result
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . API_UPDATE , ApiAccess . API_HISTORY ) ) ;
// Storing the "history" api id
mApiIds . put ( ApiAccess . API_HISTORY , apiIdResponse . result ) ;
checkNextRequestedApiAccess ( ) ;
break ;
}
case RPC . CALL_NETWORK_BROADCAST :
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken < JsonRpcResponse < Integer > > ( ) { } . getType ( ) ;
JsonRpcResponse < Integer > apiIdResponse = gson . fromJson ( text , IntegerJsonResponse ) ;
// Broadcasting result
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . API_UPDATE , ApiAccess . API_NETWORK_BROADCAST ) ) ;
// Storing the "network_broadcast" api access
mApiIds . put ( ApiAccess . API_NETWORK_BROADCAST , apiIdResponse . result ) ;
// All calls have been handled at this point
mLastCall = "" ;
break ;
}
2018-04-06 03:06:00 +00:00
}
2018-04-05 02:07:09 +00:00
}
2018-08-31 23:11:57 +00:00
if ( response . error ! = null & & response . error . message ! = null ) {
// We could not make sense of this incoming message, just log a warning
Log . w ( TAG , "Error.Msg: " + response . error . message ) ;
}
2018-08-31 03:32:50 +00:00
// Properly de-serialize all other fields and broadcasts to the event bus
handleJsonRpcResponse ( response , text ) ;
2018-04-05 02:07:09 +00:00
}
2018-08-31 03:32:50 +00:00
}
2018-04-05 02:21:13 +00:00
2018-08-31 03:32:50 +00:00
/ * *
* Private method that will de - serialize all fields of every kind of JSON - RPC response
* and broadcast it to the event bus .
*
* @param response De - serialized response
* @param text Raw text , as received
* /
private void handleJsonRpcResponse ( JsonRpcResponse response , String text ) {
2018-06-06 04:11:28 +00:00
JsonRpcResponse parsedResponse = null ;
Class requestClass = mRequestClassMap . get ( response . id ) ;
if ( requestClass ! = null ) {
// Removing the class entry in the map
2018-06-15 03:16:34 +00:00
mRequestClassMap . remove ( response . id ) ;
2018-06-06 04:11:28 +00:00
// Obtaining the response payload class
Class responsePayloadClass = mDeserializationMap . getReceivedClass ( requestClass ) ;
Gson gson = mDeserializationMap . getGson ( requestClass ) ;
if ( responsePayloadClass = = Block . class ) {
2018-06-12 18:34:42 +00:00
// If the response payload is a Block instance, we proceed to de-serialize it
2018-06-06 04:11:28 +00:00
Type GetBlockResponse = new TypeToken < JsonRpcResponse < Block > > ( ) { } . getType ( ) ;
2018-06-11 20:40:01 +00:00
parsedResponse = gson . fromJson ( text , GetBlockResponse ) ;
2018-06-12 18:34:42 +00:00
} else if ( responsePayloadClass = = BlockHeader . class ) {
// If the response payload is a BlockHeader instance, we proceed to de-serialize it
Type GetBlockHeaderResponse = new TypeToken < JsonRpcResponse < BlockHeader > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetBlockHeaderResponse ) ;
2018-08-08 23:53:57 +00:00
} else if ( responsePayloadClass = = AccountProperties . class ) {
Type GetAccountByNameResponse = new TypeToken < JsonRpcResponse < AccountProperties > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetAccountByNameResponse ) ;
2018-09-06 02:04:46 +00:00
} else if ( responsePayloadClass = = HistoryOperationDetail . class ) {
Type GetAccountHistoryByOperationsResponse = new TypeToken < JsonRpcResponse < HistoryOperationDetail > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetAccountHistoryByOperationsResponse ) ;
2018-10-04 15:13:55 +00:00
} else if ( responsePayloadClass = = DynamicGlobalProperties . class ) {
Type GetDynamicGlobalPropertiesResponse = new TypeToken < JsonRpcResponse < DynamicGlobalProperties > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetDynamicGlobalPropertiesResponse ) ;
2018-12-26 22:45:41 +00:00
} else if ( responsePayloadClass = = Transaction . class ) {
Type GetTransactionClass = new TypeToken < JsonRpcResponse < Transaction > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetTransactionClass ) ;
2018-09-06 02:04:46 +00:00
} else if ( responsePayloadClass = = List . class ) {
2018-06-06 04:11:28 +00:00
// If the response payload is a List, further inquiry is required in order to
// determine a list of what is expected here
if ( requestClass = = GetAccounts . class ) {
// If the request call was the wrapper to the get_accounts API call, we know
// the response should be in the form of a JsonRpcResponse<List<AccountProperties>>
// so we proceed with that
Type GetAccountsResponse = new TypeToken < JsonRpcResponse < List < AccountProperties > > > ( ) { } . getType ( ) ;
2018-06-11 20:40:01 +00:00
parsedResponse = gson . fromJson ( text , GetAccountsResponse ) ;
2018-06-06 15:50:14 +00:00
} else if ( requestClass = = GetRequiredFees . class ) {
Type GetRequiredFeesResponse = new TypeToken < JsonRpcResponse < List < AssetAmount > > > ( ) { } . getType ( ) ;
2018-06-11 20:40:01 +00:00
parsedResponse = gson . fromJson ( text , GetRequiredFeesResponse ) ;
} else if ( requestClass = = GetRelativeAccountHistory . class ) {
Type RelativeAccountHistoryResponse = new TypeToken < JsonRpcResponse < List < OperationHistory > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , RelativeAccountHistoryResponse ) ;
2018-06-14 03:18:36 +00:00
} else if ( requestClass = = GetMarketHistory . class ) {
Type GetMarketHistoryResponse = new TypeToken < JsonRpcResponse < List < BucketObject > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetMarketHistoryResponse ) ;
2018-06-29 04:42:40 +00:00
} else if ( requestClass = = GetObjects . class ) {
parsedResponse = handleGetObject ( text ) ;
2018-07-25 18:08:09 +00:00
} else if ( requestClass = = ListAssets . class ) {
Type LisAssetsResponse = new TypeToken < JsonRpcResponse < List < Asset > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , LisAssetsResponse ) ;
2018-08-17 04:59:14 +00:00
} else if ( requestClass = = GetLimitOrders . class ) {
Type GetLimitOrdersResponse = new TypeToken < JsonRpcResponse < List < LimitOrder > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetLimitOrdersResponse ) ;
2018-09-06 23:09:18 +00:00
} else if ( requestClass = = GetFullAccounts . class ) {
Type GetFullAccountsResponse = new TypeToken < JsonRpcResponse < List < FullAccountDetails > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetFullAccountsResponse ) ;
2018-10-09 00:55:49 +00:00
} else if ( requestClass = = GetKeyReferences . class ) {
Type GetKeyReferencesResponse = new TypeToken < JsonRpcResponse < List < List < UserAccount > > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetKeyReferencesResponse ) ;
2018-11-01 21:54:03 +00:00
} else if ( requestClass = = GetAccountBalances . class ) {
Type GetAccountBalancesResponse = new TypeToken < JsonRpcResponse < List < AssetAmount > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetAccountBalancesResponse ) ;
2018-12-19 17:49:04 +00:00
} else if ( requestClass = = GetAssets . class ) {
Type GetAssetsResponse = new TypeToken < JsonRpcResponse < List < Asset > > > ( ) { } . getType ( ) ;
parsedResponse = gson . fromJson ( text , GetAssetsResponse ) ;
2018-11-01 21:54:03 +00:00
} else {
2018-06-06 04:11:28 +00:00
Log . w ( TAG , "Unknown request class" ) ;
}
} else {
Log . w ( TAG , "Unhandled situation" ) ;
}
}
// In case the parsedResponse instance is null, we fall back to the raw response
if ( parsedResponse = = null ) {
parsedResponse = response ;
}
// Broadcasting the parsed response to all interested listeners
RxBus . getBusInstance ( ) . send ( parsedResponse ) ;
2018-04-05 02:07:09 +00:00
}
2018-08-31 03:32:50 +00:00
/ * *
* Private method that will just broadcast a de - serialized notification to all interested parties
* @param notification De - serialized notification
* /
private void handleJsonRpcNotification ( JsonRpcNotification notification ) {
// Broadcasting the parsed notification to all interested listeners
RxBus . getBusInstance ( ) . send ( notification ) ;
}
2018-06-29 04:42:40 +00:00
/ * *
* Method used to try to deserialize a ' get_objects ' API call . Since this request can be used
* for several types of objects , the de - serialization procedure can be a bit more complex .
*
* @param response Response to a ' get_objects ' API call
* /
private JsonRpcResponse handleGetObject ( String response ) {
2019-04-04 20:12:09 +00:00
//TODO: Add support for other types of 'get_objects' request types
Gson gson = mDeserializationMap . getGson ( GetObjects . class ) ;
Type GetBitAssetResponse = new TypeToken < JsonRpcResponse < List < BitAssetData > > > ( ) { } . getType ( ) ;
return gson . fromJson ( response , GetBitAssetResponse ) ;
2018-06-29 04:42:40 +00:00
}
/ * *
* Method used to check all possible API accesses .
*
* The service will try to obtain sequentially API access ids for the following APIs :
*
* - Database
* - History
* - Network broadcast
* /
2018-04-05 02:07:09 +00:00
private void checkNextRequestedApiAccess ( ) {
if ( ( mRequestedApis & ApiAccess . API_DATABASE ) = = ApiAccess . API_DATABASE & &
mApiIds . get ( ApiAccess . API_DATABASE ) = = null ) {
// If we need the "database" api access and we don't yet have it
ApiCall apiCall = new ApiCall ( 1 , RPC . CALL_DATABASE , null , RPC . VERSION , + + mCurrentId ) ;
mLastCall = RPC . CALL_DATABASE ;
sendMessage ( apiCall . toJsonString ( ) ) ;
} else if ( ( mRequestedApis & ApiAccess . API_HISTORY ) = = ApiAccess . API_HISTORY & &
mApiIds . get ( ApiAccess . API_HISTORY ) = = null ) {
// If we need the "history" api access and we don't yet have it
ApiCall apiCall = new ApiCall ( 1 , RPC . CALL_HISTORY , null , RPC . VERSION , + + mCurrentId ) ;
mLastCall = RPC . CALL_HISTORY ;
sendMessage ( apiCall . toJsonString ( ) ) ;
} else if ( ( mRequestedApis & ApiAccess . API_NETWORK_BROADCAST ) = = ApiAccess . API_NETWORK_BROADCAST & &
mApiIds . get ( ApiAccess . API_NETWORK_BROADCAST ) = = null ) {
// If we need the "network_broadcast" api access and we don't yet have it
ApiCall apiCall = new ApiCall ( 1 , RPC . CALL_NETWORK_BROADCAST , null , RPC . VERSION , + + mCurrentId ) ;
mLastCall = RPC . CALL_NETWORK_BROADCAST ;
sendMessage ( apiCall . toJsonString ( ) ) ;
}
2018-02-20 22:13:21 +00:00
}
@Override
public void onClosed ( WebSocket webSocket , int code , String reason ) {
super . onClosed ( webSocket , code , reason ) ;
2019-06-24 21:07:11 +00:00
if ( code = = GOING_AWAY_STATUS )
handleWebSocketDisconnection ( true , false ) ;
2018-11-07 22:22:29 +00:00
else
2018-11-08 03:20:36 +00:00
handleWebSocketDisconnection ( false , false ) ;
2018-02-20 22:13:21 +00:00
}
@Override
public void onFailure ( WebSocket webSocket , Throwable t , Response response ) {
super . onFailure ( webSocket , t , response ) ;
2018-05-03 01:08:02 +00:00
Log . e ( TAG , "onFailure. Exception: " + t . getClass ( ) . getName ( ) + ", Msg: " + t . getMessage ( ) ) ;
// Logging error stack trace
for ( StackTraceElement element : t . getStackTrace ( ) ) {
2018-09-21 02:51:14 +00:00
Log . v ( TAG , String . format ( "%s#%s:%s" , element . getClassName ( ) , element . getMethodName ( ) , element . getLineNumber ( ) ) ) ;
2018-05-03 01:08:02 +00:00
}
// If there is a response, we print it
2018-04-05 02:07:09 +00:00
if ( response ! = null ) {
Log . e ( TAG , "Response: " + response . message ( ) ) ;
}
2018-05-03 01:08:02 +00:00
2019-06-24 21:07:11 +00:00
handleWebSocketDisconnection ( true , true ) ;
2018-11-07 22:22:29 +00:00
}
2018-10-02 06:35:53 +00:00
2018-11-07 22:22:29 +00:00
/ * *
* Method that encapsulates the behavior of handling a disconnection to the current node , and
* potentially tries to reconnect to another one .
*
2018-11-08 03:20:36 +00:00
* @param tryReconnection States if a reconnection to other node should be tried .
2019-06-24 21:07:11 +00:00
* @param penalizeNode Whether or not to penalize the current node with a very high latency reading .
2018-11-07 22:22:29 +00:00
* /
2019-07-12 22:51:50 +00:00
private synchronized void handleWebSocketDisconnection ( boolean tryReconnection , boolean penalizeNode ) {
2019-06-24 21:07:11 +00:00
Log . d ( TAG , "handleWebSocketDisconnection. try reconnection: " + tryReconnection + ", penalizeNode: " + penalizeNode ) ;
2018-07-26 22:13:55 +00:00
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . DISCONNECTED , ApiAccess . API_NONE ) ) ;
2018-11-07 22:22:29 +00:00
isLoggedIn = false ;
2018-12-31 18:34:10 +00:00
// Clearing previous request id to class mappings
mRequestClassMap . clear ( ) ;
2018-11-15 22:54:57 +00:00
if ( mSelectedNode ! = null ) {
// Marking the selected node as not connected
mSelectedNode . setConnected ( false ) ;
2018-09-26 22:17:31 +00:00
2018-11-15 22:54:57 +00:00
// Updating the selected node's 'connected' status on the NodeLatencyVerifier instance
if ( nodeLatencyVerifier ! = null )
nodeLatencyVerifier . updateActiveNodeInformation ( mSelectedNode ) ;
2018-11-07 22:22:29 +00:00
2019-06-24 21:07:11 +00:00
if ( penalizeNode ) {
2018-11-08 03:20:36 +00:00
// Adding a very high latency value to this node in order to prevent
// us from getting it again
mSelectedNode . addLatencyValue ( Long . MAX_VALUE ) ;
nodeProvider . updateNode ( mSelectedNode ) ;
}
2019-06-24 21:07:11 +00:00
}
if ( tryReconnection ) {
// Registering current status
mCurrentId = 0 ;
mApiIds . clear ( ) ;
2018-11-07 22:22:29 +00:00
RxBus . getBusInstance ( ) . send ( new ConnectionStatusUpdate ( ConnectionStatusUpdate . DISCONNECTED , ApiAccess . API_NONE ) ) ;
if ( nodeProvider . getBestNode ( ) = = null ) {
Log . e ( TAG , "Giving up on connections" ) ;
stopSelf ( ) ;
} else {
2019-02-14 04:30:49 +00:00
mHandler . postDelayed ( new Runnable ( ) {
2018-11-07 22:22:29 +00:00
@Override
public void run ( ) {
connect ( ) ;
}
} , DEFAULT_RETRY_DELAY ) ;
}
}
2018-09-26 22:17:31 +00:00
// We have currently no selected node
mSelectedNode = null ;
2018-02-20 22:13:21 +00:00
}
} ;
2018-07-26 22:13:55 +00:00
/ * *
* Method used to check whether or not the network service is connected to a node that
* offers a specific API .
*
* @param whichApi The API we want to use .
* @return True if the node has got that API enabled , false otherwise
* /
public boolean hasApiId ( int whichApi ) {
return mApiIds . get ( whichApi ) ! = null ;
}
2018-09-17 17:21:48 +00:00
2018-09-19 21:44:26 +00:00
/ * *
* Updates the full node details
* @param fullNode Updated { @link FullNode } instance
* /
public void updateNode ( FullNode fullNode ) {
nodeProvider . updateNode ( fullNode ) ;
2018-09-17 17:21:48 +00:00
}
2018-09-19 21:44:26 +00:00
/ * *
* Returns a list of { @link FullNode } instances
* @return List of full nodes
* /
2018-09-20 21:21:25 +00:00
public List < FullNode > getNodes ( ) {
2018-09-19 21:44:26 +00:00
return nodeProvider . getSortedNodes ( ) ;
2018-09-17 17:21:48 +00:00
}
2018-09-21 02:51:14 +00:00
2019-01-10 20:20:31 +00:00
/ * *
* Returns the currently selected node
* /
public FullNode getSelectedNode ( ) { return mSelectedNode ; }
2018-09-21 02:51:14 +00:00
/ * *
* Returns an observable that will notify its observers about node latency updates .
* @return Observer of { @link FullNode } instances .
* /
public PublishSubject < FullNode > getNodeLatencyObservable ( ) {
return fullNodePublishSubject ;
}
2019-06-13 12:49:36 +00:00
public NodeLatencyVerifier getNodeLatencyVerifier ( ) { return nodeLatencyVerifier ; }
2018-02-20 22:13:21 +00:00
}