Handling only credentials and API access messages locally, deferring all other messages to the bus

develop
Nelson R. Perez 2018-04-04 21:07:09 -05:00
parent 436fb71114
commit 1e59d8ec40
14 changed files with 363 additions and 57 deletions

View File

@ -5,21 +5,6 @@ apply plugin: 'com.android.library'
apply from: 'maven-push.gradle'
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile 'com.neovisionaries:nv-websocket-client:1.30'
compile 'org.bitcoinj:bitcoinj-core:0.14.3'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
compile group: "org.tukaani", name: "xz", version: "1.6"
// Rx dependencies
compile 'io.reactivex.rxjava2:rxandroid:2.0.2'
compile 'io.reactivex.rxjava2:rxjava:2.1.9'
compile 'com.jakewharton.rxrelay2:rxrelay:2.0.0'
compile 'com.squareup.okhttp3:okhttp:3.5.0'
}
android {
compileSdkVersion 24
buildToolsVersion "25.0.0"
@ -37,4 +22,18 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile 'com.neovisionaries:nv-websocket-client:1.30'
compile 'org.bitcoinj:bitcoinj-core:0.14.3'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.0'
compile group: "org.tukaani", name: "xz", version: "1.6"
// Rx dependencies
compile 'io.reactivex.rxjava2:rxandroid:2.0.2'
compile 'io.reactivex.rxjava2:rxjava:2.1.9'
compile 'com.jakewharton.rxrelay2:rxrelay:2.0.0'
compile 'com.squareup.okhttp3:okhttp:3.5.0'
}

View File

@ -0,0 +1,11 @@
package cy.agorise.graphenej.api;
/**
* Class used to list all currently supported API accesses
*/
public class ApiAccess {
public static final int API_DATABASE = 0x01;
public static final int API_HISTORY = 0x02;
public static final int API_NETWORK_BROADCAST = 0x04;
}

View File

