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 a439f9f..8b94391 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt @@ -1,14 +1,9 @@ package cy.agorise.bitsybitshareswallet.activities -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection import android.content.pm.PackageManager import android.os.AsyncTask import android.os.Bundle import android.os.Handler -import android.os.IBinder import android.preference.PreferenceManager import android.util.Log import android.widget.Toast @@ -56,7 +51,7 @@ import kotlin.concurrent.thread * This class manages everything related to keeping the information in the database updated using graphenej's * NetworkService, leaving to MainActivity only the Navigation work and some other UI features. */ -abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { +abstract class ConnectedActivity : AppCompatActivity() { companion object { private const val TAG = "ConnectedActivity" @@ -96,7 +91,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { private var missingAssets = ArrayList() /* Network service connection */ - protected var mNetworkService: NetworkService? = null + protected var mNetworkService: NetworkService? = NetworkService.getInstance() // Map used to keep track of request and response id pairs private val responseMap = HashMap() @@ -110,11 +105,6 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { // to resolve an equivalent BTS value var transfer: Transfer? = null - /** - * Flag used to keep track of the NetworkService binding state - */ - private var mShouldUnbindNetwork: Boolean = false - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -227,8 +217,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { if (message.error == null) { if (responseMap.containsKey(message.id)) { - val responseType = responseMap[message.id] - when (responseType) { + when (responseMap[message.id]) { RESPONSE_GET_FULL_ACCOUNTS -> handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails) @@ -522,23 +511,9 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { } } - 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 - } - - override fun onServiceDisconnected(name: ComponentName?) { } - 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) mHandler.postDelayed(verifyConnectionToSuitableNodeTask, NODE_CHECK_DELAY) } @@ -549,12 +524,6 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection { mConnectedActivityViewModel.updateNodeLatencies(nodes as List) } - // Unbinding from network service - if (mShouldUnbindNetwork) { - unbindService(this) - mShouldUnbindNetwork = false - mNetworkService = null - } mHandler.removeCallbacks(mCheckMissingPaymentsTask) mHandler.removeCallbacks(mRequestMissingUserAccountsTask) mHandler.removeCallbacks(mRequestMissingAssetsTask) diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt index 577fe7d..c92985f 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt @@ -1,11 +1,6 @@ package cy.agorise.bitsybitshareswallet.fragments -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection import android.os.Bundle -import android.os.IBinder import android.util.Log import android.view.View import androidx.core.os.ConfigurationCompat @@ -23,17 +18,14 @@ import io.reactivex.disposables.CompositeDisposable * Base fragment that defines the methods and variables commonly used in all fragments that directly connect and * talk to the BitShares nodes through graphenej's NetworkService */ -abstract class ConnectedFragment : Fragment(), ServiceConnection { +abstract class ConnectedFragment : Fragment() { companion object { private const val TAG = "ConnectedFragment" } /** Network service connection */ - protected var mNetworkService: NetworkService? = null - - /** Flag used to keep track of the NetworkService binding state */ - private var mShouldUnbindNetwork: Boolean = false + protected var mNetworkService: NetworkService? = NetworkService.getInstance() /** Keeps track of all RxJava disposables, to make sure they are all disposed when the fragment is destroyed */ protected var mDisposables = CompositeDisposable() @@ -66,41 +58,12 @@ abstract class ConnectedFragment : Fragment(), ServiceConnection { } } - 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 - } - /** * Method to be implemented by all subclasses in order to be notified of JSON-RPC responses. * @param response diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt index 28e4b88..fa34009 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt @@ -1,9 +1,7 @@ package cy.agorise.bitsybitshareswallet.fragments -import android.content.ComponentName import android.os.Bundle import android.os.Handler -import android.os.IBinder import android.util.Log import android.view.LayoutInflater import android.view.View @@ -35,7 +33,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_import_brainkey.* import org.bitcoinj.core.ECKey import java.text.NumberFormat -import java.util.ArrayList +import java.util.* import java.util.concurrent.TimeUnit class ImportBrainkeyFragment : BaseAccountFragment() { @@ -249,10 +247,10 @@ class ImportBrainkeyFragment : BaseAccountFragment() { if (Character.isUpperCase(brainKey.toCharArray()[brainKey.length - 1])) { // If the last character is an uppercase, we assume the whole brainkey // was given in capital letters and turn it to lowercase - getAccountFromBrainkey(brainKey.toLowerCase()) + getAccountFromBrainkey(brainKey.toLowerCase(Locale.ROOT)) } else { // Otherwise we turn the whole brainkey to capital letters - getAccountFromBrainkey(brainKey.toUpperCase()) + getAccountFromBrainkey(brainKey.toUpperCase(Locale.ROOT)) } } else { // If no case switching should take place, we perform the network call with @@ -274,6 +272,15 @@ class ImportBrainkeyFragment : BaseAccountFragment() { keyReferencesRequestId = mNetworkService?.sendMessage(GetKeyReferences(address), GetKeyReferences.REQUIRED_API) } + override fun onStart() { + super.onStart() + + if (mNetworkService?.isConnected == true) + showConnectedState() + else + showDisconnectedState() + } + override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) { if (response.id == keyReferencesRequestId) { handleBrainKeyAccountReferences(response.result) @@ -289,7 +296,24 @@ class ImportBrainkeyFragment : BaseAccountFragment() { } override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) { - Log.d(TAG, "handleConnectionStatusUpdate. code: " + connectionStatusUpdate.updateCode) + when (connectionStatusUpdate.updateCode) { + ConnectionStatusUpdate.CONNECTED -> { + showConnectedState() + } + ConnectionStatusUpdate.DISCONNECTED -> { + showDisconnectedState() + } + } + } + + private fun showConnectedState() { + tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, + resources.getDrawable(R.drawable.ic_connected, null), null) + } + + private fun showDisconnectedState() { + tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, + resources.getDrawable(R.drawable.ic_disconnected, null), null) } /** @@ -385,18 +409,4 @@ class ImportBrainkeyFragment : BaseAccountFragment() { mHandler.postDelayed(this, Constants.BLOCK_PERIOD) } } - - override fun onServiceDisconnected(name: ComponentName?) { - super.onServiceDisconnected(name) - - tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, - resources.getDrawable(R.drawable.ic_disconnected, null), null) - } - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - super.onServiceConnected(name, service) - - tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, - resources.getDrawable(R.drawable.ic_connected, null), null) - } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt index d4b4206..296d04e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt @@ -3,7 +3,6 @@ package cy.agorise.bitsybitshareswallet.fragments import android.content.* import android.os.Bundle import android.os.Handler -import android.os.IBinder import android.preference.PreferenceManager import android.util.Log import android.view.LayoutInflater @@ -190,6 +189,15 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter } } + override fun onStart() { + super.onStart() + + if (mNetworkService?.isConnected == true) + showConnectedState() + else + showDisconnectedState() + } + override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) { if (responseMap.containsKey(response.id)) { when (responseMap[response.id]) { @@ -201,7 +209,26 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter } } - override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) { } + override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) { + when (connectionStatusUpdate.updateCode) { + ConnectionStatusUpdate.CONNECTED -> { + showConnectedState() + } + ConnectionStatusUpdate.DISCONNECTED -> { + showDisconnectedState() + } + } + } + + private fun showConnectedState() { + tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, + resources.getDrawable(R.drawable.ic_connected, null), null) + } + + private fun showDisconnectedState() { + tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, + resources.getDrawable(R.drawable.ic_disconnected, null), null) + } /** Handles the result of the [GetDynamicGlobalProperties] api call to obtain the current block number and update * it in the Nodes Dialog */ @@ -531,19 +558,5 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter activity?.finish() activity?.startActivity(intent) } - - override fun onServiceDisconnected(name: ComponentName?) { - super.onServiceDisconnected(name) - - tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, - resources.getDrawable(R.drawable.ic_disconnected, null), null) - } - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - super.onServiceConnected(name, service) - - tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, - resources.getDrawable(R.drawable.ic_connected, null), null) - } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/network/NetworkServiceManager.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/NetworkServiceManager.kt new file mode 100644 index 0000000..8e227b0 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/NetworkServiceManager.kt @@ -0,0 +1,72 @@ +package cy.agorise.bitsybitshareswallet.network + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import android.os.Handler +import cy.agorise.graphenej.api.android.NetworkService +import cy.agorise.graphenej.stats.ExponentialMovingAverage + +/** + * Class used to manage the connection status of the NetworkService. + * + * + * The basic idea here is to keep track of the sequence of activity life cycle callbacks so that we + * can infer when the user has left the app and the node connection can be salfely shut down. + */ +class NetworkServiceManager(nodes: List) : + ActivityLifecycleCallbacks { + /** + * Handler instance used to schedule tasks back to the main thread + */ + private val mHandler = Handler() + private var mNetworkService: NetworkService? = null + private val mNodeUrls: Array = nodes.toTypedArray() + /** + * Runnable used to schedule a service disconnection once the app is not visible to the user for + * more than DISCONNECT_DELAY milliseconds. + */ + private val mDisconnectRunnable = Runnable { + if (mNetworkService != null) { + mNetworkService?.stop() + mNetworkService = null + } + } + + override fun onActivityCreated(activity: Activity, bundle: Bundle) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity?) { + mHandler.removeCallbacks(mDisconnectRunnable) + if (mNetworkService == null) { + mNetworkService = NetworkService.getInstance() + mNetworkService?.start(mNodeUrls, ExponentialMovingAverage.DEFAULT_ALPHA) + } + } + + override fun onActivityPaused(activity: Activity) { + mHandler.postDelayed( + mDisconnectRunnable, + DISCONNECT_DELAY.toLong() + ) + } + + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState( + activity: Activity, + bundle: Bundle + ) { + } + + override fun onActivityDestroyed(activity: Activity) {} + + companion object { + /** + * Constant used to specify how long will the app wait for another activity to go through its starting life + * cycle events before running the teardownConnectionTask task. + * + * This is used as a means to detect whether or not the user has left the app. + */ + private const val DISCONNECT_DELAY = 1500 + } + +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/processors/TransfersLoader.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/processors/TransfersLoader.kt index 3f234fa..d6ef5ee 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/processors/TransfersLoader.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/processors/TransfersLoader.kt @@ -1,10 +1,6 @@ package cy.agorise.bitsybitshareswallet.processors -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder import android.preference.PreferenceManager import android.util.Log import cy.agorise.bitsybitshareswallet.database.entities.Transfer @@ -29,6 +25,7 @@ import org.bitcoinj.core.DumpedPrivateKey import org.bitcoinj.core.ECKey import java.util.* import javax.crypto.AEADBadTagException +import kotlin.math.floor /** * This class is responsible for loading the local database with all past transfer operations of the @@ -66,33 +63,14 @@ class TransfersLoader(private var mContext: Context?) { private var authorityRepository: AuthorityRepository? = null /* Network service connection */ - private var mNetworkService: NetworkService? = null + private var mNetworkService: NetworkService? = NetworkService.getInstance() /* Counter used to keep track of the transfer history batch count */ private var historicalTransferCount = 0 - /** Flag used to keep track of the NetworkService binding state */ - private var mBound: Boolean = false - // Map used to keep track of request and response id pairs private val responseMap = HashMap() - private val mConnection = object : ServiceConnection { - 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 - mBound = true - - // Start the transfers update - startTransfersUpdateProcedure() - } - - override fun onServiceDisconnected(name: ComponentName?) { - mBound = false - } - } - init { transferRepository = TransferRepository(mContext!!) authorityRepository = AuthorityRepository(mContext!!) @@ -138,14 +116,8 @@ class TransfersLoader(private var mContext: Context?) { } ) - onStart() - } - } - - private fun onStart() { - // Bind to LocalService - Intent(mContext, NetworkService::class.java).also { intent -> - mContext?.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) + // Start the transfers update + startTransfersUpdateProcedure() } } @@ -165,7 +137,7 @@ class TransfersLoader(private var mContext: Context?) { 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 / + historicalTransferCount = floor((transferCount / HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt() } // Retrieving account transactions @@ -257,10 +229,10 @@ class TransfersLoader(private var mContext: Context?) { val memo = op.memo if (memo.byteMessage != null) { try { - if (memo.destination.equals(myAddress)) { + if (memo.destination == myAddress) { val decryptedMessage = Memo.decryptMessage(memoKey, memo.source, memo.nonce, memo.byteMessage) memo.plaintextMessage = decryptedMessage - }else if(memo.source.equals(myAddress)){ + }else if(memo.source == myAddress){ val decryptedMessage = Memo.decryptMessage(memoKey, memo.destination, memo.nonce, memo.byteMessage) memo.plaintextMessage = decryptedMessage } @@ -271,7 +243,7 @@ class TransfersLoader(private var mContext: Context?) { Log.e(TAG, "NullPointerException. Msg: " + e.message) } catch (e: Exception) { Log.e(TAG, "Exception while decoding memo. Msg: " + e.message) - Log.e(TAG,"Exception type: " + e) + Log.e(TAG, "Exception type: $e") for(element in e.stackTrace){ Log.e(TAG, String.format("%s#%s:%d", element.className, element.methodName, element.lineNumber)) } @@ -300,13 +272,6 @@ class TransfersLoader(private var mContext: Context?) { Log.d(TAG, "Destroying TransfersLoader") if (!mDisposables.isDisposed) mDisposables.dispose() - try { - if (mBound && mNetworkService != null) - mContext?.unbindService(mConnection) - } catch (e: IllegalArgumentException) { - Log.d(TAG, "Avoid crash related to Service not registered: ${e.message}") - } - mBound = false mContext = null mNetworkService = null } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/NodeRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/NodeRepository.kt index abaaafa..86014be 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/NodeRepository.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/NodeRepository.kt @@ -50,7 +50,6 @@ class NodeRepository(private val nodeDao: NodeDao) { suspend fun getFormattedNodes(): Pair { val nodes = nodeDao.getSortedNodes() - // TODO verify if this is the best way to fire and forget launch a coroutine inside another coroutine // Launches a job to refresh the list of nodes into the database, without blocking the // execution of this function, so that the formatted nodes can be returned immediately // without waiting until the nodes have been updated in the database. diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/BitsyApplication.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/BitsyApplication.kt index 49c3f04..8a3a504 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/BitsyApplication.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/BitsyApplication.kt @@ -3,9 +3,8 @@ package cy.agorise.bitsybitshareswallet.utils import android.app.Application import com.crashlytics.android.Crashlytics import cy.agorise.bitsybitshareswallet.database.BitsyDatabase +import cy.agorise.bitsybitshareswallet.network.NetworkServiceManager import cy.agorise.bitsybitshareswallet.repositories.NodeRepository -import cy.agorise.graphenej.api.ApiAccess -import cy.agorise.graphenej.api.android.NetworkServiceManager import io.reactivex.plugins.RxJavaPlugins import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -46,18 +45,9 @@ class BitsyApplication : Application() { } private suspend fun startNetworkServiceConnection() { - // Specifying some important information regarding the connection, such as the - // credentials and the requested API accesses - val requestedApis = ApiAccess.API_DATABASE or ApiAccess.API_HISTORY or ApiAccess.API_NETWORK_BROADCAST val (nodes, autoConnect) = mNodeRepository.getFormattedNodes() - val networkManager = NetworkServiceManager.Builder() - .setUserName("") - .setPassword("") - .setRequestedApis(requestedApis) - .setCustomNodeUrls(nodes) - .setAutoConnect(autoConnect) - .setNodeLatencyVerification(true) - .build(this) + + val networkManager = NetworkServiceManager(nodes.split(",")) /* * Registering this class as a listener to all activity's callback cycle events, in order to * better estimate when the user has left the app and it is safe to disconnect the websocket connection diff --git a/graphenejlib b/graphenejlib index 1129a92..3d5a57f 160000 --- a/graphenejlib +++ b/graphenejlib @@ -1 +1 @@ -Subproject commit 1129a92aa3e4d1ceeac6826fcfe154bf909930fe +Subproject commit 3d5a57f8961c76fc2cadb327c86374d30c6692b7