Created a method that is always listening to changes in the database and finds out when at least one UserAccount involved in a transaction is not present in the user_accounts db table. When such accounts are found they are retrieved in a form of a list and their information is requested to the BitShares nodes through graphenej's NetworkService and saved into the db once a response is received. The updated user account information is automatically displayed in the transactions list because of AAC's ViewModel, LiveData and Room.

This commit is contained in:
Severiano Jaramillo 2018-12-08 20:36:31 -06:00
parent 496e0ac21f
commit 628b30ce54
10 changed files with 145 additions and 91 deletions

View file

@ -10,9 +10,16 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
import cy.agorise.graphenej.UserAccount
import cy.agorise.graphenej.api.ConnectionStatusUpdate import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.android.NetworkService import cy.agorise.graphenej.api.android.NetworkService
import cy.agorise.graphenej.api.android.RxBus import cy.agorise.graphenej.api.android.RxBus
import cy.agorise.graphenej.api.calls.GetAccounts
import cy.agorise.graphenej.models.AccountProperties
import cy.agorise.graphenej.models.FullAccountDetails import cy.agorise.graphenej.models.FullAccountDetails
import cy.agorise.graphenej.models.HistoryOperationDetail import cy.agorise.graphenej.models.HistoryOperationDetail
import cy.agorise.graphenej.models.JsonRpcResponse import cy.agorise.graphenej.models.JsonRpcResponse
@ -23,8 +30,9 @@ import io.reactivex.disposables.Disposable
* Class in charge of managing the connection to graphenej's NetworkService * Class in charge of managing the connection to graphenej's NetworkService
*/ */
abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val TAG = this.javaClass.simpleName
private val TAG = "ConnectedActivity" private lateinit var mUserAccountViewModel: UserAccountViewModel
private val mHandler = Handler() private val mHandler = Handler()
@ -33,6 +41,8 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private var storedOpCount: Long = -1 private var storedOpCount: Long = -1
private var missingUserAccounts = ArrayList<UserAccount>()
/* Network service connection */ /* Network service connection */
protected var mNetworkService: NetworkService? = null protected var mNetworkService: NetworkService? = null
@ -52,7 +62,10 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
handleJsonRpcResponse(message) handleJsonRpcResponse(message)
// Payment detection focused responses // Payment detection focused responses
if (message.error == null) { if (message.error == null) {
// if (message.result is List<*> && (message.result as List<*>).size > 0) { if (message.result is List<*> && (message.result as List<*>).size > 0) {
if ((message.result as List<*>)[0] is AccountProperties) {
handleAccountProperties(message.result as List<AccountProperties>)
}
// if ((message.result as List<*>)[0] is FullAccountDetails) { // if ((message.result as List<*>)[0] is FullAccountDetails) {
// if (message.id == recurrentAccountUpdateId) { // if (message.id == recurrentAccountUpdateId) {
// handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails) // handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails)
@ -62,7 +75,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
// } // }
// } else if (message.result is HistoryOperationDetail && message.id == accountOpRequestId) { // } else if (message.result is HistoryOperationDetail && message.id == accountOpRequestId) {
// handleNewOperations(message.result as HistoryOperationDetail) // handleNewOperations(message.result as HistoryOperationDetail)
// } }
} else { } else {
// In case of error // In case of error
Log.e(TAG, "Got error message from full node. Msg: " + message.error.message) Log.e(TAG, "Got error message from full node. Msg: " + message.error.message)
@ -81,36 +94,47 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
} }
} }
} }
// Configure UserAccountViewModel to show the current account
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)
mUserAccountViewModel.getMissingUserAccountIds().observe(this, Observer<List<String>>{ userAccountIds ->
if (!userAccountIds.isEmpty()) {
for (userAccountId in userAccountIds)
missingUserAccounts.add(UserAccount(userAccountId))
mHandler.postDelayed(mRequestMissingUserAccountsTask, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
})
} }
override fun onDestroy() { private fun handleAccountProperties(accountPropertiesList: List<AccountProperties>) {
super.onDestroy() val userAccounts = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.UserAccount>()
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
}
override fun onResume() { for (accountProperties in accountPropertiesList) {
super.onResume() val userAccount = cy.agorise.bitsybitshareswallet.database.entities.UserAccount(
accountProperties.id,
accountProperties.name,
accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
)
val intent = Intent(this, NetworkService::class.java) userAccounts.add(userAccount)
if (bindService(intent, this, Context.BIND_AUTO_CREATE)) {
mShouldUnbindNetwork = true
} else {
Log.e(TAG, "Binding to the network service failed.")
} }
// mHandler.postDelayed(mCheckMissingPaymentsTask, Constants.MISSING_PAYMENT_CHECK_PERIOD)
// storedOpCount = PreferenceManager.getDefaultSharedPreferences(this) mUserAccountViewModel.insertAll(userAccounts)
// .getLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, -1)
} }
override fun onPause() { /**
super.onPause() * Task used to obtain the missing UserAccounts.
// Unbinding from network service */
if (mShouldUnbindNetwork) { private val mRequestMissingUserAccountsTask = object : Runnable {
unbindService(this) override fun run() {
mShouldUnbindNetwork = false if (mNetworkService!!.isConnected) {
mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
} else {
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
} }
// mHandler.removeCallbacks(mCheckMissingPaymentsTask)
} }
/** /**
@ -146,7 +170,37 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
mNetworkService = binder.service mNetworkService = binder.service
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) { }
override fun onPause() {
super.onPause()
// Unbinding from network service
if (mShouldUnbindNetwork) {
unbindService(this)
mShouldUnbindNetwork = false
}
// mHandler.removeCallbacks(mCheckMissingPaymentsTask)
mHandler.removeCallbacks(mRequestMissingUserAccountsTask)
}
override fun onResume() {
super.onResume()
val intent = Intent(this, NetworkService::class.java)
if (bindService(intent, this, Context.BIND_AUTO_CREATE)) {
mShouldUnbindNetwork = true
} else {
Log.e(TAG, "Binding to the network service failed.")
}
// mHandler.postDelayed(mCheckMissingPaymentsTask, Constants.MISSING_PAYMENT_CHECK_PERIOD)
// storedOpCount = PreferenceManager.getDefaultSharedPreferences(this)
// .getLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, -1)
}
override fun onDestroy() {
super.onDestroy()
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
} }
/** /**

View file

@ -88,8 +88,8 @@ class TransfersDetailsAdapter(private val context: Context) :
if(transferDetail.direction) R.color.colorReceive else R.color.colorSend if(transferDetail.direction) R.color.colorReceive else R.color.colorSend
)) ))
viewHolder.tvFrom.text = transferDetail.from viewHolder.tvFrom.text = transferDetail.from ?: ""
viewHolder.tvTo.text = transferDetail.to viewHolder.tvTo.text = transferDetail.to ?: ""
viewHolder.tvDate.text = "02 Oct" viewHolder.tvDate.text = "02 Oct"
viewHolder.tvTime.text = "15:01:18 CET" viewHolder.tvTime.text = "15:01:18 CET"
@ -104,7 +104,7 @@ class TransfersDetailsAdapter(private val context: Context) :
Math.pow(10.toDouble(), transferDetail.cryptoPrecision.toDouble()) Math.pow(10.toDouble(), transferDetail.cryptoPrecision.toDouble())
val cryptoAmount = "${df.format(amount)} ${transferDetail.cryptoSymbol}" val cryptoAmount = "${df.format(amount)} ${transferDetail.cryptoSymbol}"
viewHolder.tvCryptoAmount.text = cryptoAmount viewHolder.tvCryptoAmount.text = cryptoAmount
viewHolder.tvFiatEquivalent.text = "$4119.75" viewHolder.tvFiatEquivalent.text = "$4119.75"
viewHolder.ivDirectionArrow.setImageDrawable(context.getDrawable( viewHolder.ivDirectionArrow.setImageDrawable(context.getDrawable(

View file

@ -3,6 +3,7 @@ package cy.agorise.bitsybitshareswallet.database.daos
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
@ -11,9 +12,16 @@ interface UserAccountDao {
@Insert @Insert
fun insert(userAccount: UserAccount) fun insert(userAccount: UserAccount)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(userAccounts: List<UserAccount>)
@Query("SELECT * FROM user_accounts WHERE user_accounts.id = :id") @Query("SELECT * FROM user_accounts WHERE user_accounts.id = :id")
fun getUserAccount(id: String): LiveData<UserAccount> fun getUserAccount(id: String): LiveData<UserAccount>
@Query("SELECT * FROM user_accounts") @Query("SELECT * FROM user_accounts")
fun getAllUserAccounts(): LiveData<List<UserAccount>> fun getAll(): LiveData<List<UserAccount>>
// TODO not sure if this is the best place for this query as it involves two entities
@Query("SELECT DISTINCT destination FROM transfers WHERE destination NOT IN (SELECT id FROM user_accounts) UNION SELECT DISTINCT source FROM transfers WHERE source NOT IN (SELECT id FROM user_accounts)")
fun getMissingAccountIds(): LiveData<List<String>>
} }

View file

@ -2,8 +2,8 @@ package cy.agorise.bitsybitshareswallet.database.joins
data class TransferDetail( data class TransferDetail(
val id: String, val id: String,
val from: String, val from: String?,
val to: String, val to: String?,
val direction: Boolean, // True -> Received, False -> Sent val direction: Boolean, // True -> Received, False -> Sent
// val date: Long, // val date: Long,
val cryptoAmount: Long, val cryptoAmount: Long,

View file

@ -7,6 +7,6 @@ import androidx.room.Query
@Dao @Dao
interface TransferDetailDao { interface TransferDetailDao {
@Query("SELECT transfers.id, IFNULL((SELECT name FROM user_accounts WHERE user_accounts.id=transfers.source), '') AS `from`, IFNULL((SELECT name FROM user_accounts WHERE user_accounts.id=transfers.destination), '') AS `to`, (CASE WHEN destination=:userId THEN 1 ELSE 0 END) AS `direction`, transfers.transfer_amount AS `cryptoAmount`, assets.precision AS `cryptoPrecision`, assets.symbol AS cryptoSymbol FROM transfers INNER JOIN assets WHERE transfers.transfer_asset_id = assets.id") @Query("SELECT transfers.id, (SELECT name FROM user_accounts WHERE user_accounts.id=transfers.source) AS `from`, (SELECT name FROM user_accounts WHERE user_accounts.id=transfers.destination) AS `to`, (CASE WHEN destination=:userId THEN 1 ELSE 0 END) AS `direction`, transfers.transfer_amount AS `cryptoAmount`, assets.precision AS `cryptoPrecision`, assets.symbol AS cryptoSymbol FROM transfers INNER JOIN assets WHERE transfers.transfer_asset_id = assets.id")
fun getAll(userId: String): LiveData<List<TransferDetail>> fun getAll(userId: String): LiveData<List<TransferDetail>>
} }

View file

@ -1,42 +0,0 @@
//package cy.agorise.bitsybitshareswallet.processors
//
//import android.content.Context
//import androidx.lifecycle.Lifecycle
//import cy.agorise.graphenej.api.android.RxBus
//import cy.agorise.graphenej.api.calls.GetAccounts
//import cy.agorise.graphenej.entities.AccountProperties
//import cy.agorise.graphenej.entities.JsonRpcResponse
//import io.reactivex.android.schedulers.AndroidSchedulers
//
///**
// * Loader used to fetch the missing accounts and update the database
// */
//class MissingAccountsLoader(context: Context, lifecycle: Lifecycle) : BaseDataLoader(context, lifecycle) {
//
// init {
// mDisposable = RxBus.getBusInstance()
// .asFlowable()
// .observeOn(AndroidSchedulers.mainThread())
// .subscribe { message ->
// if (message is JsonRpcResponse<*>) {
// if (message.result is List<*> &&
// (message.result as List<*>).size > 0 &&
// (message.result as List<*>)[0] is AccountProperties
// ) {
// database.putUserAccounts(message.result as List<AccountProperties>)
// }
// }
// }
// }
//
// protected fun onNetworkReady() {
// requestAccountInfo()
// }
//
// fun requestAccountInfo() {
// val missingAccountNames = database.getMissingAccountNames()
// if (missingAccountNames.size > 0) {
// mNetworkService.sendMessage(GetAccounts(missingAccountNames), GetAccounts.REQUIRED_API)
// }
// }
//}

View file

@ -187,18 +187,21 @@ class TransfersLoader(private var mContext: Context?, private val mLifeCycle: Li
// If we are in debug mode, we first erase all entries in the 'transfer' table // If we are in debug mode, we first erase all entries in the 'transfer' table
transferRepository!!.deleteAll() transferRepository!!.deleteAll()
} }
mDisposables.add(transferRepository!!.getCount() mDisposables.add(
.subscribeOn(Schedulers.computation()) transferRepository!!.getCount()
.observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.computation())
.subscribe { transferCount -> .observeOn(AndroidSchedulers.mainThread())
if (transferCount > 0) { .subscribe { transferCount ->
// If we already have some transfers in the database, we might want to skip the request if (transferCount > 0) {
// straight to the last batch // If we already have some transfers in the database, we might want to skip the request
historicalTransferCount = Math.floor((transferCount / HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt() // straight to the last batch
historicalTransferCount = Math.floor((transferCount /
HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt()
}
// Retrieving account transactions
loadNextOperationsBatch()
} }
// Retrieving account transactions )
loadNextOperationsBatch()
})
} }
/** /**
@ -213,6 +216,7 @@ class TransfersLoader(private var mContext: Context?, private val mLifeCycle: Li
historicalTransferCount++ historicalTransferCount++
val insertedCount = transferRepository!!.insertAll(processOperationList(operationHistoryList)) val insertedCount = transferRepository!!.insertAll(processOperationList(operationHistoryList))
// TODO return number of inserted rows
// Log.d(TAG, String.format("Inserted count: %d, list size: %d", insertedCount, operationHistoryList.size)) // Log.d(TAG, String.format("Inserted count: %d, list size: %d", insertedCount, operationHistoryList.size))
if (/* insertedCount == 0 && */ operationHistoryList.isEmpty()) { if (/* insertedCount == 0 && */ operationHistoryList.isEmpty()) {
// TODO Terminate process and obtain MissingTimes and MissingEquivalentValues // TODO Terminate process and obtain MissingTimes and MissingEquivalentValues

View file

@ -1,18 +1,18 @@
package cy.agorise.bitsybitshareswallet.repositories package cy.agorise.bitsybitshareswallet.repositories
import android.app.Application import android.content.Context
import android.os.AsyncTask import android.os.AsyncTask
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.UserAccountDao import cy.agorise.bitsybitshareswallet.database.daos.UserAccountDao
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
class UserAccountRepository internal constructor(application: Application) { class UserAccountRepository internal constructor(context: Context) {
private val mUserAccountDao: UserAccountDao private val mUserAccountDao: UserAccountDao
init { init {
val db = BitsyDatabase.getDatabase(application) val db = BitsyDatabase.getDatabase(context)
mUserAccountDao = db!!.userAccountDao() mUserAccountDao = db!!.userAccountDao()
} }
@ -20,10 +20,18 @@ class UserAccountRepository internal constructor(application: Application) {
insertAsyncTask(mUserAccountDao).execute(userAccount) insertAsyncTask(mUserAccountDao).execute(userAccount)
} }
fun insertAll(userAccounts: List<UserAccount>) {
insertAllAsyncTask(mUserAccountDao).execute(userAccounts)
}
fun getUserAccount(id: String): LiveData<UserAccount> { fun getUserAccount(id: String): LiveData<UserAccount> {
return mUserAccountDao.getUserAccount(id) return mUserAccountDao.getUserAccount(id)
} }
fun getMissingUserAccountIds(): LiveData<List<String>> {
return mUserAccountDao.getMissingAccountIds()
}
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) : private class insertAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
AsyncTask<UserAccount, Void, Void>() { AsyncTask<UserAccount, Void, Void>() {
@ -32,4 +40,13 @@ class UserAccountRepository internal constructor(application: Application) {
return null return null
} }
} }
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
AsyncTask<List<UserAccount>, Void, Void>() {
override fun doInBackground(vararg userAccounts: List<UserAccount>): Void? {
mAsyncTaskDao.insertAll(userAccounts[0])
return null
}
}
} }

View file

@ -39,10 +39,15 @@ object Constants {
const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59" const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59"
/** /**
* Smartcoin options for output * Time period between two consecutive requests to the full node performed whenever we have
* open payment requests as a matter of redundancy.
*/ */
val BTS = Asset("1.3.0") const val MISSING_PAYMENT_CHECK_PERIOD: Long = 5000
val bitUSD = Asset("1.3.121")
/**
* 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
/** Key used to store the night mode setting into the shared preferences */ /** Key used to store the night mode setting into the shared preferences */
const val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated" const val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated"

View file

@ -13,7 +13,15 @@ class UserAccountViewModel(application: Application) : AndroidViewModel(applicat
return mRepository.getUserAccount(id) return mRepository.getUserAccount(id)
} }
internal fun getMissingUserAccountIds(): LiveData<List<String>> {
return mRepository.getMissingUserAccountIds()
}
// fun insert(userAccount: UserAccount) { // fun insert(userAccount: UserAccount) {
// mRepository.insert(userAccount) // mRepository.insert(userAccount)
// } // }
fun insertAll(userAccounts: List<UserAccount>) {
mRepository.insertAll(userAccounts)
}
} }