@ -7,7 +7,6 @@ package cy.agorise.graphenej.api.bitshares;
public class Nodes {
public static final String[] NODE_URLS = {
"wss://bitshares.nus/ws",
"ws://echo.websocket.org",
"wss://dexnode.net/ws", // Dallas, USA
"wss://bitshares.crypto.fans/ws", // Munich, Germany
"wss://bitshares.openledger.info/ws", // Openledger node

View File

@ -0,0 +1,17 @@
package cy.agorise.graphenej.api.calls;
import cy.agorise.graphenej.models.ApiCall;
/**
* Interface to be implemented by all classes that will produce an ApiCall object instance
* as a result.
*/
public interface ApiCallable {
/**
*
* @return An instance of the {@link ApiCall} class
*/
ApiCall toApiCall(int apiId, long sequenceId);
}

View File

@ -0,0 +1,27 @@
package cy.agorise.graphenej.api.calls;
import java.io.Serializable;
import java.util.ArrayList;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.models.ApiCall;
/**
* Wrapper aroung the "get_block" API call.
*/
public class GetBlock implements ApiCallable {
private long blockNumber;
public GetBlock(int blockNum){
this.blockNumber = blockNum;
}
public ApiCall toApiCall(int apiId, long sequenceId){
ArrayList<Serializable> params = new ArrayList<>();
String blockNum = String.format("%d", this.blockNumber);
params.add(blockNum);
return new ApiCall(apiId, RPC.CALL_GET_BLOCK, params, RPC.VERSION, sequenceId);
}
}

View File

@ -66,33 +66,35 @@ public class ApiCall implements JsonSerializable {
paramsArray.add(this.methodToCall);
JsonArray methodParams = new JsonArray();
for(int i = 0; i < this.params.size(); i++){
if(this.params.get(i) instanceof JsonSerializable) {
// Sometimes the parameters are objects
methodParams.add(((JsonSerializable) this.params.get(i)).toJsonObject());
}else if (Number.class.isInstance(this.params.get(i))){
// Other times they are numbers
methodParams.add( (Number) this.params.get(i));
}else if(this.params.get(i) instanceof String || this.params.get(i) == null){
// Other times they are plain strings
methodParams.add((String) this.params.get(i));
}else if(this.params.get(i) instanceof ArrayList) {
// Other times it might be an array
JsonArray array = new JsonArray();
ArrayList<Serializable> listArgument = (ArrayList<Serializable>) this.params.get(i);
for (int l = 0; l < listArgument.size(); l++) {
Serializable element = listArgument.get(l);
if (element instanceof JsonSerializable)
array.add(((JsonSerializable) element).toJsonObject());
else if (element instanceof String) {
array.add((String) element);
if(this.params != null){
for(int i = 0; i < this.params.size(); i++){
if(this.params.get(i) instanceof JsonSerializable) {
// Sometimes the parameters are objects
methodParams.add(((JsonSerializable) this.params.get(i)).toJsonObject());
}else if (Number.class.isInstance(this.params.get(i))){
// Other times they are numbers
methodParams.add( (Number) this.params.get(i));
}else if(this.params.get(i) instanceof String || this.params.get(i) == null){
// Other times they are plain strings
methodParams.add((String) this.params.get(i));
}else if(this.params.get(i) instanceof ArrayList) {
// Other times it might be an array
JsonArray array = new JsonArray();
ArrayList<Serializable> listArgument = (ArrayList<Serializable>) this.params.get(i);
for (int l = 0; l < listArgument.size(); l++) {
Serializable element = listArgument.get(l);
if (element instanceof JsonSerializable)
array.add(((JsonSerializable) element).toJsonObject());
else if (element instanceof String) {
array.add((String) element);
}
}
methodParams.add(array);
}else if(this.params.get(i) instanceof Boolean){
methodParams.add((boolean) this.params.get(i));
}else{
System.out.println("Skipping parameter of type: "+this.params.get(i).getClass());
}
methodParams.add(array);
}else if(this.params.get(i) instanceof Boolean){
methodParams.add((boolean) this.params.get(i));
}else{
System.out.println("Skipping parameter of type: "+this.params.get(i).getClass());
}
}
paramsArray.add(methodParams);

View File

@ -1,11 +1,13 @@
package cy.agorise.graphenej.models;
/**
* Created by nelson on 11/12/16.
* Base response class
* @deprecated Use {@link JsonRpcResponse} instead
*/
public class BaseResponse {
public long id;
public Error error;
public Object result;
public static class Error {
public ErrorData data;

View File

@ -0,0 +1,31 @@
package cy.agorise.graphenej.models;
/**
* Used to represent a JSON-RPC response object
*/
public class JsonRpcResponse<T> {
public long id;
public Error error;
public T result;
public static class Error {
public ErrorData data;
public int code;
public String message;
public Error(String message){
this.message = message;
}
}
public static class ErrorData {
public int code;
public String name;
public String message;
//TODO: Include stack data
public ErrorData(String message){
this.message = message;
}
}
}

View File

@ -2,6 +2,7 @@ package cy.agorise.graphenej.models;
/**
* Generic witness response
* @deprecated Use {@link JsonRpcResponse} instead
*/
public class WitnessResponse<T> extends BaseResponse{
public static final String KEY_ID = "id";

View File

@ -22,7 +22,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {

View File

@ -6,16 +6,22 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.google.gson.Gson;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.ConnectionStatusUpdate;
import cy.agorise.graphenej.api.android.RxBus;
import cy.agorise.graphenej.api.calls.GetBlock;
import cy.agorise.graphenej.models.JsonRpcResponse;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
@ -31,25 +37,39 @@ public class MainActivity extends AppCompatActivity {
// In case we want to interact directly with the service
private NetworkService mService;
private Gson gson = new Gson();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
// Specifying some important information regarding the connection, such as the
// credentials and the requested API accesses
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putString(NetworkService.KEY_USERNAME, "nelson")
.putString(NetworkService.KEY_PASSWORD, "secret")
.putInt(NetworkService.KEY_REQUESTED_APIS, requestedApis)
.apply();
RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
if(o instanceof String){
Log.d(TAG,"Got message");
mResponse.setText(mResponse.getText() + ((String)o) + "\n");
}else if(o instanceof ConnectionStatusUpdate){
Log.d(TAG,"Got connection update");
mConnectionStatus.setText(((ConnectionStatusUpdate)o).getConnectionStatus());
public void accept(Object message) throws Exception {
if(message instanceof String){
Log.d(TAG,"Got text message: "+(message));
mResponse.setText(mResponse.getText() + ((String) message) + "\n");
}else if(message instanceof ConnectionStatusUpdate){
Log.d(TAG,"Got connection update. Status: "+((ConnectionStatusUpdate)message).getConnectionStatus());
mConnectionStatus.setText(((ConnectionStatusUpdate) message).getConnectionStatus());
}else if(message instanceof JsonRpcResponse){
mResponse.setText(mResponse.getText() + gson.toJson(message, JsonRpcResponse.class) + "\n");
}
}
});
@ -57,7 +77,8 @@ public class MainActivity extends AppCompatActivity {
@OnClick(R.id.send_message)
public void onSendMesage(View v){
mService.sendMessage("Sample message");
GetBlock getBlock = new GetBlock(1000000);
mService.sendMessage(getBlock);
}
@OnClick(R.id.next_activity)
@ -71,6 +92,8 @@ public class MainActivity extends AppCompatActivity {
super.onStart();
// Bind to LocalService
Intent intent = new Intent(this, NetworkService.class);
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
intent.putExtra(NetworkService.KEY_REQUESTED_APIS, requestedApis);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}

View File

@ -2,14 +2,29 @@ package com.luminiasoft.labs.sample;
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.support.annotation.Nullable;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.ConnectionStatusUpdate;
import cy.agorise.graphenej.api.android.RxBus;
import cy.agorise.graphenej.api.bitshares.Nodes;
import cy.agorise.graphenej.api.calls.ApiCallable;
import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.JsonRpcResponse;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
@ -25,12 +40,35 @@ public class NetworkService extends Service {
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";
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 int mCurrentId = 0;
// Requested APIs passed to this service
private int mRequestedApis;
// Variable used to keep track of the currently obtained API accesses
private HashMap<Integer, Integer> mApiIds = new HashMap();
private Gson gson = new Gson();
private WebSocketListener mWebSocketListener = new WebSocketListener() {
@Override
@ -38,13 +76,92 @@ public class NetworkService extends Service {
super.onOpen(webSocket, response);
mWebSocket = webSocket;
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.CONNECTED));
if(!isLoggedIn){
Log.d(TAG,"About to send login request");
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());
}else{
Log.d(TAG,"Already logged in");
}
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
Log.d(TAG,"onMessage. text: "+text);
RxBus.getBusInstance().send(text);
Log.v(TAG,"< "+text);
JsonRpcResponse<?> response = gson.fromJson(text, JsonRpcResponse.class);
// We will only handle messages that relate to the login and API accesses here.
if(response.result != null){
if(mLastCall == RPC.CALL_LOGIN){
isLoggedIn = true;
checkNextRequestedApiAccess();
}else if(mLastCall == 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);
checkNextRequestedApiAccess();
}else if(mLastCall == RPC.CALL_HISTORY){
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>(){}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Storing the "history" api id
mApiIds.put(ApiAccess.API_HISTORY, apiIdResponse.result);
checkNextRequestedApiAccess();
}else if(mLastCall == RPC.CALL_NETWORK_BROADCAST){
// Deserializing integer response
Type IntegerJsonResponse = new TypeToken<JsonRpcResponse<Integer>>(){}.getType();
JsonRpcResponse<Integer> apiIdResponse = gson.fromJson(text, IntegerJsonResponse);
// Storing the "network_broadcast" api access
mApiIds.put(ApiAccess.API_NETWORK_BROADCAST, apiIdResponse.result);
// All calls have been handled at this point
mLastCall = "";
}else{
Log.d(TAG,"New unhandled message");
}
}else{
Log.w(TAG,"Error.Msg: "+response.error.message);
}
RxBus.getBusInstance().send(response);
}
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
@ -52,12 +169,21 @@ public class NetworkService extends Service {
super.onClosed(webSocket, code, reason);
Log.d(TAG,"onClosed");
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED));
isLoggedIn = false;
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
Log.d(TAG,"onFailure. Msg: "+t.getMessage());
Log.e(TAG,"onFailure. Msg: "+t.getMessage());
isLoggedIn = false;
if(response != null){
Log.e(TAG,"Response: "+response.message());
}
for(StackTraceElement element : t.getStackTrace()){
Log.v(TAG,String.format("%s#%s:%d", element.getClassName(), element.getMethodName(), element.getLineNumber()));
}
RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED));
mSocketIndex++;
connect();
@ -68,24 +194,42 @@ public class NetworkService extends Service {
public void onCreate() {
super.onCreate();
Log.d(TAG,"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);
connect();
}
private void connect(){
OkHttpClient client = new OkHttpClient();
String url = Nodes.NODE_URLS[mSocketIndex % Nodes.NODE_URLS.length];
Log.d(TAG,"Trying to connect with: "+url);
Request request = new Request.Builder().url(url).build();
client.newWebSocket(request, mWebSocketListener);
}
public void sendMessage(String message){
public int sendMessage(String message){
if(mWebSocket.send(message)){
Log.d(TAG,"Message enqueued");
Log.v(TAG,"> " + message);
}else{
Log.w(TAG,"Message not enqueued");
}
return mCurrentId;
}
public int sendMessage(ApiCallable apiCallable){
ApiCall call = apiCallable.toApiCall(mApiIds.get(ApiAccess.API_DATABASE), mCurrentId);
if(mWebSocket.send(call.toJsonString())){
Log.v(TAG,"> "+call.toJsonString());
}else{
Log.w(TAG,"Message not enqueued");
}
return mCurrentId;
}
@Override
public void onDestroy() {

View File

@ -7,6 +7,8 @@ import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import cy.agorise.graphenej.api.ApiAccess;
/**
* Sample application class
*/
@ -43,6 +45,8 @@ public class SampleApplication extends Application implements Application.Activi
public void onCreate() {
super.onCreate();
Intent intent = new Intent(this, NetworkService.class);
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
intent.putExtra(NetworkService.KEY_REQUESTED_APIS, requestedApis);
startService(intent);
/*

View File

@ -1,13 +1,60 @@
package com.luminiasoft.labs.sample;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import cy.agorise.graphenej.api.ApiAccess;
public class SecondActivity extends AppCompatActivity {
private final String TAG = this.getClass().getName();
// In case we want to interact directly with the service
private NetworkService mService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
}
@Override
protected void onStart() {
super.onStart();
// Bind to LocalService
Intent intent = new Intent(this, NetworkService.class);
int requestedApis = ApiAccess.API_DATABASE | ApiAccess.API_HISTORY | ApiAccess.API_NETWORK_BROADCAST;
intent.putExtra(NetworkService.KEY_REQUESTED_APIS, requestedApis);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onPause() {
super.onPause();
unbindService(mConnection);
}
/** Defines callbacks for backend binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
Log.d(TAG,"onServiceConnected");
// We've bound to LocalService, cast the IBinder and get LocalService instance
NetworkService.LocalBinder binder = (NetworkService.LocalBinder) service;
mService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG,"onServiceDisconnected");
}
};
}