+ * De-serializer used to unpack data from a generic operation. The general format used in the
+ * JSON-RPC blockchain API is the following:
+ *
+ *
+ * [OPERATION_ID, OPERATION_OBJECT]
+ *
+ *
+ * Where OPERATION_ID is one of the operations defined in {@link cy.agorise.graphenej.OperationType}
+ * and OPERATION_OBJECT is the actual operation serialized in the JSON format.
+ *
+ * Here's an example of this serialized form for a transfer operation:
+ * If this class is used, this serialized data will be translated to a TransferOperation object instance.
+ *
+ * TODO: Add support for operations other than the 'transfer'
+ */
+ public static class OperationDeserializer implements JsonDeserializer {
+
+ @Override
+ public BaseOperation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ BaseOperation operation = null;
+ if(json.isJsonArray()){
+ JsonArray array = json.getAsJsonArray();
+ if(array.get(0).getAsLong() == OperationType.TRANSFER_OPERATION.ordinal()){
+ operation = context.deserialize(array.get(1), TransferOperation.class);
+ }
+ }
+ return operation;
+ }
+ }
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java b/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java
index e54e3b7..c1808f7 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/BrainKey.java
@@ -1,5 +1,7 @@
package cy.agorise.graphenej;
+import android.annotation.SuppressLint;
+
import org.bitcoinj.core.DumpedPrivateKey;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
@@ -69,7 +71,15 @@ public class BrainKey {
public BrainKey(String words, int sequence) {
this.mBrainKey = words;
this.sequenceNumber = sequence;
- String encoded = String.format("%s %d", words, sequence);
+ derivePrivateKey();
+ }
+
+ /**
+ * Generates the actual private key from the brainkey + sequence number
+ */
+ private void derivePrivateKey(){
+ @SuppressLint("DefaultLocale")
+ String encoded = String.format("%s %d", this.mBrainKey, this.sequenceNumber);
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] bytes = md.digest(encoded.getBytes("UTF-8"));
@@ -120,19 +130,28 @@ public class BrainKey {
}
/**
- * Brain key words getter
- * @return: The word sequence that comprises this brain key
+ * Brain key words getter.
+ * @return The word sequence that comprises this brain key
*/
public String getBrainKey(){
return mBrainKey;
}
/**
- * Sequence number getter
- * @return: The sequence number used alongside with the brain key words in order
+ * Sequence number getter.
+ * @return The sequence number used alongside with the brain key words in order
* to derive the private key
*/
public int getSequenceNumber(){
return sequenceNumber;
}
+
+ /**
+ * Sequence number setter.
+ * @param sequenceNumber The sequence number used to generate a specific key from this brainkey
+ */
+ public void setSequenceNumber(int sequenceNumber) {
+ this.sequenceNumber = sequenceNumber;
+ derivePrivateKey();
+ }
}
\ No newline at end of file
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java b/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java
index ada71a3..9e6bc66 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/Extensions.java
@@ -1,12 +1,17 @@
package cy.agorise.graphenej;
import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+
import cy.agorise.graphenej.interfaces.ByteSerializable;
import cy.agorise.graphenej.interfaces.JsonSerializable;
-import java.util.ArrayList;
-
/**
* Created by nelson on 11/9/16.
*/
@@ -40,4 +45,15 @@ public class Extensions implements JsonSerializable, ByteSerializable {
public int size(){
return extensions.size();
}
+
+ /**
+ * Custom de-serializer used to avoid problems when de-serializing an object that contains
+ * an extension array.
+ */
+ public static class ExtensionsDeserializer implements JsonDeserializer {
+ @Override
+ public Extensions deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ return null;
+ }
+ }
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java b/graphenej/src/main/java/cy/agorise/graphenej/Memo.java
similarity index 96%
rename from graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java
rename to graphenej/src/main/java/cy/agorise/graphenej/Memo.java
index 2f77218..e0c9f03 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/objects/Memo.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/Memo.java
@@ -1,4 +1,4 @@
-package cy.agorise.graphenej.objects;
+package cy.agorise.graphenej;
import com.google.common.primitives.Bytes;
import com.google.gson.Gson;
@@ -19,9 +19,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
-import cy.agorise.graphenej.Address;
-import cy.agorise.graphenej.PublicKey;
-import cy.agorise.graphenej.Util;
import cy.agorise.graphenej.errors.ChecksumException;
import cy.agorise.graphenej.errors.MalformedAddressException;
import cy.agorise.graphenej.interfaces.ByteSerializable;
@@ -32,7 +29,6 @@ import cy.agorise.graphenej.interfaces.JsonSerializable;
* {@url https://bitshares.org/doxygen/structgraphene_1_1chain_1_1memo__data.html}
*/
public class Memo implements ByteSerializable, JsonSerializable {
- public final static String TAG = "Memo";
public static final String KEY_FROM = "from";
public static final String KEY_TO = "to";
public static final String KEY_NONCE = "nonce";
@@ -291,13 +287,15 @@ public class Memo implements ByteSerializable, JsonSerializable {
memoObject.addProperty(KEY_FROM, "");
memoObject.addProperty(KEY_TO, "");
memoObject.addProperty(KEY_NONCE, "");
- memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
+ if(this.message != null)
+ memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
return null;
}else{
memoObject.addProperty(KEY_FROM, this.from.toString());
memoObject.addProperty(KEY_TO, this.to.toString());
memoObject.addProperty(KEY_NONCE, String.format("%x", this.nonce));
- memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
+ if(this.message != null)
+ memoObject.addProperty(KEY_MESSAGE, Util.bytesToHex(this.message));
}
return memoObject;
}
@@ -310,8 +308,9 @@ public class Memo implements ByteSerializable, JsonSerializable {
*/
public JsonElement toJson(boolean decimal){
JsonElement jsonElement = toJsonObject();
- if(decimal){
+ if(decimal && jsonElement != null){
JsonObject jsonObject = (JsonObject) jsonElement;
+ // The nonce is interpreted in base 16, but it is going to be written in base 10
BigInteger nonce = new BigInteger(jsonObject.get(KEY_NONCE).getAsString(), 16);
jsonObject.addProperty(KEY_NONCE, nonce.toString());
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java b/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java
index f383a08..62aa4f4 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java
@@ -132,4 +132,8 @@ public class OrderBook {
}
return obtainedBase;
}
+
+ public List getLimitOrders(){
+ return limitOrders;
+ }
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java b/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java
index f20f648..4dc5c05 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/PublicKey.java
@@ -53,4 +53,9 @@ public class PublicKey implements ByteSerializable, Serializable {
PublicKey other = (PublicKey) obj;
return this.publicKey.equals(other.getKey());
}
+
+ @Override
+ public String toString() {
+ return getAddress();
+ }
}
\ No newline at end of file
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/RPC.java b/graphenej/src/main/java/cy/agorise/graphenej/RPC.java
index ea6c509..028130c 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/RPC.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/RPC.java
@@ -14,14 +14,17 @@ public class RPC {
public static final String CALL_CANCEL_ALL_SUBSCRIPTIONS = "cancel_all_subscriptions";
public static final String CALL_GET_ACCOUNT_BY_NAME = "get_account_by_name";
public static final String CALL_GET_ACCOUNTS = "get_accounts";
+ public static final String CALL_GET_FULL_ACCOUNTS = "get_full_accounts";
public static final String CALL_GET_DYNAMIC_GLOBAL_PROPERTIES = "get_dynamic_global_properties";
public static final String CALL_BROADCAST_TRANSACTION = "broadcast_transaction";
public static final String CALL_GET_REQUIRED_FEES = "get_required_fees";
public static final String CALL_GET_KEY_REFERENCES = "get_key_references";
public static final String CALL_GET_RELATIVE_ACCOUNT_HISTORY = "get_relative_account_history";
+ public static final String CALL_GET_ACCOUNT_HISTORY = "get_account_history";
+ public static final String CALL_GET_ACCOUNT_HISTORY_BY_OPERATIONS = "get_account_history_by_operations";
public static final String CALL_LOOKUP_ACCOUNTS = "lookup_accounts";
public static final String CALL_LIST_ASSETS = "list_assets";
- public static final String GET_OBJECTS = "get_objects";
+ public static final String CALL_GET_OBJECTS = "get_objects";
public static final String GET_ACCOUNT_BALANCES = "get_account_balances";
public static final String CALL_LOOKUP_ASSET_SYMBOLS = "lookup_asset_symbols";
public static final String CALL_GET_BLOCK_HEADER = "get_block_header";
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java b/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java
index dee9197..addee7a 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/UserAccount.java
@@ -47,6 +47,7 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
public static final String KEY_OWNER_SPECIAL_AUTHORITY = "owner_special_authority";
public static final String KEY_ACTIVE_SPECIAL_AUTHORITY = "active_special_authority";
public static final String KEY_N_CONTROL_FLAGS = "top_n_control_flags";
+ public static final String LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59";
@Expose
private String name;
@@ -84,6 +85,7 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
@Expose
private long referrerRewardsPercentage;
+ private boolean isLifeTime;
/**
@@ -248,6 +250,14 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
this.statistics = statistics;
}
+ public boolean isLifeTime() {
+ return isLifeTime;
+ }
+
+ public void setLifeTime(boolean lifeTime) {
+ isLifeTime = lifeTime;
+ }
+
/**
* Deserializer used to build a UserAccount instance from the full JSON-formatted response obtained
* by the 'get_objects' API call.
@@ -274,8 +284,10 @@ public class UserAccount extends GrapheneObject implements ByteSerializable, Jso
// Handling the deserialization and assignation of the membership date, which internally
// is stored as a long POSIX time value
try{
- Date date = dateFormat.parse(jsonAccount.get(KEY_MEMBERSHIP_EXPIRATION_DATE).getAsString());
+ String expirationDate = jsonAccount.get(KEY_MEMBERSHIP_EXPIRATION_DATE).getAsString();
+ Date date = dateFormat.parse(expirationDate);
userAccount.setMembershipExpirationDate(date.getTime());
+ userAccount.setLifeTime(expirationDate.equals(LIFETIME_EXPIRATION_DATE));
} catch (ParseException e) {
System.out.println("ParseException. Msg: "+e.getMessage());
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/ApiAccess.java b/graphenej/src/main/java/cy/agorise/graphenej/api/ApiAccess.java
new file mode 100644
index 0000000..503293f
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/ApiAccess.java
@@ -0,0 +1,12 @@
+package cy.agorise.graphenej.api;
+
+/**
+ * Class used to list all currently supported API accesses
+ */
+
+public class ApiAccess {
+ public static final int API_NONE = 0x00;
+ public static final int API_DATABASE = 0x01;
+ public static final int API_HISTORY = 0x02;
+ public static final int API_NETWORK_BROADCAST = 0x04;
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java b/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java
new file mode 100644
index 0000000..15c5a82
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java
@@ -0,0 +1,65 @@
+package cy.agorise.graphenej.api;
+
+/**
+ * Class used to send connection status updates.
+ *
+ * Connection status updates can be any of the following:
+ * - {@link ConnectionStatusUpdate#CONNECTED}
+ * - {@link ConnectionStatusUpdate#AUTHENTICATED}
+ * - {@link ConnectionStatusUpdate#API_UPDATE}
+ * - {@link ConnectionStatusUpdate#DISCONNECTED}
+ *
+ * This is specified by the field called {@link #updateCode}.
+ *
+ * If the updateCode is ConnectionStatusUpdate#API_UPDATE another extra field called
+ * {@link #api} is used to specify which api we're getting access to.
+ */
+
+public class ConnectionStatusUpdate {
+ // Constant used to announce that a connection has been established
+ public final static int CONNECTED = 0;
+ // Constant used to announce a successful authentication
+ public final static int AUTHENTICATED = 1;
+ // Constant used to announce an api update
+ public final static int API_UPDATE = 2;
+ // Constant used to announce a disconnection event
+ public final static int DISCONNECTED = 3;
+
+ /**
+ * The update code is the general purpose of the update message. Can be any of the following:
+ * - {@link ConnectionStatusUpdate#CONNECTED}
+ * - {@link ConnectionStatusUpdate#AUTHENTICATED}
+ * - {@link ConnectionStatusUpdate#API_UPDATE}
+ * - {@link ConnectionStatusUpdate#DISCONNECTED}
+ */
+ private int updateCode;
+
+ /**
+ * This field is used in case the updateCode is {@link ConnectionStatusUpdate#API_UPDATE} and
+ * it serves to specify which API we're getting access to.
+ *
+ * It can be any of the fields defined in {@link ApiAccess}
+ */
+ private int api;
+
+ public ConnectionStatusUpdate(int updateCode, int api){
+ this.updateCode = updateCode;
+ this.api = api;
+ }
+
+ public int getUpdateCode() {
+ return updateCode;
+ }
+
+ public void setUpdateCode(int updateCode) {
+ this.updateCode = updateCode;
+ }
+
+ public int getApi() {
+ return api;
+ }
+
+ public void setApi(int api) {
+ this.api = api;
+ }
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java b/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java
index 91d83be..e9c0c4f 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/GetObjects.java
@@ -75,11 +75,9 @@ public class GetObjects extends BaseGrapheneHandler {
public void onConnected(WebSocket websocket, Map> headers) throws Exception {
ArrayList params = new ArrayList<>();
ArrayList subParams = new ArrayList<>();
- for(String id : this.ids){
- subParams.add(id);
- }
+ subParams.addAll(this.ids);
params.add(subParams);
- ApiCall apiCall = new ApiCall(0, RPC.GET_OBJECTS, params, RPC.VERSION, 0);
+ ApiCall apiCall = new ApiCall(0, RPC.CALL_GET_OBJECTS, params, RPC.VERSION, 0);
websocket.sendText(apiCall.toJsonString());
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java b/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java
index 935618f..6a8d9d7 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/GetRelativeAccountHistory.java
@@ -18,9 +18,9 @@ import cy.agorise.graphenej.UserAccount;
import cy.agorise.graphenej.interfaces.WitnessResponseListener;
import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.BaseResponse;
-import cy.agorise.graphenej.models.HistoricalTransfer;
+import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.WitnessResponse;
-import cy.agorise.graphenej.objects.Memo;
+import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.TransferOperation;
/**
@@ -158,12 +158,13 @@ public class GetRelativeAccountHistory extends BaseGrapheneHandler {
sendRelativeAccountHistoryRequest();
}else if(baseResponse.id >= GET_HISTORY_DATA){
- Type RelativeAccountHistoryResponse = new TypeToken>>(){}.getType();
+ Type RelativeAccountHistoryResponse = new TypeToken>>(){}.getType();
GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer());
gsonBuilder.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer());
gsonBuilder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer());
gsonBuilder.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer());
- WitnessResponse> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse);
+ WitnessResponse> transfersResponse = gsonBuilder.create().fromJson(response, RelativeAccountHistoryResponse);
mListener.onSuccess(transfersResponse);
}
}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java b/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java
index b6e3b43..1e234d2 100644
--- a/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/SubscriptionMessagesHub.java
@@ -23,9 +23,10 @@ import cy.agorise.graphenej.interfaces.SubscriptionHub;
import cy.agorise.graphenej.interfaces.SubscriptionListener;
import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.DynamicGlobalProperties;
+import cy.agorise.graphenej.models.OperationHistory;
import cy.agorise.graphenej.models.SubscriptionResponse;
import cy.agorise.graphenej.models.WitnessResponse;
-import cy.agorise.graphenej.objects.Memo;
+import cy.agorise.graphenej.Memo;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
@@ -61,7 +62,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
private int subscriptionCounter = 0;
private HashMap mHandlerMap = new HashMap<>();
private List pendingHandlerList = new ArrayList<>();
- private boolean printLogs;
+ private boolean printLogs = true;
// State variables
private boolean isUnsubscribing;
@@ -96,6 +97,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
builder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer());
builder.registerTypeAdapter(DynamicGlobalProperties.class, new DynamicGlobalProperties.DynamicGlobalPropertiesDeserializer());
builder.registerTypeAdapter(Memo.class, new Memo.MemoDeserializer());
+ builder.registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer());
this.gson = builder.create();
}
@@ -186,7 +188,7 @@ public class SubscriptionMessagesHub extends BaseGrapheneHandler implements Subs
}
payload.add(objects);
- ApiCall subscribe = new ApiCall(databaseApiId, RPC.GET_OBJECTS, payload, RPC.VERSION, MANUAL_SUBSCRIPTION_ID);
+ ApiCall subscribe = new ApiCall(databaseApiId, RPC.CALL_GET_OBJECTS, payload, RPC.VERSION, MANUAL_SUBSCRIPTION_ID);
websocket.sendText(subscribe.toJsonString());
subscriptionCounter++;
}else{
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java
new file mode 100644
index 0000000..449bf22
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java
@@ -0,0 +1,215 @@
+package cy.agorise.graphenej.api.android;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.util.HashMap;
+import java.util.List;
+
+import cy.agorise.graphenej.AccountOptions;
+import cy.agorise.graphenej.Asset;
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.AssetOptions;
+import cy.agorise.graphenej.Authority;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.Extensions;
+import cy.agorise.graphenej.LimitOrder;
+import cy.agorise.graphenej.Transaction;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.api.calls.GetAccountByName;
+import cy.agorise.graphenej.api.calls.GetAccountHistoryByOperations;
+import cy.agorise.graphenej.api.calls.GetAccounts;
+import cy.agorise.graphenej.api.calls.GetBlock;
+import cy.agorise.graphenej.api.calls.GetBlockHeader;
+import cy.agorise.graphenej.api.calls.GetFullAccounts;
+import cy.agorise.graphenej.api.calls.GetLimitOrders;
+import cy.agorise.graphenej.api.calls.GetMarketHistory;
+import cy.agorise.graphenej.api.calls.GetObjects;
+import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory;
+import cy.agorise.graphenej.api.calls.GetRequiredFees;
+import cy.agorise.graphenej.api.calls.ListAssets;
+import cy.agorise.graphenej.api.calls.LookupAssetSymbols;
+import cy.agorise.graphenej.models.AccountProperties;
+import cy.agorise.graphenej.models.Block;
+import cy.agorise.graphenej.models.BlockHeader;
+import cy.agorise.graphenej.models.BucketObject;
+import cy.agorise.graphenej.models.FullAccountDetails;
+import cy.agorise.graphenej.models.HistoryOperationDetail;
+import cy.agorise.graphenej.models.OperationHistory;
+import cy.agorise.graphenej.Memo;
+import cy.agorise.graphenej.operations.CustomOperation;
+import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
+import cy.agorise.graphenej.operations.TransferOperation;
+
+/**
+ * Class used to store a mapping of request class to two important things:
+ *
+ * 1- The class to which the corresponding response should be de-serialized to
+ * 2- An instance of the Gson class, with all required type adapters
+ */
+public class DeserializationMap {
+ private final String TAG = this.getClass().getName();
+
+ private HashMap mClassMap = new HashMap<>();
+
+ private HashMap mGsonMap = new HashMap<>();
+
+ public DeserializationMap(){
+ Gson genericGson = new Gson();
+
+ // GetBlock
+ mClassMap.put(GetBlock.class, Block.class);
+ Gson getBlockGson = 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())
+ .create();
+ mGsonMap.put(GetBlock.class, getBlockGson);
+
+ // GetAccounts
+ mClassMap.put(GetAccounts.class, List.class);
+ Gson getAccountsGson = new GsonBuilder()
+ .setExclusionStrategies(new SkipAccountOptionsStrategy())
+ .registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
+ .registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
+ .create();
+ mGsonMap.put(GetAccounts.class, getAccountsGson);
+
+ // GetRequiredFees
+ mClassMap.put(GetRequiredFees.class, List.class);
+ Gson getRequiredFeesGson = new GsonBuilder()
+ .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
+ .create();
+ mGsonMap.put(GetRequiredFees.class, getRequiredFeesGson);
+
+ // GetRelativeAccountHistory
+ mClassMap.put(GetRelativeAccountHistory.class, List.class);
+ Gson getRelativeAcountHistoryGson = new GsonBuilder()
+ .setExclusionStrategies(new SkipAccountOptionsStrategy(), new SkipAssetOptionsStrategy())
+ .registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
+ .registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
+ .registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
+ .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
+ .registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
+ .create();
+ mGsonMap.put(GetRelativeAccountHistory.class, getRelativeAcountHistoryGson);
+
+ // GetBlockHeader
+ mClassMap.put(GetBlockHeader.class, BlockHeader.class);
+ mGsonMap.put(GetBlockHeader.class, genericGson);
+
+ // GetMarketHistory
+ mClassMap.put(GetMarketHistory.class, List.class);
+ Gson getMarketHistoryGson = new GsonBuilder()
+ .registerTypeAdapter(BucketObject.class, new BucketObject.BucketDeserializer())
+ .create();
+ mGsonMap.put(GetMarketHistory.class, getMarketHistoryGson);
+
+ // LookupAssetSymbols
+ mClassMap.put(LookupAssetSymbols.class, List.class);
+ Gson lookupAssetSymbolGson = new GsonBuilder()
+ .registerTypeAdapter(Asset.class, new Asset.AssetDeserializer())
+ .create();
+ mGsonMap.put(LookupAssetSymbols.class, lookupAssetSymbolGson);
+
+ // GetObjects
+ mClassMap.put(GetObjects.class, List.class);
+ Gson getObjectsGson = new GsonBuilder()
+ .registerTypeAdapter(Asset.class, new Asset.AssetDeserializer())
+ .create();
+ mGsonMap.put(GetObjects.class, getObjectsGson);
+
+ // ListAssets
+ mClassMap.put(ListAssets.class, List.class);
+ Gson listAssetsGson = new GsonBuilder()
+ .registerTypeAdapter(Asset.class, new Asset.AssetDeserializer())
+ .create();
+ mGsonMap.put(ListAssets.class, listAssetsGson);
+
+ // GetAccountByName
+ mClassMap.put(GetAccountByName.class, AccountProperties.class);
+ Gson getAccountByNameGson = new GsonBuilder()
+ .registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
+ .registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
+ .create();
+ mGsonMap.put(GetAccountByName.class, getAccountByNameGson);
+
+ // GetLimitOrders
+ mClassMap.put(GetLimitOrders.class, List.class);
+ Gson getLimitOrdersGson = new GsonBuilder()
+ .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
+ .registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
+ .registerTypeAdapter(LimitOrder.class, new LimitOrder.LimitOrderDeserializer())
+ .create();
+ mGsonMap.put(GetLimitOrders.class, getLimitOrdersGson);
+
+ // GetAccountHistoryByOperations
+ mClassMap.put(GetAccountHistoryByOperations.class, HistoryOperationDetail.class);
+ Gson getAccountHistoryByOperationsGson = new GsonBuilder()
+ .setExclusionStrategies(new DeserializationMap.SkipAccountOptionsStrategy(), new DeserializationMap.SkipAssetOptionsStrategy())
+ .registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
+ .registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
+ .registerTypeAdapter(Extensions.class, new Extensions.ExtensionsDeserializer())
+ .registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
+ .registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer())
+ .registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
+ .create();
+ mGsonMap.put(GetAccountHistoryByOperations.class, getAccountHistoryByOperationsGson);
+
+ // GetFullAccounts
+ mClassMap.put(GetFullAccounts.class, List.class);
+ Gson getFullAccountsGson = new GsonBuilder()
+ .registerTypeAdapter(FullAccountDetails.class, new FullAccountDetails.FullAccountDeserializer())
+ .registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
+ .registerTypeAdapter(Memo.class, new Memo.MemoDeserializer())
+ .registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
+ .create();
+ mGsonMap.put(GetFullAccounts.class, getFullAccountsGson);
+ }
+
+ public Class getReceivedClass(Class _class){
+ return mClassMap.get(_class);
+ }
+
+ public Gson getGson(Class aClass) {
+ return mGsonMap.get(aClass);
+ }
+
+ /**
+ * This class is required in order to break a recursion loop when de-serializing the
+ * AccountProperties class instance.
+ */
+ public static class SkipAccountOptionsStrategy implements ExclusionStrategy {
+
+ @Override
+ public boolean shouldSkipField(FieldAttributes f) {
+ return false;
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class> clazz) {
+ return clazz == AccountOptions.class;
+ }
+ }
+
+ /**
+ * This class is required in order to break a recursion loop when de-serializing the
+ * AssetAmount instance.
+ */
+ public static class SkipAssetOptionsStrategy implements ExclusionStrategy {
+
+ @Override
+ public boolean shouldSkipField(FieldAttributes f) {
+ return false;
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class> clazz) {
+ return clazz == AssetOptions.class;
+ }
+ }
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java
new file mode 100644
index 0000000..eb9c12e
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java
@@ -0,0 +1,526 @@
+package cy.agorise.graphenej.api.android;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+import java.io.Serializable;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+import cy.agorise.graphenej.Asset;
+import cy.agorise.graphenej.AssetAmount;
+import cy.agorise.graphenej.BaseOperation;
+import cy.agorise.graphenej.LimitOrder;
+import cy.agorise.graphenej.RPC;
+import cy.agorise.graphenej.Transaction;
+import cy.agorise.graphenej.UserAccount;
+import cy.agorise.graphenej.api.ApiAccess;
+import cy.agorise.graphenej.api.ConnectionStatusUpdate;
+import cy.agorise.graphenej.api.bitshares.Nodes;
+import cy.agorise.graphenej.api.calls.ApiCallable;
+import cy.agorise.graphenej.api.calls.GetAccounts;
+import cy.agorise.graphenej.api.calls.GetFullAccounts;
+import cy.agorise.graphenej.api.calls.GetLimitOrders;
+import cy.agorise.graphenej.api.calls.GetMarketHistory;
+import cy.agorise.graphenej.api.calls.GetObjects;
+import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory;
+import cy.agorise.graphenej.api.calls.GetRequiredFees;
+import cy.agorise.graphenej.api.calls.ListAssets;
+import cy.agorise.graphenej.models.AccountProperties;
+import cy.agorise.graphenej.models.ApiCall;
+import cy.agorise.graphenej.models.Block;
+import cy.agorise.graphenej.models.BlockHeader;
+import cy.agorise.graphenej.models.BucketObject;
+import cy.agorise.graphenej.models.DynamicGlobalProperties;
+import cy.agorise.graphenej.models.FullAccountDetails;
+import cy.agorise.graphenej.models.HistoryOperationDetail;
+import cy.agorise.graphenej.models.JsonRpcNotification;
+import cy.agorise.graphenej.models.JsonRpcResponse;
+import cy.agorise.graphenej.models.OperationHistory;
+import cy.agorise.graphenej.Memo;
+import cy.agorise.graphenej.operations.CustomOperation;
+import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
+import cy.agorise.graphenej.operations.TransferOperation;
+import io.reactivex.annotations.Nullable;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.WebSocket;
+import okhttp3.WebSocketListener;
+
+/**
+ * Service in charge of maintaining a connection to the full node.
+ */
+
+public class NetworkService extends Service {
+ private final String TAG = this.getClass().getName();
+
+ private static final int NORMAL_CLOSURE_STATUS = 1000;
+
+ public static final String KEY_USERNAME = "key_username";
+
+ public static final String KEY_PASSWORD = "key_password";
+
+ public static final String KEY_REQUESTED_APIS = "key_requested_apis";
+
+ /**
+ * Constant used to pass a custom list of node URLs. This should be a simple
+ * comma separated list of URLs.
+ *
+ * For example:
+ *
+ * wss://domain1.com/ws,wss://domain2.com/ws,wss://domain3.com/ws
+ */
+ public static final String KEY_CUSTOM_NODE_URLS = "key_custom_node_urls";
+
+ private final IBinder mBinder = new LocalBinder();
+
+ private WebSocket mWebSocket;
+
+ private int mSocketIndex;
+
+ // Username and password used to connect to a specific node
+ private String mUsername;
+ private String mPassword;
+
+ private boolean isLoggedIn = false;
+
+ private String mLastCall;
+ private long mCurrentId = 0;
+
+ // Requested APIs passed to this service
+ private int mRequestedApis;
+
+ // Variable used to keep track of the currently obtained API accesses
+ private HashMap mApiIds = new HashMap();
+
+ private ArrayList mNodeUrls = new ArrayList<>();
+
+ 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())
+ .registerTypeAdapter(BaseOperation.class, new BaseOperation.OperationDeserializer())
+ .registerTypeAdapter(OperationHistory.class, new OperationHistory.OperationHistoryDeserializer())
+ .registerTypeAdapter(JsonRpcNotification.class, new JsonRpcNotification.JsonRpcNotificationDeserializer())
+ .create();
+
+ // 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 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();
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+
+ // Retrieving credentials and requested API data from the shared preferences
+ mUsername = pref.getString(NetworkService.KEY_USERNAME, "");
+ mPassword = pref.getString(NetworkService.KEY_PASSWORD, "");
+ mRequestedApis = pref.getInt(NetworkService.KEY_REQUESTED_APIS, -1);
+
+ // If the user of the library desires, a custom list of node URLs can
+ // be passed using the KEY_CUSTOM_NODE_URLS constant
+ String serializedNodeUrls = pref.getString(NetworkService.KEY_CUSTOM_NODE_URLS, "");
+
+ // Deciding whether to use an externally provided list of node URLs, or use our internal one
+ if(serializedNodeUrls.equals("")){
+ mNodeUrls.addAll(Arrays.asList(Nodes.NODE_URLS));
+ }else{
+ String[] urls = serializedNodeUrls.split(",");
+ mNodeUrls.addAll(Arrays.asList(urls));
+ }
+ connect();
+ }
+
+ private void connect(){
+ OkHttpClient client = new OkHttpClient();
+ String url = mNodeUrls.get(mSocketIndex % mNodeUrls.size());
+ Log.d(TAG,"Trying to connect with: "+url);
+ Request request = new Request.Builder().url(url).build();
+ client.newWebSocket(request, mWebSocketListener);
+ }
+
+ public long sendMessage(String message){
+ if(mWebSocket != null){
+ if(mWebSocket.send(message)){
+ Log.v(TAG,"-> " + message);
+ return mCurrentId;
+ }
+ }else{
+ throw new RuntimeException("Websocket connection has not yet been established");
+ }
+ return -1;
+ }
+
+ /**
+ * 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.
+ */
+ public long sendMessage(ApiCallable apiCallable, int requiredApi){
+ if(requiredApi != -1 && mApiIds.containsKey(requiredApi) || requiredApi == ApiAccess.API_NONE){
+ int apiId = 0;
+ if(requiredApi != ApiAccess.API_NONE)
+ apiId = mApiIds.get(requiredApi);
+ 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;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ public void onDestroy() {
+ if(mWebSocket != null)
+ mWebSocket.close(NORMAL_CLOSURE_STATUS, null);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ /**
+ * 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() {
+
+ @Override
+ public void onOpen(WebSocket webSocket, Response response) {
+ super.onOpen(webSocket, response);
+ mWebSocket = webSocket;
+
+ // Notifying all listeners about the new connection status
+ RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.CONNECTED, ApiAccess.API_NONE));
+
+ // If we're not yet logged in, we should do it now
+ if(!isLoggedIn){
+ ArrayList 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());
+ }
+ }
+
+ @Override
+ public void onMessage(WebSocket webSocket, String text) {
+ super.onMessage(webSocket, text);
+ Log.v(TAG,"<- "+text);
+ JsonRpcNotification notification = gson.fromJson(text, JsonRpcNotification.class);
+
+ 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>() {}.getType();
+ JsonRpcResponse 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>() {}.getType();
+ JsonRpcResponse 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>() {}.getType();
+ JsonRpcResponse 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;
+ }
+ }
+ }
+ 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);
+ }
+ // Properly de-serialize all other fields and broadcasts to the event bus
+ handleJsonRpcResponse(response, text);
+ }
+ }
+
+ /**
+ * 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){
+ JsonRpcResponse parsedResponse = null;
+
+ Class requestClass = mRequestClassMap.get(response.id);
+ if(requestClass != null){
+ // Removing the class entry in the map
+ mRequestClassMap.remove(response.id);
+
+ // Obtaining the response payload class
+ Class responsePayloadClass = mDeserializationMap.getReceivedClass(requestClass);
+ Gson gson = mDeserializationMap.getGson(requestClass);
+ if(responsePayloadClass == Block.class){
+ // If the response payload is a Block instance, we proceed to de-serialize it
+ Type GetBlockResponse = new TypeToken>() {}.getType();
+ parsedResponse = gson.fromJson(text, GetBlockResponse);
+ }else if(responsePayloadClass == BlockHeader.class){
+ // If the response payload is a BlockHeader instance, we proceed to de-serialize it
+ Type GetBlockHeaderResponse = new TypeToken>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetBlockHeaderResponse);
+ } else if(responsePayloadClass == AccountProperties.class){
+ Type GetAccountByNameResponse = new TypeToken>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetAccountByNameResponse);
+ } else if(responsePayloadClass == HistoryOperationDetail.class){
+ Type GetAccountHistoryByOperationsResponse = new TypeToken>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetAccountHistoryByOperationsResponse);
+ }else if(responsePayloadClass == List.class){
+ // 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>
+ // so we proceed with that
+ Type GetAccountsResponse = new TypeToken>>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetAccountsResponse);
+ }else if(requestClass == GetRequiredFees.class){
+ Type GetRequiredFeesResponse = new TypeToken>>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetRequiredFeesResponse);
+ }else if(requestClass == GetRelativeAccountHistory.class){
+ Type RelativeAccountHistoryResponse = new TypeToken>>(){}.getType();
+ parsedResponse = gson.fromJson(text, RelativeAccountHistoryResponse);
+ }else if(requestClass == GetMarketHistory.class){
+ Type GetMarketHistoryResponse = new TypeToken>>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetMarketHistoryResponse);
+ }else if(requestClass == GetObjects.class){
+ parsedResponse = handleGetObject(text);
+ }else if(requestClass == ListAssets.class){
+ Type LisAssetsResponse = new TypeToken>>(){}.getType();
+ parsedResponse = gson.fromJson(text, LisAssetsResponse);
+ }else if(requestClass == GetLimitOrders.class){
+ Type GetLimitOrdersResponse = new TypeToken>>() {}.getType();
+ parsedResponse = gson.fromJson(text, GetLimitOrdersResponse);
+ } else if (requestClass == GetFullAccounts.class) {
+ Type GetFullAccountsResponse = new TypeToken>>(){}.getType();
+ parsedResponse = gson.fromJson(text, GetFullAccountsResponse);
+ } else {
+ 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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){
+ //TODO: Implement a proper de-serialization logic
+ return null;
+ }
+
+ /**
+ * 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
+ */
+ 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());
+ }
+ }
+
+ @Override
+ public void onClosed(WebSocket webSocket, int code, String reason) {
+ super.onClosed(webSocket, code, reason);
+ Log.d(TAG,"onClosed");
+ RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
+
+ isLoggedIn = false;
+ }
+
+ @Override
+ public void onFailure(WebSocket webSocket, Throwable t, Response response) {
+ super.onFailure(webSocket, t, response);
+ Log.e(TAG,"onFailure. Exception: "+t.getClass().getName()+", Msg: "+t.getMessage());
+ // Logging error stack trace
+ for(StackTraceElement element : t.getStackTrace()){
+ Log.e(TAG,String.format("%s#%s:%s", element.getClassName(), element.getMethodName(), element.getLineNumber()));
+ }
+ // Registering current status
+ isLoggedIn = false;
+ mCurrentId = 0;
+ mApiIds.clear();
+
+ // If there is a response, we print it
+ if(response != null){
+ Log.e(TAG,"Response: "+response.message());
+ }
+
+ RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED, ApiAccess.API_NONE));
+ mSocketIndex++;
+
+ if(mSocketIndex > mNodeUrls.size() * 3){
+ Log.e(TAG,"Giving up on connections");
+ stopSelf();
+ }else{
+ connect();
+ }
+ }
+ };
+
+ /**
+ * 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;
+ }
+
+ public ArrayList getNodeUrls() {
+ return mNodeUrls;
+ }
+
+ public void setNodeUrls(ArrayList mNodeUrls) {
+ this.mNodeUrls = mNodeUrls;
+ }
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java
new file mode 100644
index 0000000..2f31c0d
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkServiceManager.java
@@ -0,0 +1,117 @@
+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;
+
+/**
+ * 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 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 mContextReference;
+
+ // In case we want to interact directly with the service
+ private NetworkService mService;
+
+ /**
+ * 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));
+ }
+ };
+
+ public NetworkServiceManager(Context context){
+ mContextReference = new WeakReference(context);
+ }
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle bundle) {
+ if(mService == null){
+ // Starting a NetworkService instance
+ Context context = mContextReference.get();
+ Intent intent = new Intent(context, NetworkService.class);
+ context.startService(intent);
+ }
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ mHandler.removeCallbacks(mDisconnectRunnable);
+ if(mService == null){
+ Context context = mContextReference.get();
+ Intent intent = new Intent(context, NetworkService.class);
+ context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {}
+
+ @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;
+ mService = binder.getService();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {}
+ };
+}
diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/android/RxBus.java b/graphenej/src/main/java/cy/agorise/graphenej/api/android/RxBus.java
new file mode 100644
index 0000000..f246f2d
--- /dev/null
+++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/RxBus.java
@@ -0,0 +1,36 @@
+package cy.agorise.graphenej.api.android;
+
+import com.jakewharton.rxrelay2.PublishRelay;
+import com.jakewharton.rxrelay2.Relay;
+
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+
+/**
+ * Explained here: https://blog.kaush.co/2014/12/24/implementing-an-event-bus-with-rxjava-rxbus/
+ */
+public class RxBus {
+
+ private static RxBus rxBus;
+
+ public static final RxBus getBusInstance(){
+ if(rxBus == null){
+ rxBus = new RxBus();
+ }
+ return rxBus;
+ }
+
+ private final Relay