Improved Nodes Dialog.

- Made a couple of improvements to the Nodes Dialog in the Settings, so that it complies with the Nodes Dialog in PalmPay.
This commit is contained in:
Severiano Jaramillo 2019-08-29 11:46:22 -05:00
parent e2637ddfa4
commit a42736b738
2 changed files with 83 additions and 91 deletions

View file

@ -10,62 +10,54 @@ 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 androidx.recyclerview.widget.SortedListAdapterCallback
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
class FullNodesAdapter(private val context: Context) :
RecyclerView.Adapter<FullNodesAdapter.ViewHolder>() {
private val mComparator =
Comparator<FullNode> { a, b -> java.lang.Double.compare(a.latencyValue, b.latencyValue) }
companion object {
private const val TAG = "FullNodesAdapter"
private const val MAX_LATENCY = 9999
}
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)
}
private val mSortedList = SortedList(FullNode::class.java,
object : SortedListAdapterCallback<FullNode>(this) {
override fun compare(a: FullNode, b: FullNode): Int {
return mComparator.compare(a, b)
return a.latencyValue.compareTo(b.latencyValue)
}
override fun areContentsTheSame(oldItem: FullNode, newItem: FullNode): Boolean {
return oldItem.latencyValue == newItem.latencyValue
return oldItem.latencyValue == newItem.latencyValue &&
oldItem.isConnected == oldItem.isConnected &&
oldItem.isRemoved == oldItem.isRemoved
}
override fun areItemsTheSame(item1: FullNode, item2: FullNode): Boolean {
return item1.url == item2.url
override fun areItemsTheSame(oldItem: FullNode, newItem: FullNode): Boolean {
return oldItem.url == newItem.url
}
})
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FullNodesAdapter.ViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(context)
val transactionView = inflater.inflate(R.layout.item_node, parent, false)
@ -96,9 +88,8 @@ class FullNodesAdapter(private val context: Context) : RecyclerView.Adapter<Full
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 "??"
// Make sure that the latency shown is no greater than MAX_LATENCY
val ms = if(latency <= MAX_LATENCY) "%.0fms".format(latency) else ">%dms".format(MAX_LATENCY)
ssb.append(ms, colorSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
ssb.append(")")
@ -109,28 +100,36 @@ class FullNodesAdapter(private val context: Context) : RecyclerView.Adapter<Full
/**
* 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++)
@Synchronized fun add(fullNode: FullNode) {
var index = -1
for (i in 0 until mSortedList.size()) {
if (mSortedList[i].url == fullNode.url) {
index = i
break
}
}
mSortedList.add(fullNode)
if (index == -1) {
Log.d(TAG , "Adding node: ${fullNode.url}")
mSortedList.add(fullNode) // Add it if it does not exist
} else {
Log.d(TAG , "Updating node: ${fullNode.url}")
mSortedList.updateItemAt(index, fullNode) // Update it if it does exist
}
}
/**
* 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 add(fullNodes: List<FullNode>) = fullNodes.forEach { fullNode ->
// Make sure we are not adding duplicate nodes when the app returns from another activity
if (fullNode.latencyValue < Long.MAX_VALUE-1)
add(fullNode)
}
fun remove(fullNode: FullNode) {
Log.d(TAG , "Removing node: ${fullNode.url}")
mSortedList.remove(fullNode)
}

View file

@ -32,11 +32,8 @@ import cy.agorise.graphenej.api.calls.GetAccounts
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 cy.agorise.graphenej.operations.AccountUpgradeOperationBuilder
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.fragment_settings.*
import org.bitcoinj.core.DumpedPrivateKey
@ -129,30 +126,7 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
initNightModeSwitch()
tvNetworkStatus.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 = String.format("%s v%s", getString(R.string.app_name), BuildConfig.VERSION_NAME))
.message(text = getString(R.string.title__bitshares_nodes_dialog, "-------"))
.customListAdapter(nodesAdapter as FullNodesAdapter)
.negativeButton(android.R.string.ok)
.onDismiss { mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask) }
mNodesDialog?.show()
// Registering a recurrent task used to poll for dynamic global properties requests
mHandler.post(mRequestDynamicGlobalPropertiesTask)
}
}
tvNetworkStatus.setOnClickListener { v -> showNodesDialog(v) }
// Obtain the current Security Lock Option selected and display it in the screen
val securityLockSelected = PreferenceManager.getDefaultSharedPreferences(context)
@ -172,26 +146,45 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
btnUpgradeToLTM.setOnClickListener { onUpgradeToLTMButtonSelected() }
}
/**
* Observer used to be notified about node latency measurement updates.
*/
private val nodeLatencyObserver = object : Observer<FullNode> {
override fun onSubscribe(d: Disposable) {
mDisposables.add(d)
}
private fun showNodesDialog(v: View) {
if (mNetworkService != null) {
val fullNodes = mNetworkService!!.nodes
override fun onNext(fullNode: FullNode) {
nodesAdapter = FullNodesAdapter(v.context)
nodesAdapter?.add(fullNodes)
// PublishSubject used to announce full node latencies updates
val fullNodePublishSubject = mNetworkService!!.nodeLatencyObservable ?: return
val nodesDisposable = fullNodePublishSubject
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ fullNode ->
if (!fullNode.isRemoved)
nodesAdapter?.add(fullNode)
else
nodesAdapter?.remove(fullNode)
}, {
Log.e(TAG, "nodeLatencyObserver.onError.Msg: " + it.message)
}
)
mNodesDialog = MaterialDialog(v.context)
.title(text = String.format("%s v%s", getString(R.string.app_name), BuildConfig.VERSION_NAME))
.message(text = getString(R.string.title__bitshares_nodes_dialog, "-------"))
.customListAdapter(nodesAdapter as FullNodesAdapter)
.negativeButton(android.R.string.ok)
.onDismiss {
mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask)
nodesDisposable.dispose()
}
override fun onError(e: Throwable) {
Log.e(TAG, "nodeLatencyObserver.onError.Msg: " + e.message)
}
mNodesDialog?.show()
override fun onComplete() {}
// Registering a recurrent task used to poll for dynamic global properties requests
mHandler.post(mRequestDynamicGlobalPropertiesTask)
}
}
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {