From 7f462bec635f803e11e2ebebad12c3a5b2b0da8c Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Thu, 20 Dec 2018 13:34:11 -0600 Subject: [PATCH] - 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. --- .../activities/ConnectedActivity.kt | 100 +++++++++----- .../adapters/AutoSuggestAssetAdapter.kt | 44 ++++++ .../fragments/ReceiveTransactionFragment.kt | 129 +++++++++++++++++- .../fragments/SendTransactionFragment.kt | 9 +- 4 files changed, 243 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/AutoSuggestAssetAdapter.kt diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt index 8646334..54ff0e2 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt @@ -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() + /** * 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) - } else if ((message.result as List<*>)[0] is AssetAmount) { - handleBalanceUpdate(message.result as List) - } else if ((message.result as List<*>)[0] is Asset) { - handleAssets(message.result as List) - } - } - } 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) + + RESPONSE_GET_ACCOUNT_BALANCES -> + handleBalanceUpdate(message.result as List) + + RESPONSE_GET_ASSETS -> + handleAssets(message.result as List) } - } 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() 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") diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/AutoSuggestAssetAdapter.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/AutoSuggestAssetAdapter.kt new file mode 100644 index 0000000..0e2d78d --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/AutoSuggestAssetAdapter.kt @@ -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(context, resource) { + + private var mAssets = ArrayList() + + fun setData(assets: List) { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt index 4b3272f..1dc985a 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt @@ -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() private var selectedAssetSymbol = "" + // Map used to keep track of request and response id pairs + private val responseMap = HashMap() + + /* 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> { assets -> +// val adapter = ArrayAdapter(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) + } + 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) { + 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 + } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt index 637c786..fd2b0a0 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt @@ -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() }