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..4cd85e8 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/DeserializationMap.java @@ -0,0 +1,86 @@ +package cy.agorise.graphenej.api.android; + +import com.google.common.reflect.TypeToken; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; + +import cy.agorise.graphenej.AccountOptions; +import cy.agorise.graphenej.AssetAmount; +import cy.agorise.graphenej.Authority; +import cy.agorise.graphenej.Transaction; +import cy.agorise.graphenej.api.calls.GetAccounts; +import cy.agorise.graphenej.api.calls.GetBlock; +import cy.agorise.graphenej.models.AccountProperties; +import cy.agorise.graphenej.models.Block; +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(){ + // 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 + Type GetAccountResponse = new TypeToken>() {}.getType(); + mClassMap.put(GetAccounts.class, GetAccountResponse.getClass()); + mClassMap.put(GetAccounts.class, List.class); + Gson getAccountsGson = new GsonBuilder() + .setExclusionStrategies(new GetAccountsExclusionStrategy()) + .registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer()) + .registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer()) + .create(); + mGsonMap.put(GetAccounts.class, getAccountsGson); + } + + 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. + */ + private class GetAccountsExclusionStrategy implements ExclusionStrategy { + + @Override + public boolean shouldSkipField(FieldAttributes f) { + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return clazz == AccountOptions.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 index 9e0d677..bccfa8e 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/android/NetworkService.java @@ -15,13 +15,17 @@ import java.io.Serializable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import cy.agorise.graphenej.RPC; 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.models.AccountProperties; import cy.agorise.graphenej.models.ApiCall; +import cy.agorise.graphenej.models.Block; import cy.agorise.graphenej.models.JsonRpcResponse; import io.reactivex.annotations.Nullable; import okhttp3.OkHttpClient; @@ -68,7 +72,7 @@ public class NetworkService extends Service { private boolean isLoggedIn = false; private String mLastCall; - private int mCurrentId = 0; + private long mCurrentId = 0; // Requested APIs passed to this service private int mRequestedApis; @@ -80,6 +84,16 @@ public class NetworkService extends Service { private Gson gson = new Gson(); + // 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(); @@ -117,7 +131,7 @@ public class NetworkService extends Service { client.newWebSocket(request, mWebSocketListener); } - public int sendMessage(String message){ + public long sendMessage(String message){ if(mWebSocket != null){ if(mWebSocket.send(message)){ Log.v(TAG,"-> " + message); @@ -128,12 +142,13 @@ public class NetworkService extends Service { return mCurrentId; } - public int sendMessage(ApiCallable apiCallable, int requiredApi){ + public long sendMessage(ApiCallable apiCallable, int requiredApi){ int apiId = 0; if(requiredApi != -1 && mApiIds.containsKey(requiredApi)){ apiId = mApiIds.get(requiredApi); } ApiCall call = apiCallable.toApiCall(apiId, ++mCurrentId); + mRequestClassMap.put(mCurrentId, apiCallable.getClass()); if(mWebSocket.send(call.toJsonString())){ Log.v(TAG,"-> "+call.toJsonString()); } @@ -156,14 +171,12 @@ public class NetworkService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { - Log.d(TAG,"onStartCommand"); return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { - Log.d(TAG,"onBind"); return mBinder; } @@ -183,7 +196,6 @@ public class NetworkService extends Service { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); - Log.d(TAG,"onOpen"); mWebSocket = webSocket; // Notifying all listeners about the new connection status @@ -247,7 +259,46 @@ public class NetworkService extends Service { Log.w(TAG,"Error.Msg: "+response.error.message); } - RxBus.getBusInstance().send(response); + JsonRpcResponse parsedResponse = null; + + Class requestClass = mRequestClassMap.get(response.id); + if(requestClass != null){ + // Removing the class entry in the map + mRequestClassMap.remove(mCurrentId); + + // 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 simple Block instance, we proceed to de-serialize it + Type GetBlockResponse = new TypeToken>() {}.getType(); + JsonRpcResponse blockResponse = (JsonRpcResponse) gson.fromJson(text, GetBlockResponse); + parsedResponse = blockResponse; + }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(); + JsonRpcResponse> accountResponse = (JsonRpcResponse) gson.fromJson(text, GetAccountsResponse); + parsedResponse = accountResponse; + }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 void checkNextRequestedApiAccess(){ diff --git a/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java b/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java index f4ac073..c3f3959 100644 --- a/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java +++ b/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java @@ -45,7 +45,7 @@ public class MainActivity extends AppCompatActivity { private Disposable mDisposable; - private HashMap responseMap = new HashMap<>(); + private HashMap responseMap = new HashMap<>(); private final int GET_BLOCK_RESPONSE = 0; private final int GET_ACCOUNTS_RESPONSE = 1; @@ -66,6 +66,7 @@ public class MainActivity extends AppCompatActivity { if(message instanceof String){ Log.d(TAG,"Got text message: "+(message)); mResponse.setText(mResponse.getText() + ((String) message) + "\n"); + handleTextMessage((String) message); }else if(message instanceof ConnectionStatusUpdate){ Log.d(TAG,"Got connection update. Status: "+((ConnectionStatusUpdate)message).getConnectionStatus()); mConnectionStatus.setText(((ConnectionStatusUpdate) message).getConnectionStatus()); @@ -76,6 +77,10 @@ public class MainActivity extends AppCompatActivity { }); } + private void handleTextMessage(String text){ + + } + /** * Internal method that will decide what to do with each JSON-RPC response * @@ -106,7 +111,7 @@ public class MainActivity extends AppCompatActivity { @OnClick(R.id.call_get_block) public void onGetBlock(View v){ GetBlock getBlock = new GetBlock(1000000); - int id = mService.sendMessage(getBlock, GetBlock.REQUIRED_API); + long id = mService.sendMessage(getBlock, GetBlock.REQUIRED_API); // Registering the used sequence id responseMap.put(id, GET_BLOCK_RESPONSE); } @@ -114,7 +119,7 @@ public class MainActivity extends AppCompatActivity { @OnClick(R.id.call_get_accounts) public void onGetAccounts(View v){ GetAccounts getAccounts = new GetAccounts(new UserAccount("1.2.1000")); - int id = mService.sendMessage(getAccounts, GetBlock.REQUIRED_API); + long id = mService.sendMessage(getAccounts, GetBlock.REQUIRED_API); // Registering the used sequence id responseMap.put(id, GET_ACCOUNTS_RESPONSE); }