Create FullNodesAdapter to display a live updated list of the nodes the app is watching and also the one it is connected to. This list is displayed inside a dialog that pops when the user clicks the status icon in the Settings Footer. The dialog also shows the live updated BitShares Block Number
This commit is contained in:
parent
504441afa2
commit
c4fd79f22c
5 changed files with 264 additions and 1 deletions
|
@ -0,0 +1,140 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.graphenej.network.FullNode
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* Adapter used to populate the elements of the Bitshares nodes dialog in order to show a list of
|
||||
* nodes with their latency.
|
||||
*/
|
||||
class FullNodesAdapter(private val context: Context) : RecyclerView.Adapter<FullNodesAdapter.ViewHolder>() {
|
||||
val TAG: String = this.javaClass.name
|
||||
|
||||
private val mComparator =
|
||||
Comparator<FullNode> { a, b -> java.lang.Double.compare(a.latencyValue, b.latencyValue) }
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val tvNodeName: TextView = itemView.findViewById(R.id.tvNodeName)
|
||||
val ivNodeStatus: ImageView = itemView.findViewById(R.id.ivNodeStatus)
|
||||
}
|
||||
|
||||
private val mSortedList = SortedList(FullNode::class.java, object : SortedList.Callback<FullNode>() {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int) {
|
||||
notifyItemRangeChanged(position, count)
|
||||
}
|
||||
|
||||
override fun compare(a: FullNode, b: FullNode): Int {
|
||||
return mComparator.compare(a, b)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: FullNode, newItem: FullNode): Boolean {
|
||||
return oldItem.latencyValue == newItem.latencyValue
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: FullNode, item2: FullNode): Boolean {
|
||||
return item1.url == item2.url
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FullNodesAdapter.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
val transactionView = inflater.inflate(R.layout.item_node, parent, false)
|
||||
|
||||
return ViewHolder(transactionView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val fullNode = mSortedList[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)
|
||||
|
||||
val latency = fullNode.latencyValue
|
||||
|
||||
// Select correct color span according to the latency value
|
||||
val colorSpan = when {
|
||||
latency < 400 -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorPrimary))
|
||||
latency < 800 -> ForegroundColorSpan(Color.rgb(255,136,0)) // Holo orange
|
||||
else -> ForegroundColorSpan(Color.rgb(204,0,0)) // Holo red
|
||||
}
|
||||
|
||||
// Create a string with the latency number colored according to their amount
|
||||
val ssb = SpannableStringBuilder()
|
||||
ssb.append(fullNode.url.replace("wss://", ""), 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.
|
||||
val ms = if(latency < 2000) "%.0f ms".format(latency) else "??"
|
||||
|
||||
ssb.append(ms, colorSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
ssb.append(")")
|
||||
|
||||
viewHolder.tvNodeName.text = ssb
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions that adds/updates a FullNode to the SortedList
|
||||
*/
|
||||
fun 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.
|
||||
var removed = 0
|
||||
for (i in 0 until mSortedList.size())
|
||||
if (mSortedList[i-removed].url == (fullNode.url))
|
||||
mSortedList.removeItemAt(i-removed++)
|
||||
|
||||
mSortedList.add(fullNode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that adds a whole list of nodes to the SortedList. It should only be used at the
|
||||
* moment of populating the SortedList for the first time.
|
||||
*/
|
||||
fun add(fullNodes: List<FullNode>) {
|
||||
mSortedList.addAll(fullNodes)
|
||||
}
|
||||
|
||||
fun remove(fullNode: FullNode) {
|
||||
mSortedList.remove(fullNode)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mSortedList.size()
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package cy.agorise.bitsybitshareswallet.fragments
|
|||
import android.content.*
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
|
@ -13,18 +14,27 @@ import android.widget.Toast
|
|||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.list.customListAdapter
|
||||
import cy.agorise.bitsybitshareswallet.BuildConfig
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.adapters.FullNodesAdapter
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
|
||||
import cy.agorise.graphenej.BrainKey
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
|
||||
import cy.agorise.graphenej.models.DynamicGlobalProperties
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import cy.agorise.graphenej.network.FullNode
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.fragment_settings.*
|
||||
import java.text.NumberFormat
|
||||
|
||||
class SettingsFragment : Fragment(), ServiceConnection {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
@ -37,6 +47,14 @@ class SettingsFragment : Fragment(), ServiceConnection {
|
|||
/** Flag used to keep track of the NetworkService binding state */
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
// Dialog displaying the list of nodes and their latencies
|
||||
private var mNodesDialog: MaterialDialog? = null
|
||||
|
||||
/** Adapter that holds the FullNode list used in the Bitshares nodes modal */
|
||||
private var nodesAdapter: FullNodesAdapter? = null
|
||||
|
||||
private val mHandler = Handler()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
|
@ -54,6 +72,31 @@ class SettingsFragment : Fragment(), ServiceConnection {
|
|||
|
||||
tvFooterAppVersion.text = String.format("%s v%s", getString(R.string.app_name), BuildConfig.VERSION_NAME)
|
||||
|
||||
ivConnectionStatusIcon.setOnClickListener { v ->
|
||||
if (mNetworkService != null) {
|
||||
// PublishSubject used to announce full node latencies updates
|
||||
val fullNodePublishSubject = mNetworkService!!.nodeLatencyObservable
|
||||
fullNodePublishSubject?.observeOn(AndroidSchedulers.mainThread())?.subscribe(nodeLatencyObserver)
|
||||
|
||||
val fullNodes = mNetworkService!!.nodes
|
||||
|
||||
nodesAdapter = FullNodesAdapter(v.context)
|
||||
nodesAdapter!!.add(fullNodes)
|
||||
|
||||
mNodesDialog = MaterialDialog(v.context)
|
||||
.title(text = getString(R.string.title__bitshares_nodes_dialog, "-------"))
|
||||
.customListAdapter(nodesAdapter as FullNodesAdapter)
|
||||
.negativeButton(android.R.string.ok) {
|
||||
mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask)
|
||||
}
|
||||
|
||||
mNodesDialog?.show()
|
||||
|
||||
// Registering a recurrent task used to poll for dynamic global properties requests
|
||||
mHandler.post(mRequestDynamicGlobalPropertiesTask)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the RxBus, which receives events from the NetworkService
|
||||
mDisposables.add(
|
||||
RxBus.getBusInstance()
|
||||
|
@ -63,8 +106,56 @@ class SettingsFragment : Fragment(), ServiceConnection {
|
|||
)
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(message: Any?) {
|
||||
/**
|
||||
* Observer used to be notified about node latency measurement updates.
|
||||
*/
|
||||
private val nodeLatencyObserver = object : Observer<FullNode> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
mDisposables.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(fullNode: FullNode) {
|
||||
if (!fullNode.isRemoved)
|
||||
nodesAdapter?.add(fullNode)
|
||||
else
|
||||
nodesAdapter?.remove(fullNode)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "nodeLatencyObserver.onError.Msg: " + e.message)
|
||||
}
|
||||
|
||||
override fun onComplete() {}
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(message: Any?) {
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
if (message.result is DynamicGlobalProperties) {
|
||||
val dynamicGlobalProperties = message.result as DynamicGlobalProperties
|
||||
if (mNodesDialog != null && mNodesDialog?.isShowing == true) {
|
||||
val blockNumber = NumberFormat.getInstance().format(dynamicGlobalProperties.head_block_number)
|
||||
mNodesDialog?.title(text = getString(R.string.title__bitshares_nodes_dialog, blockNumber))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to obtain frequent updates on the global dynamic properties object
|
||||
*/
|
||||
private val mRequestDynamicGlobalPropertiesTask = object : Runnable {
|
||||
override fun run() {
|
||||
if (mNetworkService != null) {
|
||||
if (mNetworkService?.isConnected == true) {
|
||||
mNetworkService?.sendMessage(GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API)
|
||||
} else {
|
||||
Log.d(TAG, "NetworkService exists but is not connected")
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "NetworkService reference is null")
|
||||
}
|
||||
mHandler.postDelayed(this, Constants.BLOCK_PERIOD)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,6 +35,9 @@ object Constants {
|
|||
/** Time period to wait to send a request to the NetworkService, and retry in case it is still not connected */
|
||||
const val NETWORK_SERVICE_RETRY_PERIOD: Long = 5000
|
||||
|
||||
/** Bitshares block period */
|
||||
const val BLOCK_PERIOD: Long = 3000
|
||||
|
||||
/** Key used to store the number of operations that the currently selected account had last time we checked */
|
||||
const val KEY_ACCOUNT_OPERATION_COUNT = "key_account_operation_count"
|
||||
|
||||
|
|
28
app/src/main/res/layout/item_node.xml
Normal file
28
app/src/main/res/layout/item_node.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivNodeStatus"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
tools:src="@drawable/ic_connected"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvNodeName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
tools:text="de.palmpayisthebestappinthefreakingworld.io/ws (123ms)"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -76,6 +76,7 @@
|
|||
<string name="msg__bugs_or_ideas">Telegram chat: http://t.me/Agorise\nEmail: Agorise@protonmail.ch\nOpen Source:
|
||||
https://github.com/Agorise
|
||||
</string>
|
||||
<string name="title__bitshares_nodes_dialog">Block: %1$s</string>
|
||||
<string name="text__coming_soon">Coming soon</string>
|
||||
<string name="title_net_worth">Net Worth</string>
|
||||
<string name="title_search">Search</string>
|
||||
|
|
Loading…
Reference in a new issue