bitsy-wallet/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt

642 lines
26 KiB
Kotlin

package cy.agorise.bitsybitshareswallet.fragments
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.collection.LongSparseArray
import androidx.core.content.edit
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.customListAdapter
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import com.google.common.primitives.UnsignedLong
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.BuildConfig
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.FullNodesAdapter
import cy.agorise.bitsybitshareswallet.databinding.FragmentSettingsBinding
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
import cy.agorise.bitsybitshareswallet.viewmodels.SettingsFragmentViewModel
import cy.agorise.graphenej.*
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.BroadcastTransaction
import cy.agorise.graphenej.api.calls.GetAccounts
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
import cy.agorise.graphenej.models.DynamicGlobalProperties
import cy.agorise.graphenej.models.JsonRpcResponse
import cy.agorise.graphenej.operations.AccountUpgradeOperationBuilder
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.bitcoinj.core.DumpedPrivateKey
import org.bitcoinj.core.ECKey
import java.text.NumberFormat
import javax.crypto.AEADBadTagException
class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatternEnteredListener {
companion object {
private const val TAG = "SettingsFragment"
// Constants used to perform security locked requests
private const val ACTION_CHANGE_SECURITY_LOCK = 1
private const val ACTION_SHOW_BRAINKEY = 2
private const val ACTION_UPGRADE_TO_LTM = 3
private const val ACTION_REMOVE_ACCOUNT = 4
// Constants used to organize NetworkService requests
private const val RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_NODES = 1
private const val RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_LTM = 2
private const val RESPONSE_BROADCAST_TRANSACTION = 3
}
private val viewModel: SettingsFragmentViewModel by viewModels()
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private var mUserAccount: UserAccount? = null
private var privateKey: String? = null
// Dialog displaying the list of nodes and their latencies
private var mNodesDialog: MaterialDialog? = null
// NodesDialog's RecyclerView LayoutManager used to always keep showing the first node of the list.
private var mNodesDialogLinearLayoutManager: LinearLayoutManager? = null
/** Adapter that holds the FullNode list used in the Bitshares nodes modal */
private var nodesAdapter: FullNodesAdapter? = null
// Map used to keep track of request and response id pairs
private val responseMap = LongSparseArray<Int>()
/** Transaction to upgrade to LTM */
private var ltmTransaction: Transaction? = null
private val mHandler = Handler()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
// Make sure the toolbar show the correct colors in both day and night modes
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.setBackgroundResource(if (!nightMode) R.color.colorPrimary else R.color.colorToolbarDark)
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val crashlytics = FirebaseCrashlytics.getInstance()
crashlytics.setCustomKey(Constants.CRASHLYTICS_KEY_LAST_SCREEN, TAG)
val userId = PreferenceManager.getDefaultSharedPreferences(context)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
// Configure ViewModel
viewModel.getUserAccount(userId).observe(viewLifecycleOwner, { userAccount ->
if (userAccount != null) {
mUserAccount = UserAccount(userAccount.id, userAccount.name)
binding.btnUpgradeToLTM.isEnabled =
!userAccount.isLtm // Disable button if already LTM
}
})
viewModel.getWIF(userId, AuthorityType.ACTIVE.ordinal)
.observe(viewLifecycleOwner, { encryptedWIF ->
context?.let {
try {
privateKey = CryptoUtils.decrypt(it, encryptedWIF)
} catch (e: AEADBadTagException) {
Log.e(
TAG,
"AEADBadTagException. Class: " + e.javaClass + ", Msg: " + e.message
)
} catch (e: IllegalStateException) {
crashlytics.recordException(e)
}
}
})
initAutoCloseSwitch()
initNightModeSwitch()
binding.tvNetworkStatus.setOnClickListener { v -> showNodesDialog(v) }
// Obtain the current Security Lock Option selected and display it in the screen
val securityLockSelected = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(Constants.KEY_SECURITY_LOCK_SELECTED, 0)
// Security Lock Options
// 0 -> None
// 1 -> PIN
// 2 -> Pattern
binding.tvSecurityLockSelected.text =
resources.getStringArray(R.array.security_lock_options)[securityLockSelected]
binding.tvSecurityLock.setOnClickListener { onSecurityLockTextSelected() }
binding.tvSecurityLockSelected.setOnClickListener { onSecurityLockTextSelected() }
binding.btnViewBrainKey.setOnClickListener { onShowBrainKeyButtonSelected() }
val lastAccountBackup = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(Constants.KEY_LAST_ACCOUNT_BACKUP, 0L)
val now = System.currentTimeMillis()
if (lastAccountBackup + Constants.ACCOUNT_BACKUP_PERIOD < now)
binding.tvBackupWarning.visibility = View.VISIBLE
binding.btnUpgradeToLTM.setOnClickListener { onUpgradeToLTMButtonSelected() }
binding.btnRemoveAccount.setOnClickListener { onRemoveAccountButtonSelected() }
}
private fun showNodesDialog(v: View) {
if (mNetworkService != null) {
val fullNodes = mNetworkService!!.nodes
nodesAdapter = FullNodesAdapter(v.context)
nodesAdapter?.add(fullNodes)
// PublishSubject used to announce full node latencies updates
val fullNodePublishSubject = mNetworkService?.nodeLatencyObservable ?: return
val nodesDisposable = fullNodePublishSubject
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ fullNode ->
mNodesDialogLinearLayoutManager?.scrollToPositionWithOffset(0, 0)
if (!fullNode.isRemoved)
nodesAdapter?.add(fullNode)
else
nodesAdapter?.remove(fullNode)
}, {
Log.e(TAG, "nodeLatencyObserver.onError.Msg: " + it.message)
}
)
mNodesDialogLinearLayoutManager = LinearLayoutManager(v.context)
mNodesDialog = MaterialDialog(v.context).show {
title(
text = String.format(
"%s v%s",
getString(R.string.app_name),
BuildConfig.VERSION_NAME
)
)
message(text = getString(R.string.title__bitshares_nodes_dialog, "-------"))
customListAdapter(nodesAdapter as FullNodesAdapter, mNodesDialogLinearLayoutManager)
negativeButton(android.R.string.ok)
onDismiss {
mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask)
nodesDisposable.dispose()
}
}
// Registering a recurrent task used to poll for dynamic global properties requests
mHandler.post(mRequestDynamicGlobalPropertiesTask)
}
}
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]) {
RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_NODES -> handleDynamicGlobalPropertiesNodes(
response.result
)
RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_LTM -> handleDynamicGlobalPropertiesLTM(
response.result
)
RESPONSE_BROADCAST_TRANSACTION -> handleBroadcastTransaction(response)
}
responseMap.remove(response.id)
}
}
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
when (connectionStatusUpdate.updateCode) {
ConnectionStatusUpdate.CONNECTED -> {
showConnectedState()
}
ConnectionStatusUpdate.DISCONNECTED -> {
showDisconnectedState()
}
}
}
private fun showConnectedState() {
binding.tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null,
ResourcesCompat.getDrawable(resources, R.drawable.ic_connected, null), null
)
}
private fun showDisconnectedState() {
binding.tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null,
ResourcesCompat.getDrawable(resources, 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 */
private fun handleDynamicGlobalPropertiesNodes(result: Any?) {
if (result is DynamicGlobalProperties) {
if (mNodesDialog != null && mNodesDialog?.isShowing == true) {
val blockNumber = NumberFormat.getInstance().format(result.head_block_number)
mNodesDialog?.message(
text = getString(R.string.title__bitshares_nodes_dialog, blockNumber)
)
}
}
}
private fun handleDynamicGlobalPropertiesLTM(result: Any?) {
if (result is DynamicGlobalProperties) {
val expirationTime = (result.time.time / 1000) + Transaction.DEFAULT_EXPIRATION_TIME
val headBlockId = result.head_block_id
val headBlockNumber = result.head_block_number
ltmTransaction?.blockData = BlockData(headBlockNumber, headBlockId, expirationTime)
val id = mNetworkService?.sendMessage(
BroadcastTransaction(ltmTransaction),
BroadcastTransaction.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_BROADCAST_TRANSACTION)
// TODO use an indicator to show that a transaction is in progress
}
}
/** Handles the result of the [BroadcastTransaction] api call to find out if the Transaction to upgrade the
* current account to LTM was successful or not */
private fun handleBroadcastTransaction(message: JsonRpcResponse<*>) {
if (message.result == null && message.error == null) {
// Looks like the upgrade to LTM was successful, we need to update the current account information from
// the blockchain and show a success dialog
mNetworkService?.sendMessage(GetAccounts(mUserAccount), GetAccounts.REQUIRED_API)
context?.let { context ->
MaterialDialog(context).show {
title(R.string.title__account_upgraded)
message(R.string.msg__account_upgraded)
positiveButton(android.R.string.ok)
}
}
} else if (message.error != null) {
// The upgrade to LTM wasn't successful, show a dialog to the user explaining the situation
context?.let { context ->
MaterialDialog(context).show {
title(R.string.title__upgrade_account_error)
message(R.string.msg__upgrade_account_error)
positiveButton(android.R.string.ok)
}
}
}
}
/**
* Task used to obtain frequent updates on the global dynamic properties object
*/
private val mRequestDynamicGlobalPropertiesTask = object : Runnable {
override fun run() {
val id = mNetworkService?.sendMessage(
GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_NODES)
mHandler.postDelayed(this, Constants.BLOCK_PERIOD)
}
}
/**
* Fetches the relevant preference from the SharedPreferences and configures the corresponding switch accordingly,
* and adds a listener to the said switch to store the preference in case the user changes it.
*/
private fun initAutoCloseSwitch() {
val autoCloseOn = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, true)
binding.switchAutoClose.isChecked = autoCloseOn
binding.switchAutoClose.setOnCheckedChangeListener { buttonView, isChecked ->
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
.putBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, isChecked).apply()
}
}
/**
* Fetches the relevant preference from the SharedPreferences and configures the corresponding switch accordingly,
* and adds a listener to the said switch to store the preference in case the user changes it. Also makes a call to
* recreate the activity and apply the selected theme.
*/
private fun initNightModeSwitch() {
val nightModeOn = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
binding.switchNightMode.isChecked = nightModeOn
binding.switchNightMode.setOnCheckedChangeListener { buttonView, isChecked ->
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
.putBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, isChecked).apply()
// Recreates the activity to apply the selected theme
activity?.recreate()
}
}
private fun onSecurityLockTextSelected() {
if (!verifySecurityLock(ACTION_CHANGE_SECURITY_LOCK))
showChooseSecurityLockDialog()
}
/**
* Encapsulates the logic required to do actions possibly locked by the Security Lock. If PIN/Pattern is selected
* then it prompts for it.
*
* @param actionIdentifier Identifier used to know why a verify security lock was launched
* @return true if the action was handled, false otherwise
*/
private fun verifySecurityLock(actionIdentifier: Int): Boolean {
val securityLockSelected = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(Constants.KEY_SECURITY_LOCK_SELECTED, 0)
// Security Lock Options
// 0 -> None
// 1 -> PIN
// 2 -> Pattern
// Args used for both PIN and Pattern options
val args = Bundle()
args.putInt(
BaseSecurityLockDialog.KEY_STEP_SECURITY_LOCK,
BaseSecurityLockDialog.STEP_SECURITY_LOCK_VERIFY
)
args.putInt(BaseSecurityLockDialog.KEY_ACTION_IDENTIFIER, actionIdentifier)
return when (securityLockSelected) {
0 -> { /* None */
false
}
1 -> { /* PIN */
val pinFrag = PINSecurityLockDialog()
pinFrag.arguments = args
pinFrag.show(childFragmentManager, "pin_security_lock_tag")
true
}
else -> { /* Pattern */
val patternFrag = PatternSecurityLockDialog()
patternFrag.arguments = args
patternFrag.show(childFragmentManager, "pattern_security_lock_tag")
true
}
}
}
override fun onPINPatternEntered(actionIdentifier: Int) {
when (actionIdentifier) {
ACTION_CHANGE_SECURITY_LOCK -> showChooseSecurityLockDialog()
ACTION_SHOW_BRAINKEY -> getBrainkey()
ACTION_UPGRADE_TO_LTM -> showUpgradeToLTMDialog()
ACTION_REMOVE_ACCOUNT -> showRemoveAccountDialog()
}
}
override fun onPINPatternChanged() {
// Obtain the new Security Lock Option selected and display it in the screen
val securityLockSelected = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(Constants.KEY_SECURITY_LOCK_SELECTED, 0)
// Security Lock Options
// 0 -> None
// 1 -> PIN
// 2 -> Pattern
binding.tvSecurityLockSelected.text =
resources.getStringArray(R.array.security_lock_options)[securityLockSelected]
}
/**
* Shows a dialog so the user can select its desired Security Lock option.
*/
private fun showChooseSecurityLockDialog() {
// Obtain the current Security Lock Option selected and display it in the screen
val securityLockSelected = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(Constants.KEY_SECURITY_LOCK_SELECTED, 0)
// Security Lock Options
// 0 -> None
// 1 -> PIN
// 2 -> Pattern
context?.let {
MaterialDialog(it).show {
title(R.string.title__security_dialog)
listItemsSingleChoice(
R.array.security_lock_options,
initialSelection = securityLockSelected
) { _, index, _ ->
// Args used for both PIN and Pattern options
val args = Bundle()
args.putInt(
BaseSecurityLockDialog.KEY_STEP_SECURITY_LOCK,
BaseSecurityLockDialog.STEP_SECURITY_LOCK_CREATE
)
args.putInt(BaseSecurityLockDialog.KEY_ACTION_IDENTIFIER, -1)
when (index) {
0 -> { /* None */
PreferenceManager.getDefaultSharedPreferences(context).edit {
putInt(Constants.KEY_SECURITY_LOCK_SELECTED, 0) // 0 -> None
}
// Call this function to update the UI
onPINPatternChanged()
}
1 -> { /* PIN */
val pinFrag = PINSecurityLockDialog()
pinFrag.arguments = args
pinFrag.show(childFragmentManager, "pin_security_lock_tag")
}
else -> { /* Pattern */
val patternFrag = PatternSecurityLockDialog()
patternFrag.arguments = args
patternFrag.show(childFragmentManager, "pattern_security_lock_tag")
}
}
}
}
}
}
private fun onShowBrainKeyButtonSelected() {
if (!verifySecurityLock(ACTION_SHOW_BRAINKEY))
getBrainkey()
}
private fun onUpgradeToLTMButtonSelected() {
if (!verifySecurityLock(ACTION_UPGRADE_TO_LTM))
showUpgradeToLTMDialog()
}
private fun onRemoveAccountButtonSelected() {
if (!verifySecurityLock(ACTION_REMOVE_ACCOUNT))
showRemoveAccountDialog()
}
/**
* Obtains the brainKey from the authorities db table for the current user account and if it is not null it passes
* the brainKey to a method to show it in a nice MaterialDialog
*/
private fun getBrainkey() {
context?.let {
val userId = PreferenceManager.getDefaultSharedPreferences(it)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
val authorityRepository = AuthorityRepository(it)
mDisposables.add(authorityRepository.get(userId)
.subscribeOn(Schedulers.io())
.map { authority ->
val plainBrainKey = CryptoUtils.decrypt(it, authority.encryptedBrainKey)
val plainSequenceNumber =
CryptoUtils.decrypt(it, authority.encryptedSequenceNumber)
val sequenceNumber = Integer.parseInt(plainSequenceNumber)
BrainKey(plainBrainKey, sequenceNumber)
}
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { brainkey ->
showBrainKeyDialog(brainkey)
}
)
}
}
/**
* Shows the plain brainkey in a dialog so that the user can view and Copy it.
*/
private fun showBrainKeyDialog(brainKey: BrainKey) {
context?.let { context ->
val dialog = MaterialDialog(context)
.title(text = "BrainKey")
.message(text = brainKey.brainKey)
.customView(R.layout.dialog_copy_brainkey)
.cancelable(false)
.positiveButton(R.string.button__copied) {
val now = System.currentTimeMillis()
PreferenceManager.getDefaultSharedPreferences(it.context).edit {
putLong(Constants.KEY_LAST_ACCOUNT_BACKUP, now)
}
binding.tvBackupWarning.visibility = View.GONE
}
dialog.show()
}
}
private fun showUpgradeToLTMDialog() {
context?.let { context ->
val content = getString(R.string.msg__account_upgrade_dialog, mUserAccount?.name)
MaterialDialog(context).show {
message(text = content)
negativeButton(android.R.string.cancel)
positiveButton(android.R.string.ok) {
val operation = AccountUpgradeOperationBuilder()
.setIsUpgrade(true)
.setFee(AssetAmount(UnsignedLong.ZERO, Asset("1.3.0"))) // 0 BTS
.setAccountToUpgrade(mUserAccount).build()
val operations = ArrayList<BaseOperation>()
operations.add(operation)
val currentPrivateKey = ECKey.fromPrivate(
DumpedPrivateKey.fromBase58(null, privateKey).key.privKeyBytes
)
ltmTransaction = Transaction(currentPrivateKey, null, operations)
val id = mNetworkService?.sendMessage(
GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API
)
if (id != null) responseMap.append(
id,
RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_LTM
)
}
}
}
}
private fun showRemoveAccountDialog() {
context?.let { context ->
MaterialDialog(context).show {
title(R.string.title__remove_account)
message(R.string.msg__remove_account_confirmation)
negativeButton(android.R.string.cancel)
positiveButton(android.R.string.ok) {
removeAccount(it.context)
}
}
}
}
private fun removeAccount(context: Context) {
// Clears the database.
viewModel.clearDatabase(context)
val pref = PreferenceManager.getDefaultSharedPreferences(context).edit {
// Clears the shared preferences.
clear()
// Marks the license as agreed, so that it is not shown to the user again.
putInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION)
}
// Restarts the activity, which will restart the whole application since it uses a
// single activity architecture.
val intent = activity?.intent
activity?.finish()
activity?.startActivity(intent)
}
}