- Add a responseMap (HashMap) to ConnectedActivity so that it only reacts to calls it has made.

- Create AutoSuggestAssetAdapter which is used in the ReceiveTransactionFragment's AutoCompleteTextView to show suggestions backed by the response from queries to the BitShares nodes, according to what the user has already typed in the text field.
This commit is contained in:
Severiano Jaramillo 2018-12-20 13:34:11 -06:00
parent 8fd7c28256
commit 7f462bec63
4 changed files with 243 additions and 39 deletions

View file

@ -31,6 +31,7 @@ import cy.agorise.graphenej.models.FullAccountDetails
import cy.agorise.graphenej.models.JsonRpcResponse
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import java.util.HashMap
import kotlin.collections.ArrayList
/**
@ -39,6 +40,11 @@ import kotlin.collections.ArrayList
abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val TAG = this.javaClass.simpleName
private val RESPONSE_GET_FULL_ACCOUNTS = 1
private val RESPONSE_GET_ACCOUNTS = 2
private val RESPONSE_GET_ACCOUNT_BALANCES = 3
private val RESPONSE_GET_ASSETS = 4
private lateinit var mUserAccountViewModel: UserAccountViewModel
private lateinit var mBalanceViewModel: BalanceViewModel
@ -60,6 +66,9 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
/* Network service connection */
protected var mNetworkService: NetworkService? = null
// Map used to keep track of request and response id pairs
private val responseMap = HashMap<Long, Int>()
/**
* Flag used to keep track of the NetworkService binding state
*/
@ -104,36 +113,49 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
mDisposable = RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { message ->
if (message is JsonRpcResponse<*>) {
// Generic processing taken care by subclasses
handleJsonRpcResponse(message)
// Payment detection focused responses
if (message.error == null) {
if (message.result is List<*> && (message.result as List<*>).size > 0) {
if ((message.result as List<*>)[0] is FullAccountDetails) {
handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails)
} else if ((message.result as List<*>)[0] is AccountProperties) {
handleAccountProperties(message.result as List<AccountProperties>)
} else if ((message.result as List<*>)[0] is AssetAmount) {
handleBalanceUpdate(message.result as List<AssetAmount>)
} else if ((message.result as List<*>)[0] is Asset) {
handleAssets(message.result as List<Asset>)
}
}
} else {
// In case of error
Log.e(TAG, "Got error message from full node. Msg: " + message.error.message)
Toast.makeText(
this@ConnectedActivity,
String.format("Error from full node. Msg: %s", message.error.message),
Toast.LENGTH_LONG
).show()
.subscribe { handleIncomingMessage(it) }
}
private fun handleIncomingMessage(message: Any?) {
if (message is JsonRpcResponse<*>) {
// Generic processing taken care by subclasses
handleJsonRpcResponse(message)
if (message.error == null) {
if (responseMap.containsKey(message.id)) {
val responseType = responseMap[message.id]
when (responseType) {
RESPONSE_GET_FULL_ACCOUNTS ->
handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails)
RESPONSE_GET_ACCOUNTS ->
handleAccountProperties(message.result as List<AccountProperties>)
RESPONSE_GET_ACCOUNT_BALANCES ->
handleBalanceUpdate(message.result as List<AssetAmount>)
RESPONSE_GET_ASSETS ->
handleAssets(message.result as List<Asset>)
}
} else if (message is ConnectionStatusUpdate) {
handleConnectionStatusUpdate(message)
responseMap.remove(message.id)
}
} else {
// In case of error
Log.e(TAG, "Got error message from full node. Msg: " + message.error.message)
Toast.makeText(
this@ConnectedActivity,
String.format("Error from full node. Msg: %s", message.error.message),
Toast.LENGTH_LONG
).show()
}
} else if (message is ConnectionStatusUpdate) {
handleConnectionStatusUpdate(message)
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
// If we got a disconnection notification, we should clear our response map, since
// all its stored request ids will now be reset
responseMap.clear()
}
}
}
/**
@ -225,10 +247,10 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private fun updateBalances() {
if (mNetworkService!!.isConnected) {
mNetworkService!!.sendMessage(
GetAccountBalances(mCurrentAccount, ArrayList()),
GetAccountBalances.REQUIRED_API
)
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
GetAccountBalances.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ACCOUNT_BALANCES
}
}
@ -238,7 +260,9 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val mRequestMissingUserAccountsTask = object : Runnable {
override fun run() {
if (mNetworkService!!.isConnected) {
mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
val id = mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ACCOUNTS
} else if (missingUserAccounts.isNotEmpty()){
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
@ -251,7 +275,9 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val mRequestMissingAssetsTask = object : Runnable {
override fun run() {
if (mNetworkService!!.isConnected) {
mNetworkService!!.sendMessage(GetAssets(missingAssets), GetAssets.REQUIRED_API)
val id = mNetworkService!!.sendMessage(GetAssets(missingAssets), GetAssets.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ASSETS
} else if (missingAssets.isNotEmpty()){
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
@ -267,10 +293,10 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
if (mCurrentAccount != null) {
val userAccounts = ArrayList<String>()
userAccounts.add(mCurrentAccount!!.objectId)
mNetworkService!!.sendMessage(
GetFullAccounts(userAccounts, false),
GetFullAccounts.REQUIRED_API
)
val id = mNetworkService!!.sendMessage(GetFullAccounts(userAccounts, false),
GetFullAccounts.REQUIRED_API)
responseMap[id] = RESPONSE_GET_FULL_ACCOUNTS
}
} else {
Log.w(TAG, "NetworkService is null or is not connected. mNetworkService: $mNetworkService")

View file

@ -0,0 +1,44 @@
package cy.agorise.bitsybitshareswallet.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import cy.agorise.graphenej.Asset
class AutoSuggestAssetAdapter(context: Context, resource: Int):
ArrayAdapter<Asset>(context, resource) {
private var mAssets = ArrayList<Asset>()
fun setData(assets: List<Asset>) {
mAssets.clear()
mAssets.addAll(assets)
}
override fun getCount(): Int {
return mAssets.size
}
override fun getItem(position: Int): Asset? {
return mAssets[position]
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var cv = convertView
if (cv == null) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
cv = inflater.inflate(android.R.layout.simple_spinner_dropdown_item, parent, false)
}
val text: TextView = cv!!.findViewById(android.R.id.text1)
val asset = getItem(position)
text.text = asset!!.symbol
return cv
}
}

View file

@ -1,8 +1,13 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.os.IBinder
import android.preference.PreferenceManager
import android.util.Log
import android.view.LayoutInflater
@ -21,10 +26,16 @@ import com.google.zxing.common.BitMatrix
import com.jakewharton.rxbinding2.widget.RxTextView
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.AssetsAdapter
import cy.agorise.bitsybitshareswallet.adapters.AutoSuggestAssetAdapter
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.viewmodels.AssetViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
import cy.agorise.graphenej.*
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.android.NetworkService
import cy.agorise.graphenej.api.android.RxBus
import cy.agorise.graphenej.api.calls.ListAssets
import cy.agorise.graphenej.models.JsonRpcResponse
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_receive_transaction.*
@ -35,9 +46,14 @@ import java.text.DecimalFormatSymbols
import java.util.*
import java.util.concurrent.TimeUnit
class ReceiveTransactionFragment : Fragment() {
class ReceiveTransactionFragment : Fragment(), ServiceConnection {
private val TAG = this.javaClass.simpleName
private val RESPONSE_LIST_ASSETS = 1
/** Number of assets to request from the NetworkService to show as suggestions in the AutoCompleteTextView */
private val AUTO_SUGGEST_ASSET_LIMIT = 5
private val OTHER_ASSET = "other_asset"
private lateinit var mUserAccountViewModel: UserAccountViewModel
@ -52,10 +68,21 @@ class ReceiveTransactionFragment : Fragment() {
private var mAssetsAdapter: AssetsAdapter? = null
private lateinit var mAutoSuggestAssetAdapter: AutoSuggestAssetAdapter
private var mAssets = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.Asset>()
private var selectedAssetSymbol = ""
// Map used to keep track of request and response id pairs
private val responseMap = HashMap<Long, Int>()
/* Network service connection */
private var mNetworkService: NetworkService? = null
/** Flag used to keep track of the NetworkService binding state */
private var mShouldUnbindNetwork: Boolean = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_receive_transaction, container, false)
}
@ -119,6 +146,19 @@ class ReceiveTransactionFragment : Fragment() {
}
}
// mAssetViewModel.getAll().observe(this,
// Observer<List<cy.agorise.bitsybitshareswallet.database.entities.Asset>> { assets ->
// val adapter = ArrayAdapter<cy.agorise.bitsybitshareswallet.database.entities.Asset>(context!!,
// android.R.layout.simple_dropdown_item_1line, assets)
// actvAsset.setAdapter(adapter)
// })
//
// actvAsset.setOnItemClickListener { parent, _, position, _ ->
// val asset = parent.adapter.getItem(position) as cy.agorise.bitsybitshareswallet.database.entities.Asset
// mAsset = Asset(asset.id, asset.symbol, asset.precision)
// updateQR()
// }
// Use RxJava Debounce to create QR code only after the user stopped typing an amount
mDisposables.add(
RxTextView.textChanges(tietAmount)
@ -126,11 +166,69 @@ class ReceiveTransactionFragment : Fragment() {
.observeOn(AndroidSchedulers.mainThread())
.subscribe { updateQR() }
)
// Add adapter to the Assets AutoCompleteTextView
mAutoSuggestAssetAdapter = AutoSuggestAssetAdapter(context!!, android.R.layout.simple_dropdown_item_1line)
actvAsset.setAdapter(mAutoSuggestAssetAdapter)
// Use RxJava Debounce to avoid making calls to the NetworkService on every text change event and also avoid
// the first call when the View is created
mDisposables.add(
RxTextView.textChanges(actvAsset)
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.toString().trim().toUpperCase() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
mAsset = null
updateQR()
// Get a list of assets that match the already typed string by the user
if (it.length > 1 && mNetworkService != null) {
val id = mNetworkService!!.sendMessage(ListAssets(it, AUTO_SUGGEST_ASSET_LIMIT),
ListAssets.REQUIRED_API)
responseMap[id] = RESPONSE_LIST_ASSETS
}
}
)
// Connect to the RxBus, which receives events from the NetworkService
mDisposables.add(
RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { handleIncomingMessage(it) }
)
}
private fun handleIncomingMessage(message: Any?) {
if (message is JsonRpcResponse<*>) {
if (responseMap.containsKey(message.id)) {
val responseType = responseMap[message.id]
when (responseType) {
RESPONSE_LIST_ASSETS -> handleListAssets(message.result as List<Asset>)
}
responseMap.remove(message.id)
}
} else if (message is ConnectionStatusUpdate) {
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
// If we got a disconnection notification, we should clear our response map, since
// all its stored request ids will now be reset
responseMap.clear()
}
}
}
private fun handleListAssets(assetList: List<Asset>) {
Log.d(TAG, "handleListAssets")
mAutoSuggestAssetAdapter.setData(assetList)
mAutoSuggestAssetAdapter.notifyDataSetChanged()
}
private fun updateQR() {
if (mAsset == null) {
ivQR.setImageDrawable(null)
// TODO clean the please pay and to text at the bottom too
return
}
@ -222,9 +320,38 @@ class ReceiveTransactionFragment : Fragment() {
tvTo.text = txtAccount
}
override fun onResume() {
super.onResume()
val intent = Intent(context, NetworkService::class.java)
if (context?.bindService(intent, this, Context.BIND_AUTO_CREATE) == true) {
mShouldUnbindNetwork = true
} else {
Log.e(TAG, "Binding to the network service failed.")
}
}
override fun onPause() {
super.onPause()
// Unbinding from network service
if (mShouldUnbindNetwork) {
context?.unbindService(this)
mShouldUnbindNetwork = false
}
}
override fun onDestroy() {
super.onDestroy()
if (!mDisposables.isDisposed) mDisposables.dispose()
}
override fun onServiceDisconnected(name: ComponentName?) { }
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
val binder = service as NetworkService.LocalBinder
mNetworkService = binder.service
}
}

View file

@ -440,7 +440,7 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
startCameraPreview()
val intent = Intent(context, NetworkService::class.java)
if (context!!.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
if (context?.bindService(intent, this, Context.BIND_AUTO_CREATE) == true) {
mShouldUnbindNetwork = true
} else {
Log.e(TAG, "Binding to the network service failed.")
@ -449,6 +449,13 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
override fun onPause() {
super.onPause()
// Unbinding from network service
if (mShouldUnbindNetwork) {
context?.unbindService(this)
mShouldUnbindNetwork = false
}
if (!isCameraPreviewVisible)
stopCameraPreview()
}