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() }