From 21311ea5a3fe6cec1364d42766f3e24c995d28ea Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Thu, 8 Nov 2018 13:01:09 -0600 Subject: [PATCH] Create RemoveNodeActivity in the sample project that shows an updated list of nodes sorted by latency. This whole activity while be used to add and test the functionallity of removing a node from the nodes list when the app that uses graphenej decides so, and then reconnects to the next best node. --- sample/src/main/AndroidManifest.xml | 7 +- .../cy/agorise/labs/sample/CallsActivity.java | 7 +- .../labs/sample/RemoveNodeActivity.java | 274 ++++++++++++++++++ sample/src/main/res/drawable/ic_connected.xml | 5 + .../main/res/layout/activity_remove_node.xml | 25 ++ sample/src/main/res/layout/item_node.xml | 30 ++ 6 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 sample/src/main/java/cy/agorise/labs/sample/RemoveNodeActivity.java create mode 100644 sample/src/main/res/drawable/ic_connected.xml create mode 100644 sample/src/main/res/layout/activity_remove_node.xml create mode 100644 sample/src/main/res/layout/item_node.xml diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index de61cce..62c9b15 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -11,7 +12,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> @@ -19,7 +21,8 @@ - + + \ No newline at end of file diff --git a/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java index f2a9638..534bd59 100644 --- a/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java +++ b/sample/src/main/java/cy/agorise/labs/sample/CallsActivity.java @@ -17,6 +17,8 @@ import cy.agorise.graphenej.RPC; public class CallsActivity extends AppCompatActivity { + private static final String REMOVE_CURRENT_NODE = "remove_current_node"; + @BindView(R.id.call_list) RecyclerView mRecyclerView; @@ -50,7 +52,8 @@ public class CallsActivity extends AppCompatActivity { RPC.CALL_SET_SUBSCRIBE_CALLBACK, RPC.CALL_GET_DYNAMIC_GLOBAL_PROPERTIES, RPC.CALL_GET_KEY_REFERENCES, - RPC.CALL_GET_ACCOUNT_BALANCES + RPC.CALL_GET_ACCOUNT_BALANCES, + REMOVE_CURRENT_NODE }; @NonNull @@ -71,6 +74,8 @@ public class CallsActivity extends AppCompatActivity { Intent intent; if(selectedCall.equals(RPC.CALL_SET_SUBSCRIBE_CALLBACK)){ intent = new Intent(CallsActivity.this, SubscriptionActivity.class); + } else if (selectedCall.equals(REMOVE_CURRENT_NODE)){ + intent = new Intent(CallsActivity.this, RemoveNodeActivity.class); }else{ intent = new Intent(CallsActivity.this, PerformCallActivity.class); intent.putExtra(Constants.KEY_SELECTED_CALL, selectedCall); diff --git a/sample/src/main/java/cy/agorise/labs/sample/RemoveNodeActivity.java b/sample/src/main/java/cy/agorise/labs/sample/RemoveNodeActivity.java new file mode 100644 index 0000000..81f7a6b --- /dev/null +++ b/sample/src/main/java/cy/agorise/labs/sample/RemoveNodeActivity.java @@ -0,0 +1,274 @@ +package cy.agorise.labs.sample; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.util.SortedList; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +import butterknife.BindView; +import butterknife.ButterKnife; +import cy.agorise.graphenej.api.android.NetworkService; +import cy.agorise.graphenej.network.FullNode; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.subjects.PublishSubject; + +public class RemoveNodeActivity extends AppCompatActivity implements ServiceConnection { + + private final String TAG = this.getClass().getName(); + + @BindView(R.id.rvNodes) + RecyclerView rvNodes; + + FullNodesAdapter nodesAdapter; + + // Comparator used to sort the nodes in ascending order + private final Comparator LATENCY_COMPARATOR = (a, b) -> + Double.compare(a.getLatencyValue(), b.getLatencyValue()); + + /* Network service connection */ + private NetworkService mNetworkService; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_remove_node); + + ButterKnife.bind(this); + + rvNodes.setLayoutManager(new LinearLayoutManager(this)); + nodesAdapter = new FullNodesAdapter(this, LATENCY_COMPARATOR); + rvNodes.setAdapter(nodesAdapter); + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + // We've bound to LocalService, cast the IBinder and get LocalService instance + NetworkService.LocalBinder binder = (NetworkService.LocalBinder) iBinder; + mNetworkService = binder.getService(); + + if(mNetworkService != null){ + // PublishSubject used to announce full node latencies updates + PublishSubject fullNodePublishSubject = mNetworkService.getNodeLatencyObservable(); + if(fullNodePublishSubject != null) + fullNodePublishSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(nodeLatencyObserver); + + List fullNodes = mNetworkService.getNodes(); + nodesAdapter.add(fullNodes); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + + } + + /** + * Observer used to be notified about node latency measurement updates. + */ + private Observer nodeLatencyObserver = new Observer() { + @Override + public void onSubscribe(Disposable d) { } + + @Override + public void onNext(FullNode fullNode) { + nodesAdapter.add(fullNode); + } + + @Override + public void onError(Throwable e) { + Log.e(TAG,"nodeLatencyObserver.onError.Msg: "+e.getMessage()); + } + + @Override + public void onComplete() { } + }; + + @Override + protected void onStart() { + super.onStart(); + // Bind to LocalService + Intent intent = new Intent(this, NetworkService.class); + bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onPause() { + super.onPause(); + unbindService(this); + } + + class FullNodesAdapter extends RecyclerView.Adapter { + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView ivNodeStatus; + TextView tvNodeName; + + ViewHolder(View itemView) { + super(itemView); + + ivNodeStatus = itemView.findViewById(R.id.ivNodeStatus); + tvNodeName = itemView.findViewById(R.id.tvNodeName); + } + } + + private final SortedList mSortedList = new SortedList<>(FullNode.class, new SortedList.Callback() { + @Override + public void onInserted(int position, int count) { + notifyItemRangeInserted(position, count); + } + + @Override + public void onRemoved(int position, int count) { + notifyItemRangeRemoved(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public void onChanged(int position, int count) { + notifyItemRangeChanged(position, count); + } + + @Override + public int compare(FullNode a, FullNode b) { + return mComparator.compare(a, b); + } + + @Override + public boolean areContentsTheSame(FullNode oldItem, FullNode newItem) { + return oldItem.getLatencyValue() == newItem.getLatencyValue(); + } + + @Override + public boolean areItemsTheSame(FullNode item1, FullNode item2) { + return item1.getUrl().equals(item2.getUrl()); + } + }); + + private final Comparator mComparator; + + private Context mContext; + + FullNodesAdapter(Context context, Comparator comparator) { + mContext = context; + mComparator = comparator; + } + + private Context getContext() { + return mContext; + } + + @NonNull + @Override + public FullNodesAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + View transactionView = inflater.inflate(R.layout.item_node, parent, false); + + return new ViewHolder(transactionView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { + final FullNode fullNode = mSortedList.get(position); + + // Show the green check mark before the node name if that node is the one being used + if (fullNode.isConnected()) + viewHolder.ivNodeStatus.setImageResource(R.drawable.ic_connected); + else + viewHolder.ivNodeStatus.setImageDrawable(null); + + double latency = fullNode.getLatencyValue(); + + // Select correct color span according to the latency value + ForegroundColorSpan colorSpan; + + if (latency < 400) + colorSpan = new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.colorPrimary)); + else if (latency < 800) + colorSpan = new ForegroundColorSpan(Color.rgb(255,136,0)); // Holo orange + else + colorSpan = new ForegroundColorSpan(Color.rgb(204,0,0)); // Holo red + + // Create a string with the latency number colored according to their amount + SpannableStringBuilder ssb = new SpannableStringBuilder(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ssb.append(fullNode.getUrl().replace("wss://", ""), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + ssb.append(" ("); + + // 2000 ms is the timeout of the websocket used to calculate the latency, therefore if the + // received latency is greater than such value we can assume the node was not reachable. + String ms = latency < 2000 ? String.format(Locale.US, "%.0f ms", latency) : "??"; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ssb.append(ms, colorSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + ssb.append(")"); + + viewHolder.tvNodeName.setText(ssb); + } + + /** + * Functions that adds/updates a FullNode to the SortedList + */ + public void add(FullNode fullNode) { + // Remove the old instance of the FullNode before adding a new one. My understanding is that + // the sorted list should be able to automatically find repeated elements and update them + // instead of adding duplicates but it wasn't working so I opted for manually removing old + // instances of FullNodes before adding the updated ones. + int removed = 0; + for (int i=0; i fullNodes) { + mSortedList.addAll(fullNodes); + } + + + @Override + public int getItemCount() { + return mSortedList.size(); + } + } +} diff --git a/sample/src/main/res/drawable/ic_connected.xml b/sample/src/main/res/drawable/ic_connected.xml new file mode 100644 index 0000000..5b57096 --- /dev/null +++ b/sample/src/main/res/drawable/ic_connected.xml @@ -0,0 +1,5 @@ + + + diff --git a/sample/src/main/res/layout/activity_remove_node.xml b/sample/src/main/res/layout/activity_remove_node.xml new file mode 100644 index 0000000..a1cb381 --- /dev/null +++ b/sample/src/main/res/layout/activity_remove_node.xml @@ -0,0 +1,25 @@ + + + + + +