601 lines
25 KiB
Kotlin
601 lines
25 KiB
Kotlin
package cy.agorise.bitsybitshareswallet.fragments
|
|
|
|
import android.Manifest
|
|
import android.content.pm.PackageManager
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.preference.PreferenceManager
|
|
import android.util.Log
|
|
import android.view.*
|
|
import android.widget.AdapterView
|
|
import android.widget.Toast
|
|
import androidx.appcompat.widget.Toolbar
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.lifecycle.Observer
|
|
import androidx.lifecycle.ViewModelProviders
|
|
import androidx.navigation.fragment.findNavController
|
|
import androidx.navigation.fragment.navArgs
|
|
import com.afollestad.materialdialogs.MaterialDialog
|
|
import com.afollestad.materialdialogs.customview.customView
|
|
import com.crashlytics.android.Crashlytics
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import com.google.common.primitives.UnsignedLong
|
|
import com.google.zxing.BarcodeFormat
|
|
import com.google.zxing.Result
|
|
import com.jakewharton.rxbinding3.widget.textChanges
|
|
import cy.agorise.bitsybitshareswallet.R
|
|
import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
|
|
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
|
import cy.agorise.bitsybitshareswallet.utils.*
|
|
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
|
|
import cy.agorise.bitsybitshareswallet.viewmodels.SendTransactionViewModel
|
|
import cy.agorise.graphenej.*
|
|
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
|
import cy.agorise.graphenej.api.calls.BroadcastTransaction
|
|
import cy.agorise.graphenej.api.calls.GetAccountByName
|
|
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
|
|
import cy.agorise.graphenej.api.calls.GetRequiredFees
|
|
import cy.agorise.graphenej.crypto.SecureRandomGenerator
|
|
import cy.agorise.graphenej.models.AccountProperties
|
|
import cy.agorise.graphenej.models.DynamicGlobalProperties
|
|
import cy.agorise.graphenej.models.JsonRpcResponse
|
|
import cy.agorise.graphenej.operations.TransferOperation
|
|
import cy.agorise.graphenej.operations.TransferOperationBuilder
|
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
import kotlinx.android.synthetic.main.fragment_send_transaction.*
|
|
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
|
import org.bitcoinj.core.DumpedPrivateKey
|
|
import org.bitcoinj.core.ECKey
|
|
import java.math.RoundingMode
|
|
import java.text.DecimalFormat
|
|
import java.text.DecimalFormatSymbols
|
|
import java.util.ArrayList
|
|
import java.util.Locale
|
|
import java.util.concurrent.TimeUnit
|
|
import javax.crypto.AEADBadTagException
|
|
|
|
class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHandler,
|
|
BaseSecurityLockDialog.OnPINPatternEnteredListener {
|
|
|
|
companion object {
|
|
private const val TAG = "SendTransactionFragment"
|
|
|
|
// Camera Permission
|
|
private const val REQUEST_CAMERA_PERMISSION = 1
|
|
|
|
// Constants used to organize NetworkService requests
|
|
private const val RESPONSE_GET_ACCOUNT_BY_NAME = 1
|
|
private const val RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES = 2
|
|
private const val RESPONSE_GET_REQUIRED_FEES = 3
|
|
private const val RESPONSE_BROADCAST_TRANSACTION = 4
|
|
|
|
// Constant used to perform security locked requests
|
|
private const val ACTION_SEND_TRANSFER = 1
|
|
}
|
|
|
|
// Navigation AAC Safe Args
|
|
private val args: SendTransactionFragmentArgs by navArgs()
|
|
|
|
private lateinit var mViewModel: SendTransactionViewModel
|
|
|
|
/** Variables used in field's validation */
|
|
private var isCameraPreviewVisible = false
|
|
private var isToAccountCorrect = false
|
|
private var isAmountCorrect = false
|
|
|
|
private var mBalancesDetails = ArrayList<BalanceDetail>()
|
|
|
|
private lateinit var mBalanceDetailViewModel: BalanceDetailViewModel
|
|
|
|
private var mBalancesDetailsAdapter: BalancesDetailsAdapter? = null
|
|
|
|
/** Keeps track of the asset's symbol selected in the Asset spinner */
|
|
private var selectedAssetSymbol = "BTS"
|
|
|
|
/** Current user account */
|
|
private var mUserAccount: UserAccount? = null
|
|
|
|
/** User account to which send the funds */
|
|
private var mSelectedUserAccount: UserAccount? = null
|
|
|
|
// Map used to keep track of request and response id pairs
|
|
private val responseMap = HashMap<Long, Int>()
|
|
|
|
/** Transaction being built */
|
|
private var transaction: Transaction? = null
|
|
|
|
/** Variable holding the current user's private key in the WIF format */
|
|
private var wifKey: String? = null
|
|
|
|
/** This is one of the recipient account's public key, it will be used for memo encoding */
|
|
private var destinationPublicKey: PublicKey? = null
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
setHasOptionsMenu(true)
|
|
|
|
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
|
|
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
|
|
|
|
// Sets the toolbar background color to red
|
|
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
|
|
toolbar?.setBackgroundResource(if (!nightMode) R.color.colorSend else R.color.colorToolbarDark)
|
|
|
|
// Sets the status and navigation bars background color to a dark red or just dark
|
|
val window = activity?.window
|
|
context?.let { context ->
|
|
val statusBarColor = ContextCompat.getColor(context,
|
|
if (!nightMode) R.color.colorSendDark else R.color.colorStatusBarDark)
|
|
window?.statusBarColor = statusBarColor
|
|
window?.navigationBarColor = statusBarColor
|
|
}
|
|
|
|
return inflater.inflate(R.layout.fragment_send_transaction, container, false)
|
|
}
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
Crashlytics.setString(Constants.CRASHLYTICS_KEY_LAST_SCREEN, TAG)
|
|
|
|
val userId = PreferenceManager.getDefaultSharedPreferences(context)
|
|
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
|
|
|
|
if (userId != "")
|
|
mUserAccount = UserAccount(userId)
|
|
|
|
// Configure ViewModel
|
|
mViewModel= ViewModelProviders.of(this).get(SendTransactionViewModel::class.java)
|
|
|
|
mViewModel.getWIF(userId, AuthorityType.ACTIVE.ordinal).observe(this,
|
|
androidx.lifecycle.Observer<String> { encryptedWIF ->
|
|
context?.let {
|
|
try {
|
|
wifKey = CryptoUtils.decrypt(it, encryptedWIF)
|
|
} catch (e: AEADBadTagException) {
|
|
Log.e(TAG, "AEADBadTagException. Class: " + e.javaClass + ", Msg: " + e.message)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Use Navigation SafeArgs to decide if we should activate or not the camera feed
|
|
if (args.openCamera) {
|
|
// Delay the camera action to avoid flicker in the fragment transition
|
|
Handler().postDelayed({
|
|
run { verifyCameraPermission() }
|
|
}, 500)
|
|
}
|
|
|
|
fabOpenCamera.setOnClickListener { if (isCameraPreviewVisible) stopCameraPreview() else verifyCameraPermission() }
|
|
|
|
// Configure BalanceDetailViewModel to show the current balances
|
|
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
|
|
|
|
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
|
|
mBalancesDetails.clear()
|
|
mBalancesDetails.addAll(balancesDetails)
|
|
mBalancesDetails.sortWith(
|
|
Comparator { a, b -> a.toString().compareTo(b.toString(), true) }
|
|
)
|
|
mBalancesDetailsAdapter = BalancesDetailsAdapter(context!!, android.R.layout.simple_spinner_item, mBalancesDetails)
|
|
spAsset.adapter = mBalancesDetailsAdapter
|
|
|
|
// Try to select the selectedAssetSymbol
|
|
for (i in 0 until mBalancesDetailsAdapter!!.count) {
|
|
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == selectedAssetSymbol) {
|
|
spAsset.setSelection(i)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
spAsset.onItemSelectedListener = assetItemSelectedListener
|
|
|
|
fabSendTransaction.setOnClickListener { verifySecurityLockSendTransfer() }
|
|
fabSendTransaction.disable(R.color.lightGray)
|
|
|
|
// Use RxJava Debounce to avoid making calls to the NetworkService on every text change event
|
|
mDisposables.add(
|
|
tietTo.textChanges()
|
|
.skipInitialValue()
|
|
.debounce(500, TimeUnit.MILLISECONDS)
|
|
.map { it.toString().trim() }
|
|
.subscribe {
|
|
val id = mNetworkService?.sendMessage(GetAccountByName(it!!), GetAccountByName.REQUIRED_API)
|
|
if (id != null) responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME
|
|
}
|
|
)
|
|
|
|
// Use RxJava Debounce to update the Amount error only after the user stops writing for > 500 ms
|
|
mDisposables.add(
|
|
tietAmount.textChanges()
|
|
.skipInitialValue()
|
|
.debounce(500, TimeUnit.MILLISECONDS)
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.subscribe { validateAmount() }
|
|
)
|
|
}
|
|
|
|
/** Handles the selection of items in the Asset spinner, to keep track of the selectedAssetSymbol and show the
|
|
* current user's balance of the selected asset. */
|
|
private val assetItemSelectedListener = object : AdapterView.OnItemSelectedListener{
|
|
override fun onNothingSelected(parent: AdapterView<*>?) { }
|
|
|
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
val balance = mBalancesDetailsAdapter!!.getItem(position)!!
|
|
selectedAssetSymbol = balance.symbol
|
|
|
|
val amount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
|
|
|
|
tvAvailableAssetAmount.text =
|
|
String.format("%." + Math.min(balance.precision, 8) + "f %s", amount, balance.toString())
|
|
|
|
validateAmount()
|
|
}
|
|
}
|
|
|
|
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
|
|
if (responseMap.containsKey(response.id)) {
|
|
val responseType = responseMap[response.id]
|
|
when (responseType) {
|
|
RESPONSE_GET_ACCOUNT_BY_NAME -> handleAccountProperties(response.result)
|
|
RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES -> handleDynamicGlobalProperties(response.result)
|
|
RESPONSE_GET_REQUIRED_FEES -> handleRequiredFees(response.result)
|
|
RESPONSE_BROADCAST_TRANSACTION -> handleBroadcastTransaction(response)
|
|
}
|
|
responseMap.remove(response.id)
|
|
}
|
|
}
|
|
|
|
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
|
|
if (connectionStatusUpdate.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()
|
|
}
|
|
}
|
|
|
|
/** Handles the result of the [GetAccountByName] api call to find out if the account written in the To text
|
|
* field corresponds to an actual BitShares account or not and acts accordingly */
|
|
private fun handleAccountProperties(result: Any?) {
|
|
if (result is AccountProperties) {
|
|
mSelectedUserAccount = UserAccount(result.id, result.name)
|
|
destinationPublicKey = result.active.keyAuths.keys.iterator().next()
|
|
tilTo.isErrorEnabled = false
|
|
isToAccountCorrect = true
|
|
} else {
|
|
mSelectedUserAccount = null
|
|
destinationPublicKey = null
|
|
tilTo.error = getString(R.string.error__invalid_account)
|
|
isToAccountCorrect = false
|
|
}
|
|
|
|
enableDisableSendFAB()
|
|
}
|
|
|
|
/** Handles the result of the [GetDynamicGlobalProperties] api call to add the needed metadata to the [Transaction]
|
|
* the app is building and ultimately send, if everything is correct adds the needed info to the [Transaction] and
|
|
* calls the next step which is [GetRequiredFees] else it shows an error */
|
|
private fun handleDynamicGlobalProperties(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
|
|
|
|
transaction!!.blockData = BlockData(headBlockNumber, headBlockId, expirationTime)
|
|
|
|
val asset = Asset(mBalancesDetailsAdapter!!.getItem(spAsset.selectedItemPosition)!!.id)
|
|
|
|
val id = mNetworkService?.sendMessage(GetRequiredFees(transaction!!, asset), GetRequiredFees.REQUIRED_API)
|
|
if (id != null) responseMap[id] = RESPONSE_GET_REQUIRED_FEES
|
|
} else {
|
|
context?.toast(getString(R.string.msg__transaction_not_sent))
|
|
}
|
|
}
|
|
|
|
/** Handles the result of the [GetRequiredFees] api call to add the fees to the [Transaction] the app is building
|
|
* and ultimately send, and if everything is correct broadcasts the [Transaction] else it shows an error */
|
|
private fun handleRequiredFees(result: Any?) {
|
|
if (result is List<*> && result[0] is AssetAmount) {
|
|
Log.d(TAG, "GetRequiredFees: " + transaction.toString())
|
|
transaction!!.setFees(result as List<AssetAmount>) // TODO find how to remove this warning
|
|
|
|
val id = mNetworkService?.sendMessage(BroadcastTransaction(transaction), BroadcastTransaction.REQUIRED_API)
|
|
if (id != null) responseMap[id] = RESPONSE_BROADCAST_TRANSACTION
|
|
} else {
|
|
context?.toast(getString(R.string.msg__transaction_not_sent))
|
|
}
|
|
}
|
|
|
|
/** Handles the result of the [BroadcastTransaction] api call to find out if the Transaction was sent successfully
|
|
* or not and acts accordingly */
|
|
private fun handleBroadcastTransaction(message: JsonRpcResponse<*>) {
|
|
if (message.result == null && message.error == null) {
|
|
context?.toast(getString(R.string.text__transaction_sent))
|
|
|
|
// Return to the main screen
|
|
findNavController().navigateUp()
|
|
} else if (message.error != null) {
|
|
context?.toast(message.error.message, Toast.LENGTH_LONG)
|
|
}
|
|
}
|
|
|
|
/** Verifies if the user has already granted the Camera permission, if not the asks for it */
|
|
private fun verifyCameraPermission() {
|
|
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA)
|
|
!= PackageManager.PERMISSION_GRANTED) {
|
|
// Permission is not already granted
|
|
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)
|
|
} else {
|
|
// Permission is already granted
|
|
startCameraPreview()
|
|
}
|
|
}
|
|
|
|
/** Handles the result from the camera permission request */
|
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
|
|
if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
|
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
|
startCameraPreview()
|
|
} else {
|
|
context?.toast(getString(R.string.msg__camera_permission_necessary))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
private fun startCameraPreview() {
|
|
cameraPreview.visibility = View.VISIBLE
|
|
fabOpenCamera.setImageResource(R.drawable.ic_close)
|
|
isCameraPreviewVisible = true
|
|
|
|
// Configure QR scanner
|
|
cameraPreview.setFormats(listOf(BarcodeFormat.QR_CODE))
|
|
cameraPreview.setAspectTolerance(0.5f)
|
|
cameraPreview.setAutoFocus(true)
|
|
cameraPreview.setLaserColor(R.color.colorAccent)
|
|
cameraPreview.setMaskColor(R.color.colorAccent)
|
|
cameraPreview.setResultHandler(this)
|
|
cameraPreview.startCamera()
|
|
|
|
cameraPreview.scrollY = holderCamera.width / 6
|
|
}
|
|
|
|
private fun stopCameraPreview() {
|
|
cameraPreview.visibility = View.INVISIBLE
|
|
fabOpenCamera.setImageResource(R.drawable.ic_camera)
|
|
isCameraPreviewVisible = false
|
|
cameraPreview.stopCamera()
|
|
}
|
|
|
|
/** Handles the result of the QR code read from the camera and tries to populate the Account, Amount and Memo fields
|
|
* and the Asset spinner with the obtained information */
|
|
override fun handleResult(result: Result?) {
|
|
try {
|
|
val invoice = Invoice.fromQrCode(result!!.text)
|
|
|
|
Log.d(TAG, "QR Code read: " + invoice.toJsonString())
|
|
|
|
tietTo.setText(invoice.to)
|
|
|
|
if (invoice.memo != null) {
|
|
tietMemo.setText(invoice.memo)
|
|
if (invoice.memo.startsWith("PP"))
|
|
tietMemo.isEnabled = false
|
|
}
|
|
|
|
var balanceDetail: BalanceDetail? = null
|
|
|
|
// Try to select the invoice's Asset in the Assets spinner
|
|
for (i in 0 until (mBalancesDetailsAdapter?.count ?: 0)) {
|
|
if (mBalancesDetailsAdapter?.getItem(i)?.symbol == invoice.currency.toUpperCase() ||
|
|
(invoice.currency.startsWith("bit", true) &&
|
|
invoice.currency.replaceFirst("bit", "").toUpperCase() ==
|
|
mBalancesDetailsAdapter?.getItem(i)?.symbol)) {
|
|
spAsset.setSelection(i)
|
|
balanceDetail = mBalancesDetailsAdapter?.getItem(i)
|
|
break
|
|
}
|
|
}
|
|
|
|
// If the user does not own any of the requested asset then show a SnackBar to explain the issue and
|
|
// return early to avoid filling the asset field
|
|
if (balanceDetail == null) {
|
|
Snackbar.make(rootView, getString(R.string.error__you_dont_own_asset, invoice.currency.toUpperCase()),
|
|
Snackbar.LENGTH_INDEFINITE).setAction(android.R.string.ok) { }.show()
|
|
return
|
|
}
|
|
|
|
var amount = 0.0
|
|
for (nextItem in invoice.lineItems) {
|
|
amount += nextItem.quantity * nextItem.price
|
|
}
|
|
|
|
val df = DecimalFormat("####." + "#".repeat(balanceDetail.precision))
|
|
df.roundingMode = RoundingMode.CEILING
|
|
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
|
tietAmount.setText(df.format(amount))
|
|
|
|
}catch (e: Exception) {
|
|
Log.d(TAG, "Invoice error: " + e.message)
|
|
}
|
|
}
|
|
|
|
private fun validateAmount() {
|
|
val txtAmount = tietAmount.text.toString()
|
|
|
|
if (mBalancesDetailsAdapter?.isEmpty != false) return
|
|
val balance = mBalancesDetailsAdapter?.getItem(spAsset.selectedItemPosition) ?: return
|
|
val currentAmount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
|
|
|
|
val amount: Double = try {
|
|
txtAmount.toDouble()
|
|
} catch (e: Exception) {
|
|
0.0
|
|
}
|
|
|
|
when {
|
|
currentAmount < amount -> {
|
|
tilAmount.error = getString(R.string.error__not_enough_funds)
|
|
isAmountCorrect = false
|
|
}
|
|
amount == 0.0 -> {
|
|
tilAmount.isErrorEnabled = false
|
|
isAmountCorrect = false
|
|
}
|
|
else -> {
|
|
tilAmount.isErrorEnabled = false
|
|
isAmountCorrect = true
|
|
}
|
|
}
|
|
|
|
enableDisableSendFAB()
|
|
}
|
|
|
|
private fun enableDisableSendFAB() {
|
|
if (isToAccountCorrect && isAmountCorrect) {
|
|
fabSendTransaction.enable(R.color.colorSend)
|
|
vSend.setBackgroundResource(R.drawable.send_fab_background)
|
|
} else {
|
|
fabSendTransaction.disable(R.color.lightGray)
|
|
vSend.setBackgroundResource(R.drawable.send_fab_background_disabled)
|
|
}
|
|
}
|
|
|
|
private fun verifySecurityLockSendTransfer() {
|
|
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, ACTION_SEND_TRANSFER)
|
|
|
|
when (securityLockSelected) {
|
|
0 -> { /* None */
|
|
startSendTransferOperation()
|
|
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPINPatternEntered(actionIdentifier: Int) {
|
|
if (actionIdentifier == ACTION_SEND_TRANSFER) {
|
|
startSendTransferOperation()
|
|
}
|
|
}
|
|
|
|
override fun onPINPatternChanged() { /* Do nothing */ }
|
|
|
|
/** Starts the Send Transfer operation procedure, creating a [TransferOperation] and sending a call to the
|
|
* NetworkService to obtain the [DynamicGlobalProperties] object needed to successfully send a Transfer */
|
|
private fun startSendTransferOperation() {
|
|
// Create TransferOperation
|
|
if (mNetworkService?.isConnected == true) {
|
|
val balance = mBalancesDetailsAdapter!!.getItem(spAsset.selectedItemPosition)!!
|
|
val amount = (tietAmount.text.toString().toDouble() * Math.pow(10.0, balance.precision.toDouble())).toLong()
|
|
|
|
val transferAmount = AssetAmount(UnsignedLong.valueOf(amount), Asset(balance.id))
|
|
|
|
val operationBuilder = TransferOperationBuilder()
|
|
.setSource(mUserAccount)
|
|
.setDestination(mSelectedUserAccount)
|
|
.setTransferAmount(transferAmount)
|
|
|
|
val privateKey = ECKey.fromPrivate(DumpedPrivateKey.fromBase58(null, wifKey).key.privKeyBytes)
|
|
|
|
// Add memo if it is not empty
|
|
val memoMsg = tietMemo.text.toString()
|
|
if (memoMsg.isNotEmpty()) {
|
|
val nonce = Math.abs(SecureRandomGenerator.getSecureRandom().nextLong()).toBigInteger()
|
|
val encryptedMemo = Memo.encryptMessage(privateKey, destinationPublicKey!!, nonce, memoMsg)
|
|
val from = Address(ECKey.fromPublicOnly(privateKey.pubKey))
|
|
val to = Address(destinationPublicKey!!.key)
|
|
val memo = Memo(from, to, nonce, encryptedMemo)
|
|
operationBuilder.setMemo(memo)
|
|
}
|
|
|
|
// Object that will contain all operations to be sent at once
|
|
val operations = ArrayList<BaseOperation>()
|
|
// Transfer from the current user to the selected one
|
|
val transferOperation = operationBuilder.build()
|
|
operations.add(transferOperation)
|
|
|
|
// Transfer operation to be sent as a fee to Agorise
|
|
val feeOperation = getAgoriseFeeOperation(transferOperation)
|
|
if (feeOperation != null && (feeOperation.assetAmount?.amount?.toLong() ?: 0L) > 0L)
|
|
operations.add(feeOperation)
|
|
|
|
transaction = Transaction(privateKey, null, operations)
|
|
|
|
// Start the send transaction procedure which includes a series of calls
|
|
val id = mNetworkService?.sendMessage(GetDynamicGlobalProperties(),
|
|
GetDynamicGlobalProperties.REQUIRED_API)
|
|
if (id != null ) responseMap[id] = RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES
|
|
} else
|
|
Log.d(TAG, "Network Service is not connected")
|
|
}
|
|
|
|
/**
|
|
* Obtains the correct [TransferOperation] object to send the fee to Agorise. A fee is only sent if the Asset is
|
|
* BTS or a SmartCoin.
|
|
*/
|
|
private fun getAgoriseFeeOperation(transferOperation: TransferOperation): TransferOperation? {
|
|
// Verify that the current Asset is either BTS or a SmartCoin
|
|
if (Constants.assetsWhichSendFeeToAgorise.contains(transferOperation.assetAmount?.asset?.objectId ?: "")) {
|
|
val fee = transferOperation.assetAmount?.multiplyBy(Constants.FEE_PERCENTAGE) ?: return null
|
|
|
|
return TransferOperationBuilder()
|
|
.setSource(mUserAccount)
|
|
.setDestination(Constants.AGORISE_ACCOUNT)
|
|
.setTransferAmount(fee)
|
|
.build()
|
|
} else
|
|
return null
|
|
}
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
inflater.inflate(R.menu.menu_send_transaction, menu)
|
|
}
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
|
if (item?.itemId == R.id.menu_info) {
|
|
MaterialDialog(context!!).show {
|
|
customView(R.layout.dialog_send_transaction_info, scrollable = true)
|
|
positiveButton(android.R.string.ok) { dismiss() }
|
|
}
|
|
return true
|
|
}
|
|
return super.onOptionsItemSelected(item)
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
|
|
if (isCameraPreviewVisible)
|
|
startCameraPreview()
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
|
|
if (!isCameraPreviewVisible)
|
|
stopCameraPreview()
|
|
}
|
|
}
|