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.widget.Toast
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.android.NetworkService
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.HistoryOperationDetail
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
*/
abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val TAG = this.javaClass.simpleName
private val TAG = "ConnectedActivity"
private lateinit var mUserAccountViewModel: UserAccountViewModel
private val mHandler = Handler()
@ -33,6 +41,8 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private var storedOpCount: Long = -1
private var missingUserAccounts = ArrayList<UserAccount>()
/* Network service connection */
protected var mNetworkService: NetworkService? = null
@ -52,7 +62,10 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
handleJsonRpcResponse(message)
// Payment detection focused responses
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.id == recurrentAccountUpdateId) {
// 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) {
// handleNewOperations(message.result as HistoryOperationDetail)
// }
}
} else {
// In case of error
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() {
super.onDestroy()
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
private fun handleAccountProperties(accountPropertiesList: List<AccountProperties>) {
val userAccounts = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.UserAccount>()
for (accountProperties in accountPropertiesList) {
val userAccount = cy.agorise.bitsybitshareswallet.database.entities.UserAccount(
accountProperties.id,
accountProperties.name,
accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
)
userAccounts.add(userAccount)
}
override fun onResume() {
super.onResume()
mUserAccountViewModel.insertAll(userAccounts)
}
val intent = Intent(this, NetworkService::class.java)
if (bindService(intent, this, Context.BIND_AUTO_CREATE)) {
mShouldUnbindNetwork = true
/**
* Task used to obtain the missing UserAccounts.
*/
private val mRequestMissingUserAccountsTask = object : Runnable {
override fun run() {
if (mNetworkService!!.isConnected) {
mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
} else {
Log.e(TAG, "Binding to the network service failed.")
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
// mHandler.postDelayed(mCheckMissingPaymentsTask, Constants.MISSING_PAYMENT_CHECK_PERIOD)
// storedOpCount = PreferenceManager.getDefaultSharedPreferences(this)
// .getLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, -1)
}
override fun onPause() {
super.onPause()
// Unbinding from network service
if (mShouldUnbindNetwork) {
unbindService(this)
mShouldUnbindNetwork = false
}
// mHandler.removeCallbacks(mCheckMissingPaymentsTask)
}
/**
@ -146,7 +170,37 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
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
))
viewHolder.tvFrom.text = transferDetail.from
viewHolder.tvTo.text = transferDetail.to
viewHolder.tvFrom.text = transferDetail.from ?: ""
viewHolder.tvTo.text = transferDetail.to ?: ""
viewHolder.tvDate.text = "02 Oct"
viewHolder.tvTime.text = "15:01:18 CET"

View file

@ -3,6 +3,7 @@ package cy.agorise.bitsybitshareswallet.database.daos
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
@ -11,9 +12,16 @@ interface UserAccountDao {
@Insert
fun insert(userAccount: UserAccount)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(userAccounts: List<UserAccount>)
@Query("SELECT * FROM user_accounts WHERE user_accounts.id = :id")
fun getUserAccount(id: String): LiveData<UserAccount>
@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(
val id: String,
val from: String,
val to: String,
val from: String?,
val to: String?,
val direction: Boolean, // True -> Received, False -> Sent
// val date: Long,
val cryptoAmount: Long,

View file

@ -7,6 +7,6 @@ import androidx.room.Query
@Dao
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>>
}

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
transferRepository!!.deleteAll()
}
mDisposables.add(transferRepository!!.getCount()
mDisposables.add(
transferRepository!!.getCount()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { transferCount ->
if (transferCount > 0) {
// If we already have some transfers in the database, we might want to skip the request
// straight to the last batch
historicalTransferCount = Math.floor((transferCount / HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt()
historicalTransferCount = Math.floor((transferCount /
HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt()
}
// Retrieving account transactions
loadNextOperationsBatch()
})
}
)
}
/**
@ -213,6 +216,7 @@ class TransfersLoader(private var mContext: Context?, private val mLifeCycle: Li
historicalTransferCount++
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))
if (/* insertedCount == 0 && */ operationHistoryList.isEmpty()) {
// TODO Terminate process and obtain MissingTimes and MissingEquivalentValues

View file

@ -1,18 +1,18 @@
package cy.agorise.bitsybitshareswallet.repositories
import android.app.Application
import android.content.Context
import android.os.AsyncTask
import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.daos.UserAccountDao
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
class UserAccountRepository internal constructor(application: Application) {
class UserAccountRepository internal constructor(context: Context) {
private val mUserAccountDao: UserAccountDao
init {
val db = BitsyDatabase.getDatabase(application)
val db = BitsyDatabase.getDatabase(context)
mUserAccountDao = db!!.userAccountDao()
}
@ -20,10 +20,18 @@ class UserAccountRepository internal constructor(application: Application) {
insertAsyncTask(mUserAccountDao).execute(userAccount)
}
fun insertAll(userAccounts: List<UserAccount>) {
insertAllAsyncTask(mUserAccountDao).execute(userAccounts)
}
fun getUserAccount(id: String): LiveData<UserAccount> {
return mUserAccountDao.getUserAccount(id)
}
fun getMissingUserAccountIds(): LiveData<List<String>> {
return mUserAccountDao.getMissingAccountIds()
}
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
AsyncTask<UserAccount, Void, Void>() {
@ -32,4 +40,13 @@ class UserAccountRepository internal constructor(application: Application) {
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"
/**
* 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")
val bitUSD = Asset("1.3.121")
const val MISSING_PAYMENT_CHECK_PERIOD: Long = 5000
/**
* 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 */
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)
}
internal fun getMissingUserAccountIds(): LiveData<List<String>> {
return mRepository.getMissingUserAccountIds()
}
// fun insert(userAccount: UserAccount) {
// mRepository.insert(userAccount)
// }
fun insertAll(userAccounts: List<UserAccount>) {
mRepository.insertAll(userAccounts)
}
}