diff --git a/build.gradle b/build.gradle index 676dacc..178a870 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,22 @@ subprojects { mavenCentral() } } +allprojects { + repositories { + mavenCentral() + jcenter() + maven { + url "https://maven.google.com" + } + } +} buildscript { repositories { mavenCentral() + maven { + url 'https://maven.google.com/' + name 'Google' + } } dependencies { classpath 'com.android.tools.build:gradle:2.3.0' diff --git a/graphenej/build.gradle b/graphenej/build.gradle index 8d86c09..85c6653 100644 --- a/graphenej/build.gradle +++ b/graphenej/build.gradle @@ -11,6 +11,12 @@ dependencies { 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' } 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..b58b1bd --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/ConnectionStatusUpdate.java @@ -0,0 +1,24 @@ +package cy.agorise.graphenej.api; + +/** + * Class used to send connection status updates + */ + +public class ConnectionStatusUpdate { + public final static String CONNECTED = "Connected"; + public final static String DISCONNECTED = "Disconnected"; + + private String connectionStatus; + + public ConnectionStatusUpdate(String status){ + this.connectionStatus = status; + } + + public String getConnectionStatus() { + return connectionStatus; + } + + public void setConnectionStatus(String connectionStatus) { + this.connectionStatus = connectionStatus; + } +} 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 _bus = PublishRelay.create().toSerialized(); + + public void send(Object o) { + _bus.accept(o); + } + + public Flowable asFlowable() { + return _bus.toFlowable(BackpressureStrategy.LATEST); + } + + public boolean hasObservers() { + return _bus.hasObservers(); + } +} \ No newline at end of file diff --git a/graphenej/src/main/java/cy/agorise/graphenej/api/bitshares/Nodes.java b/graphenej/src/main/java/cy/agorise/graphenej/api/bitshares/Nodes.java new file mode 100644 index 0000000..8fdc8d2 --- /dev/null +++ b/graphenej/src/main/java/cy/agorise/graphenej/api/bitshares/Nodes.java @@ -0,0 +1,15 @@ +package cy.agorise.graphenej.api.bitshares; + +/** + * Known public nodes + */ + +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 + }; +} diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..6364279 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 26 + buildToolsVersion "27.0.0" + + + defaultConfig { + applicationId "com.luminiasoft.labs.sample" + minSdkVersion 14 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + multiDexEnabled true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':graphenej') + compile 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile 'com.jakewharton.rxbinding2:rxbinding:2.1.1' + compile 'com.jakewharton:butterknife:8.8.1' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' + testCompile 'junit:junit:4.12' + androidTestCompile('com.android.support.test.espresso:espresso-core:3.0.1', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:multidex:1.0.1' +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sample/src/androidTest/java/com/luminiasoft/labs/sample/ExampleInstrumentedTest.java b/sample/src/androidTest/java/com/luminiasoft/labs/sample/ExampleInstrumentedTest.java new file mode 100644 index 0000000..8fa7174 --- /dev/null +++ b/sample/src/androidTest/java/com/luminiasoft/labs/sample/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.luminiasoft.labs.sample; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.luminiasoft.labs.sample", appContext.getPackageName()); + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8762327 --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java b/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java new file mode 100644 index 0000000..11b831f --- /dev/null +++ b/sample/src/main/java/com/luminiasoft/labs/sample/MainActivity.java @@ -0,0 +1,100 @@ +package com.luminiasoft.labs.sample; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import cy.agorise.graphenej.api.ConnectionStatusUpdate; +import cy.agorise.graphenej.api.android.RxBus; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.functions.Consumer; + +public class MainActivity extends AppCompatActivity { + private final String TAG = this.getClass().getName(); + + @BindView(R.id.connection_status) + TextView mConnectionStatus; + + @BindView(R.id.response) + TextView mResponse; + + // 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_main); + ButterKnife.bind(this); + + RxBus.getBusInstance() + .asFlowable() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Consumer() { + + @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()); + } + } + }); + } + + @OnClick(R.id.send_message) + public void onSendMesage(View v){ + mService.sendMessage("Sample message"); + } + + @OnClick(R.id.next_activity) + public void onNextActivity(View v){ + Intent intent = new Intent(this, SecondActivity.class); + startActivity(intent); + } + + @Override + protected void onStart() { + super.onStart(); + // Bind to LocalService + Intent intent = new Intent(this, NetworkService.class); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onPause() { + super.onPause(); + unbindService(mConnection); + } + + /** 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"); + } + }; +} diff --git a/sample/src/main/java/com/luminiasoft/labs/sample/NetworkService.java b/sample/src/main/java/com/luminiasoft/labs/sample/NetworkService.java new file mode 100644 index 0000000..15fa515 --- /dev/null +++ b/sample/src/main/java/com/luminiasoft/labs/sample/NetworkService.java @@ -0,0 +1,119 @@ +package com.luminiasoft.labs.sample; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.util.Log; + +import cy.agorise.graphenej.api.ConnectionStatusUpdate; +import cy.agorise.graphenej.api.android.RxBus; +import cy.agorise.graphenej.api.bitshares.Nodes; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +/** + * Service in charge of mantaining 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; + + private final IBinder mBinder = new LocalBinder(); + + private WebSocket mWebSocket; + + private int mSocketIndex; + + private WebSocketListener mWebSocketListener = new WebSocketListener() { + + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + mWebSocket = webSocket; + RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.CONNECTED)); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + super.onMessage(webSocket, text); + Log.d(TAG,"onMessage. text: "+text); + RxBus.getBusInstance().send(text); + } + + @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)); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + Log.d(TAG,"onFailure. Msg: "+t.getMessage()); + RxBus.getBusInstance().send(new ConnectionStatusUpdate(ConnectionStatusUpdate.DISCONNECTED)); + mSocketIndex++; + connect(); + } + }; + + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG,"onCreate"); + connect(); + } + + private void connect(){ + OkHttpClient client = new OkHttpClient(); + String url = Nodes.NODE_URLS[mSocketIndex % Nodes.NODE_URLS.length]; + Request request = new Request.Builder().url(url).build(); + client.newWebSocket(request, mWebSocketListener); + } + + public void sendMessage(String message){ + if(mWebSocket.send(message)){ + Log.d(TAG,"Message enqueued"); + }else{ + Log.w(TAG,"Message not enqueued"); + } + } + + + @Override + public void onDestroy() { + Log.d(TAG,"onDestroy"); + mWebSocket.close(NORMAL_CLOSURE_STATUS, null); + } + + @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; + } + + /** + * 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; + } + } +} diff --git a/sample/src/main/java/com/luminiasoft/labs/sample/SampleApplication.java b/sample/src/main/java/com/luminiasoft/labs/sample/SampleApplication.java new file mode 100644 index 0000000..2459212 --- /dev/null +++ b/sample/src/main/java/com/luminiasoft/labs/sample/SampleApplication.java @@ -0,0 +1,89 @@ +package com.luminiasoft.labs.sample; + +import android.app.Activity; +import android.app.Application; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; + +/** + * Sample application class + */ + +public class SampleApplication extends Application implements Application.ActivityLifecycleCallbacks { + private final String TAG = this.getClass().getName(); + + /** + * Handler instance used to schedule tasks back to the main thread + */ + private Handler mHandler = new Handler(); + + /** + * 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; + + /** + * 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() { + Log.d(TAG,"Runing stopService"); + stopService(new Intent(getApplicationContext(), NetworkService.class)); + } + }; + + @Override + public void onCreate() { + super.onCreate(); + Intent intent = new Intent(this, NetworkService.class); + startService(intent); + + /* + * Registering this class as a listener to all activity's callback cycle events, in order to + * better estimate when the user has left the app and it is safe to disconnect the websocket connection + */ + registerActivityLifecycleCallbacks(this); + } + + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { + + } + + @Override + public void onActivityStarted(Activity activity) { + mHandler.removeCallbacks(mDisconnectRunnable); + } + + @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) { + + } +} diff --git a/sample/src/main/java/com/luminiasoft/labs/sample/SecondActivity.java b/sample/src/main/java/com/luminiasoft/labs/sample/SecondActivity.java new file mode 100644 index 0000000..e16489b --- /dev/null +++ b/sample/src/main/java/com/luminiasoft/labs/sample/SecondActivity.java @@ -0,0 +1,13 @@ +package com.luminiasoft.labs.sample; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +public class SecondActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_second); + } +} diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..6a55457 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,47 @@ + + +