401 lines
15 KiB
Kotlin
401 lines
15 KiB
Kotlin
package cy.agorise.bitsybitshareswallet.fragments
|
|
|
|
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.collection.LongSparseArray
|
|
import androidx.navigation.fragment.findNavController
|
|
import com.afollestad.materialdialogs.MaterialDialog
|
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|
import com.jakewharton.rxbinding3.widget.textChanges
|
|
import cy.agorise.bitsybitshareswallet.R
|
|
import cy.agorise.bitsybitshareswallet.databinding.FragmentCreateAccountBinding
|
|
import cy.agorise.bitsybitshareswallet.models.FaucetRequest
|
|
import cy.agorise.bitsybitshareswallet.models.FaucetResponse
|
|
import cy.agorise.bitsybitshareswallet.network.FaucetService
|
|
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
|
|
import cy.agorise.bitsybitshareswallet.utils.Constants
|
|
import cy.agorise.bitsybitshareswallet.utils.containsDigits
|
|
import cy.agorise.bitsybitshareswallet.utils.containsVowels
|
|
import cy.agorise.bitsybitshareswallet.utils.toast
|
|
import cy.agorise.graphenej.Address
|
|
import cy.agorise.graphenej.BrainKey
|
|
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
|
import cy.agorise.graphenej.api.calls.GetAccountByName
|
|
import cy.agorise.graphenej.models.AccountProperties
|
|
import cy.agorise.graphenej.models.JsonRpcResponse
|
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
import org.bitcoinj.core.ECKey
|
|
import retrofit2.Call
|
|
import retrofit2.Callback
|
|
import retrofit2.Response
|
|
import java.io.BufferedReader
|
|
import java.io.IOException
|
|
import java.io.InputStreamReader
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
|
|
class CreateAccountFragment : BaseAccountFragment() {
|
|
|
|
companion object {
|
|
private const val TAG = "CreateAccountFragment"
|
|
|
|
private const val BRAINKEY_FILE = "brainkeydict.txt"
|
|
private const val MIN_ACCOUNT_NAME_LENGTH = 3
|
|
private const val MAX_ACCOUNT_NAME_LENGTH = 16
|
|
|
|
// Used when trying to validate that the account name is available
|
|
private const val RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION = 1
|
|
|
|
// Used when trying to obtain the info of the newly created account
|
|
private const val RESPONSE_GET_ACCOUNT_BY_NAME_CREATED = 2
|
|
}
|
|
|
|
private var _binding: FragmentCreateAccountBinding? = null
|
|
private val binding get() = _binding!!
|
|
|
|
private lateinit var mAddress: String
|
|
|
|
/** Variables used to store the validation status of the form fields */
|
|
private var isPINValid = false
|
|
private var isPINConfirmationValid = false
|
|
private var isAccountValidAndAvailable = false
|
|
|
|
// Map used to keep track of request and response id pairs
|
|
private val responseMap = LongSparseArray<Int>()
|
|
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View {
|
|
setHasOptionsMenu(true)
|
|
|
|
_binding = FragmentCreateAccountBinding.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)
|
|
|
|
// Use RxJava Debounce to check the validity and availability of the user's proposed account name
|
|
mDisposables.add(
|
|
binding.tietAccountName.textChanges()
|
|
.skipInitialValue()
|
|
.debounce(800, TimeUnit.MILLISECONDS)
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.subscribe(
|
|
{ validateAccountName(it.toString()) },
|
|
{ crashlytics.log("D/$TAG: ${it.message}") }
|
|
)
|
|
)
|
|
|
|
// Use RxJava Debounce to update the PIN error only after the user stops writing for > 500 ms
|
|
mDisposables.add(
|
|
binding.tietPin.textChanges()
|
|
.skipInitialValue()
|
|
.debounce(500, TimeUnit.MILLISECONDS)
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.subscribe(
|
|
{ validatePIN() },
|
|
{ crashlytics.log("D/$TAG: ${it.message}") }
|
|
)
|
|
)
|
|
|
|
// Use RxJava Debounce to update the PIN Confirmation error only after the user stops writing for > 500 ms
|
|
mDisposables.add(
|
|
binding.tietPinConfirmation.textChanges()
|
|
.skipInitialValue()
|
|
.debounce(500, TimeUnit.MILLISECONDS)
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
.subscribe(
|
|
{ validatePINConfirmation() },
|
|
{ crashlytics.log("D/$TAG: ${it.message}") }
|
|
)
|
|
)
|
|
|
|
binding.btnCancel.setOnClickListener { findNavController().navigateUp() }
|
|
|
|
binding.btnCreate.isEnabled = false
|
|
binding.btnCreate.setOnClickListener { createAccount() }
|
|
|
|
// Generating BrainKey
|
|
generateKeys()
|
|
}
|
|
|
|
private fun validateAccountName(accountName: String) {
|
|
isAccountValidAndAvailable = false
|
|
|
|
if (!isAccountLengthValid(accountName)) {
|
|
binding.tilAccountName.helperText = ""
|
|
binding.tilAccountName.error = getString(R.string.error__invalid_account_length)
|
|
} else if (!isAccountStartValid(accountName)) {
|
|
binding.tilAccountName.helperText = ""
|
|
binding.tilAccountName.error = getString(R.string.error__invalid_account_start)
|
|
} else if (!isAccountNameValid(accountName)) {
|
|
binding.tilAccountName.helperText = ""
|
|
binding.tilAccountName.error = getString(R.string.error__invalid_account_name)
|
|
} else {
|
|
binding.tilAccountName.isErrorEnabled = false
|
|
binding.tilAccountName.helperText =
|
|
getString(R.string.text__verifying_account_availability)
|
|
val id = mNetworkService?.sendMessage(
|
|
GetAccountByName(accountName),
|
|
GetAccountByName.REQUIRED_API
|
|
)
|
|
|
|
if (id != null)
|
|
responseMap.append(id, RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION)
|
|
}
|
|
|
|
enableDisableCreateButton()
|
|
}
|
|
|
|
/**
|
|
* Verifies if the account length is valid, so that the faucet does not pay for a premium account.
|
|
*/
|
|
private fun isAccountLengthValid(accountName: String): Boolean {
|
|
return accountName.length in MIN_ACCOUNT_NAME_LENGTH..MAX_ACCOUNT_NAME_LENGTH
|
|
}
|
|
|
|
/**
|
|
* Verifies if the account start is valid, the account name should start with a letter.
|
|
*/
|
|
private fun isAccountStartValid(accountName: String): Boolean {
|
|
return accountName[0].isLetter()
|
|
}
|
|
|
|
/**
|
|
* Method used to determine if the account name entered by the user is valid
|
|
* @param accountName The proposed account name
|
|
* @return True if the name is valid, false otherwise
|
|
*/
|
|
private fun isAccountNameValid(accountName: String): Boolean {
|
|
return accountName.contains("-") ||
|
|
accountName.containsDigits() ||
|
|
!accountName.containsVowels()
|
|
}
|
|
|
|
private fun validatePIN() {
|
|
val pin = binding.tietPin.text.toString()
|
|
|
|
if (pin.length < Constants.MIN_PIN_LENGTH) {
|
|
binding.tilPin.error = getString(R.string.error__pin_too_short)
|
|
isPINValid = false
|
|
} else {
|
|
binding.tilPin.isErrorEnabled = false
|
|
isPINValid = true
|
|
}
|
|
|
|
validatePINConfirmation()
|
|
}
|
|
|
|
private fun validatePINConfirmation() {
|
|
val pinConfirmation = binding.tietPinConfirmation.text.toString()
|
|
|
|
if (pinConfirmation != binding.tietPin.text.toString()) {
|
|
binding.tilPinConfirmation.error = getString(R.string.error__pin_mismatch)
|
|
isPINConfirmationValid = false
|
|
} else {
|
|
binding.tilPinConfirmation.isErrorEnabled = false
|
|
isPINConfirmationValid = true
|
|
}
|
|
|
|
enableDisableCreateButton()
|
|
}
|
|
|
|
private fun enableDisableCreateButton() {
|
|
binding.btnCreate.isEnabled =
|
|
(isPINValid && isPINConfirmationValid && isAccountValidAndAvailable)
|
|
}
|
|
|
|
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
|
|
if (responseMap.containsKey(response.id)) {
|
|
when (responseMap[response.id]) {
|
|
RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION -> handleAccountNameValidation(response.result)
|
|
RESPONSE_GET_ACCOUNT_BY_NAME_CREATED -> handleAccountNameCreated(response.result)
|
|
}
|
|
responseMap.remove(response.id)
|
|
}
|
|
}
|
|
|
|
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {}
|
|
|
|
/**
|
|
* Handles the response from the NetworkService's GetAccountByName call to decide if the user's suggested
|
|
* account is available or not.
|
|
*/
|
|
private fun handleAccountNameValidation(result: Any?) {
|
|
if (result is AccountProperties) {
|
|
binding.tilAccountName.helperText = ""
|
|
binding.tilAccountName.error = getString(R.string.error__account_not_available)
|
|
isAccountValidAndAvailable = false
|
|
} else {
|
|
binding.tilAccountName.isErrorEnabled = false
|
|
binding.tilAccountName.helperText = getString(R.string.text__account_is_available)
|
|
isAccountValidAndAvailable = true
|
|
}
|
|
|
|
enableDisableCreateButton()
|
|
}
|
|
|
|
/**
|
|
* Handles the response from the NetworkService's GetAccountByName call and stores the information of the newly
|
|
* created account if the result is successful, shows a toast error otherwise
|
|
*/
|
|
private fun handleAccountNameCreated(result: Any?) {
|
|
if (result is AccountProperties) {
|
|
onAccountSelected(result, binding.tietPin.text.toString())
|
|
} else {
|
|
context?.toast(getString(R.string.error__created_account_not_found))
|
|
setStateError()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the state to Loading, when the app is trying to create an account and waiting for the response.
|
|
*/
|
|
private fun setStateLoading() {
|
|
binding.btnCancel.isEnabled = false
|
|
binding.btnCreate.isEnabled = false
|
|
binding.progressBar.visibility = View.VISIBLE
|
|
}
|
|
|
|
/**
|
|
* Sets the state to Error, when the app is unable to create the account or unable to retrieve
|
|
* the information from the newly created account.
|
|
*/
|
|
private fun setStateError() {
|
|
binding.btnCancel.isEnabled = true
|
|
binding.btnCreate.isEnabled = false
|
|
binding.progressBar.visibility = View.GONE
|
|
}
|
|
|
|
/**
|
|
* Sends the account-creation request to the faucet server.
|
|
* Only account name and public address is sent here.
|
|
*/
|
|
private fun createAccount() {
|
|
setStateLoading()
|
|
|
|
val accountName = binding.tietAccountName.text.toString()
|
|
val faucetRequest = FaucetRequest(accountName, mAddress, Constants.FAUCET_REFERRER)
|
|
|
|
val sg = ServiceGenerator(Constants.FAUCET_URL)
|
|
val faucetService = sg.getService(FaucetService::class.java)
|
|
|
|
val call = faucetService?.registerPrivateAccount(faucetRequest)
|
|
|
|
// Execute the call asynchronously. Get a positive or negative callback.
|
|
call?.enqueue(object : Callback<FaucetResponse> {
|
|
override fun onResponse(
|
|
call: Call<FaucetResponse>,
|
|
response: Response<FaucetResponse>
|
|
) {
|
|
// The network call was a success and we got a response, obtain the info of the newly created account
|
|
// with a delay to let the nodes update their information
|
|
val handler = Handler()
|
|
|
|
handler.postDelayed({
|
|
getCreatedAccountInfo(response.body())
|
|
}, 4000)
|
|
}
|
|
|
|
override fun onFailure(call: Call<FaucetResponse>, t: Throwable) {
|
|
// the network call was a failure
|
|
context?.let { context ->
|
|
MaterialDialog(context).show {
|
|
title(R.string.title_error)
|
|
message(R.string.error__faucet)
|
|
negativeButton(android.R.string.ok)
|
|
}
|
|
}
|
|
|
|
setStateError()
|
|
}
|
|
})
|
|
}
|
|
|
|
private fun getCreatedAccountInfo(faucetResponse: FaucetResponse?) {
|
|
if (faucetResponse?.account != null) {
|
|
val id = mNetworkService?.sendMessage(
|
|
GetAccountByName(faucetResponse.account?.name),
|
|
GetAccountByName.REQUIRED_API
|
|
)
|
|
|
|
if (id != null)
|
|
responseMap.append(id, RESPONSE_GET_ACCOUNT_BY_NAME_CREATED)
|
|
} else {
|
|
Log.d(TAG, "Private account creation failed ")
|
|
val content = if (faucetResponse?.error?.base?.size ?: 0 > 0) {
|
|
getString(R.string.error__faucet_template, faucetResponse?.error?.base?.get(0))
|
|
} else {
|
|
getString(R.string.error__faucet_template, "None")
|
|
}
|
|
|
|
context?.let { context ->
|
|
MaterialDialog(context)
|
|
.title(R.string.title_error)
|
|
.message(text = content)
|
|
.show()
|
|
}
|
|
|
|
setStateError()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method that generates a fresh key that will be controlling the newly created account.
|
|
*/
|
|
private fun generateKeys() {
|
|
var reader: BufferedReader? = null
|
|
val dictionary: String
|
|
try {
|
|
reader =
|
|
BufferedReader(InputStreamReader(context!!.assets.open(BRAINKEY_FILE), "UTF-8"))
|
|
dictionary = reader.readLine()
|
|
|
|
val brainKeySuggestion = BrainKey.suggest(dictionary)
|
|
mBrainKey = BrainKey(brainKeySuggestion, 0)
|
|
val address = Address(ECKey.fromPublicOnly(mBrainKey?.privateKey?.pubKey))
|
|
Log.d(TAG, "brain key: $brainKeySuggestion")
|
|
Log.d(TAG, "address would be: $address")
|
|
mAddress = address.toString()
|
|
binding.tvBrainKey.text = mBrainKey?.brainKey
|
|
|
|
} catch (e: IOException) {
|
|
Log.e(TAG, "IOException while trying to generate key. Msg: " + e.message)
|
|
context?.toast(getString(R.string.error__read_dict_file))
|
|
findNavController().navigateUp()
|
|
} catch (e: IllegalArgumentException) {
|
|
val crashlytics = FirebaseCrashlytics.getInstance()
|
|
crashlytics.recordException(e)
|
|
// TODO if this does happen to real devices, use a proper error message
|
|
context?.toast(getString(R.string.error__try_again))
|
|
findNavController().navigateUp()
|
|
} finally {
|
|
if (reader != null) {
|
|
try {
|
|
reader.close()
|
|
} catch (e: IOException) {
|
|
Log.e(
|
|
TAG,
|
|
"IOException while trying to close BufferedReader. Msg: " + e.message
|
|
)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
} |