Migrate app to ViewBinding. (#6)

* Enable ViewBinding

- Migrate MainActivity to ViewBinding.
- Migrate HomeFragment to ViewBinding.

* Migrate a batch of Fragments to ViewBinding.

- Migrate BalancesFragment to ViewBinding.
- Migrate CreateAccountFragment to ViewBinding.
- Migrate EReceiptFragment to ViewBinding.
- Migrate FilterOptionsDialog to ViewBinding.
- Migrate HomeFragment to ViewBinding.

* Migrate another batch of Fragments to ViewBinding.

- Migrate ImportBrainkeyFragment to ViewBinding.
- Migrate LicenseFragment to ViewBinding.
- Migrate MerchantsFragment to ViewBinding.
- Migrate NetWorthFragment to ViewBinding.
- Migrate PatternSecurityLockDialog to ViewBinding.

* Migrate final batch of Fragments to ViewBinding.

- Migrate PINSecurityLockScreen to ViewBinding.
- Migrate ReceiveTransactionFragment to ViewBinding.
- Migrate SendTransactionFragment to ViewBinding.
- Migrate SettingsFragment to ViewBinding.
- Migrate TransactionsFragment to ViewBinding.

* Migrate to the kotlin-parcelize plugin.

- Get rid of the deprecated kotlin-android-extensions plugin.
This commit is contained in:
Severiano Jaramillo 2021-03-04 22:46:36 -08:00 committed by GitHub
parent cc53526038
commit 0ec0457eb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1049 additions and 597 deletions

View file

@ -1,16 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
// Needed for Kotlin's @Parcelize annotation
androidExtensions {
experimental = true
}
android {
compileSdkVersion 29
defaultConfig {
@ -31,6 +26,11 @@ android {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
buildFeatures {
viewBinding true
}
buildTypes {
debug {
minifyEnabled false

View file

@ -12,8 +12,8 @@ import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupActionBarWithNavController
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.databinding.ActivityMainBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import kotlinx.android.synthetic.main.activity_main.*
/**
* Uses the AAC Navigation Component with a NavHostFragment which is the place where all Fragments are shown,
@ -21,6 +21,7 @@ import kotlinx.android.synthetic.main.activity_main.*
*/
class MainActivity : ConnectedActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var appBarConfiguration: AppBarConfiguration
// Handler and Runnable used to add a timer for user inaction and close the app if enough time has passed
@ -31,12 +32,14 @@ class MainActivity : ConnectedActivity() {
super.onCreate(savedInstanceState)
// Sets the theme to night mode if it has been selected by the user
if (PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)) {
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
) {
setTheme(R.style.Theme_Bitsy_Dark)
}
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(toolbar)
setSupportActionBar(binding.toolbar)
val host: NavHostFragment = supportFragmentManager
.findFragmentById(R.id.navHostFragment) as NavHostFragment? ?: return
@ -56,7 +59,8 @@ class MainActivity : ConnectedActivity() {
// closes the app, if not then it just restarts the Handler (timer)
mRunnable = Runnable {
if (PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, true)) {
.getBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, true)
) {
finish()
android.os.Process.killProcess(android.os.Process.myPid())
} else
@ -107,7 +111,7 @@ class MainActivity : ConnectedActivity() {
override fun onBackPressed() {
// Trick used to avoid crashes when the user is in the License or ImportBrainkey and presses the back button
val currentDestination=NavHostFragment.findNavController(navHostFragment).currentDestination
val currentDestination = binding.navHostFragment.findNavController().currentDestination
when (currentDestination?.id) {
R.id.license_dest, R.id.import_brainkey_dest -> finish()
else -> super.onBackPressed()

View file

@ -9,37 +9,49 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.BalancesAdapter
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
import cy.agorise.bitsybitshareswallet.databinding.FragmentBalancesBinding
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
import kotlinx.android.synthetic.main.fragment_balances.*
class BalancesFragment : Fragment() {
private var _binding: FragmentBalancesBinding? = null
private val binding get() = _binding!!
private lateinit var mBalanceDetailViewModel: BalanceDetailViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_balances, container, false)
_binding = FragmentBalancesBinding.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)
// Configure BalanceDetailViewModel to show the current balances
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
mBalanceDetailViewModel =
ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
val balancesAdapter = BalancesAdapter(context!!)
rvBalances.adapter = balancesAdapter
rvBalances.layoutManager = LinearLayoutManager(context!!)
rvBalances.addItemDecoration(DividerItemDecoration(context!!, DividerItemDecoration.VERTICAL))
binding.rvBalances.adapter = balancesAdapter
binding.rvBalances.layoutManager = LinearLayoutManager(context!!)
binding.rvBalances.addItemDecoration(
DividerItemDecoration(context!!, DividerItemDecoration.VERTICAL)
)
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
mBalanceDetailViewModel.getAll()
.observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
balancesAdapter.replaceAll(balancesDetails)
})
}

View file

@ -8,9 +8,15 @@ 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
@ -22,20 +28,14 @@ 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 kotlinx.android.synthetic.main.fragment_create_account.*
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
import com.afollestad.materialdialogs.MaterialDialog
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.models.FaucetRequest
import cy.agorise.bitsybitshareswallet.models.FaucetResponse
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
import retrofit2.Call
import retrofit2.Response
class CreateAccountFragment : BaseAccountFragment() {
@ -49,10 +49,14 @@ class CreateAccountFragment : BaseAccountFragment() {
// 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 */
@ -63,10 +67,20 @@ class CreateAccountFragment : BaseAccountFragment() {
// 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? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_create_account, container, false)
_binding = FragmentCreateAccountBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -77,7 +91,7 @@ class CreateAccountFragment : BaseAccountFragment() {
// Use RxJava Debounce to check the validity and availability of the user's proposed account name
mDisposables.add(
tietAccountName.textChanges()
binding.tietAccountName.textChanges()
.skipInitialValue()
.debounce(800, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -89,7 +103,7 @@ class CreateAccountFragment : BaseAccountFragment() {
// Use RxJava Debounce to update the PIN error only after the user stops writing for > 500 ms
mDisposables.add(
tietPin.textChanges()
binding.tietPin.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -101,7 +115,7 @@ class CreateAccountFragment : BaseAccountFragment() {
// Use RxJava Debounce to update the PIN Confirmation error only after the user stops writing for > 500 ms
mDisposables.add(
tietPinConfirmation.textChanges()
binding.tietPinConfirmation.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -111,10 +125,10 @@ class CreateAccountFragment : BaseAccountFragment() {
)
)
btnCancel.setOnClickListener { findNavController().navigateUp() }
binding.btnCancel.setOnClickListener { findNavController().navigateUp() }
btnCreate.isEnabled = false
btnCreate.setOnClickListener { createAccount() }
binding.btnCreate.isEnabled = false
binding.btnCreate.setOnClickListener { createAccount() }
// Generating BrainKey
generateKeys()
@ -124,18 +138,22 @@ class CreateAccountFragment : BaseAccountFragment() {
isAccountValidAndAvailable = false
if (!isAccountLengthValid(accountName)) {
tilAccountName.helperText = ""
tilAccountName.error = getString(R.string.error__invalid_account_length)
binding.tilAccountName.helperText = ""
binding.tilAccountName.error = getString(R.string.error__invalid_account_length)
} else if (!isAccountStartValid(accountName)) {
tilAccountName.helperText = ""
tilAccountName.error = getString(R.string.error__invalid_account_start)
binding.tilAccountName.helperText = ""
binding.tilAccountName.error = getString(R.string.error__invalid_account_start)
} else if (!isAccountNameValid(accountName)) {
tilAccountName.helperText = ""
tilAccountName.error = getString(R.string.error__invalid_account_name)
binding.tilAccountName.helperText = ""
binding.tilAccountName.error = getString(R.string.error__invalid_account_name)
} else {
tilAccountName.isErrorEnabled = false
tilAccountName.helperText = getString(R.string.text__verifying_account_availability)
val id = mNetworkService?.sendMessage(GetAccountByName(accountName), GetAccountByName.REQUIRED_API)
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)
@ -170,13 +188,13 @@ class CreateAccountFragment : BaseAccountFragment() {
}
private fun validatePIN() {
val pin = tietPin.text.toString()
val pin = binding.tietPin.text.toString()
if (pin.length < Constants.MIN_PIN_LENGTH) {
tilPin.error = getString(R.string.error__pin_too_short)
binding.tilPin.error = getString(R.string.error__pin_too_short)
isPINValid = false
} else {
tilPin.isErrorEnabled = false
binding.tilPin.isErrorEnabled = false
isPINValid = true
}
@ -184,13 +202,13 @@ class CreateAccountFragment : BaseAccountFragment() {
}
private fun validatePINConfirmation() {
val pinConfirmation = tietPinConfirmation.text.toString()
val pinConfirmation = binding.tietPinConfirmation.text.toString()
if (pinConfirmation != tietPin.text.toString()) {
tilPinConfirmation.error = getString(R.string.error__pin_mismatch)
if (pinConfirmation != binding.tietPin.text.toString()) {
binding.tilPinConfirmation.error = getString(R.string.error__pin_mismatch)
isPINConfirmationValid = false
} else {
tilPinConfirmation.isErrorEnabled = false
binding.tilPinConfirmation.isErrorEnabled = false
isPINConfirmationValid = true
}
@ -198,7 +216,8 @@ class CreateAccountFragment : BaseAccountFragment() {
}
private fun enableDisableCreateButton() {
btnCreate.isEnabled = (isPINValid && isPINConfirmationValid && isAccountValidAndAvailable)
binding.btnCreate.isEnabled =
(isPINValid && isPINConfirmationValid && isAccountValidAndAvailable)
}
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
@ -219,12 +238,12 @@ class CreateAccountFragment : BaseAccountFragment() {
*/
private fun handleAccountNameValidation(result: Any?) {
if (result is AccountProperties) {
tilAccountName.helperText = ""
tilAccountName.error = getString(R.string.error__account_not_available)
binding.tilAccountName.helperText = ""
binding.tilAccountName.error = getString(R.string.error__account_not_available)
isAccountValidAndAvailable = false
} else {
tilAccountName.isErrorEnabled = false
tilAccountName.helperText = getString(R.string.text__account_is_available)
binding.tilAccountName.isErrorEnabled = false
binding.tilAccountName.helperText = getString(R.string.text__account_is_available)
isAccountValidAndAvailable = true
}
@ -237,7 +256,7 @@ class CreateAccountFragment : BaseAccountFragment() {
*/
private fun handleAccountNameCreated(result: Any?) {
if (result is AccountProperties) {
onAccountSelected(result, tietPin.text.toString())
onAccountSelected(result, binding.tietPin.text.toString())
} else {
context?.toast(getString(R.string.error__created_account_not_found))
setStateError()
@ -248,9 +267,9 @@ class CreateAccountFragment : BaseAccountFragment() {
* Sets the state to Loading, when the app is trying to create an account and waiting for the response.
*/
private fun setStateLoading() {
btnCancel.isEnabled = false
btnCreate.isEnabled = false
progressBar.visibility = View.VISIBLE
binding.btnCancel.isEnabled = false
binding.btnCreate.isEnabled = false
binding.progressBar.visibility = View.VISIBLE
}
/**
@ -258,9 +277,9 @@ class CreateAccountFragment : BaseAccountFragment() {
* the information from the newly created account.
*/
private fun setStateError() {
btnCancel.isEnabled = true
btnCreate.isEnabled = false
progressBar.visibility = View.GONE
binding.btnCancel.isEnabled = true
binding.btnCreate.isEnabled = false
binding.progressBar.visibility = View.GONE
}
/**
@ -270,7 +289,7 @@ class CreateAccountFragment : BaseAccountFragment() {
private fun createAccount() {
setStateLoading()
val accountName = tietAccountName.text.toString()
val accountName = binding.tietAccountName.text.toString()
val faucetRequest = FaucetRequest(accountName, mAddress, Constants.FAUCET_REFERRER)
val sg = ServiceGenerator(Constants.FAUCET_URL)
@ -280,7 +299,10 @@ class CreateAccountFragment : BaseAccountFragment() {
// Execute the call asynchronously. Get a positive or negative callback.
call?.enqueue(object : Callback<FaucetResponse> {
override fun onResponse(call: Call<FaucetResponse>, response: Response<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()
@ -307,8 +329,10 @@ class CreateAccountFragment : BaseAccountFragment() {
private fun getCreatedAccountInfo(faucetResponse: FaucetResponse?) {
if (faucetResponse?.account != null) {
val id = mNetworkService?.sendMessage(GetAccountByName(faucetResponse.account?.name),
GetAccountByName.REQUIRED_API)
val id = mNetworkService?.sendMessage(
GetAccountByName(faucetResponse.account?.name),
GetAccountByName.REQUIRED_API
)
if (id != null)
responseMap.append(id, RESPONSE_GET_ACCOUNT_BY_NAME_CREATED)
@ -338,7 +362,8 @@ class CreateAccountFragment : BaseAccountFragment() {
var reader: BufferedReader? = null
val dictionary: String
try {
reader = BufferedReader(InputStreamReader(context!!.assets.open(BRAINKEY_FILE), "UTF-8"))
reader =
BufferedReader(InputStreamReader(context!!.assets.open(BRAINKEY_FILE), "UTF-8"))
dictionary = reader.readLine()
val brainKeySuggestion = BrainKey.suggest(dictionary)
@ -347,7 +372,7 @@ class CreateAccountFragment : BaseAccountFragment() {
Log.d(TAG, "brain key: $brainKeySuggestion")
Log.d(TAG, "address would be: $address")
mAddress = address.toString()
tvBrainKey.text = mBrainKey?.brainKey
binding.tvBrainKey.text = mBrainKey?.brainKey
} catch (e: IOException) {
Log.e(TAG, "IOException while trying to generate key. Msg: " + e.message)
@ -364,7 +389,10 @@ class CreateAccountFragment : BaseAccountFragment() {
try {
reader.close()
} catch (e: IOException) {
Log.e(TAG, "IOException while trying to close BufferedReader. Msg: " + e.message)
Log.e(
TAG,
"IOException while trying to close BufferedReader. Msg: " + e.message
)
}
}

View file

@ -16,14 +16,13 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.fragment.navArgs
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.databinding.FragmentEReceiptBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.utils.toast
import cy.agorise.bitsybitshareswallet.viewmodels.EReceiptViewModel
import kotlinx.android.synthetic.main.fragment_e_receipt.*
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
@ -39,15 +38,28 @@ class EReceiptFragment : Fragment() {
private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
}
private var _binding: FragmentEReceiptBinding? = null
private val binding get() = _binding!!
private val args: EReceiptFragmentArgs by navArgs()
private lateinit var mEReceiptViewModel: EReceiptViewModel
private lateinit var mLocale: Locale
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_e_receipt, container, false)
_binding = FragmentEReceiptBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -65,18 +77,20 @@ class EReceiptFragment : Fragment() {
mEReceiptViewModel = ViewModelProviders.of(this).get(EReceiptViewModel::class.java)
mEReceiptViewModel.get(userId, transferId).observe(this, Observer<TransferDetail> { transferDetail ->
mEReceiptViewModel.get(userId, transferId)
.observe(this, Observer<TransferDetail> { transferDetail ->
bindTransferDetail(transferDetail)
})
}
private fun bindTransferDetail(transferDetail: TransferDetail) {
context?.let { vPaymentDirection.setBackgroundColor(ContextCompat.getColor(it,
if(transferDetail.direction) R.color.colorReceive else R.color.colorSend
))}
context?.let { context ->
val colorRes = if (transferDetail.direction) R.color.colorReceive else R.color.colorSend
binding.vPaymentDirection.setBackgroundColor(ContextCompat.getColor(context, colorRes))
}
tvFrom.text = transferDetail.from ?: ""
tvTo.text = transferDetail.to ?: ""
binding.tvFrom.text = transferDetail.from ?: ""
binding.tvTo.text = transferDetail.to ?: ""
// Show the crypto amount correctly formatted
val df = DecimalFormat("####." + ("#".repeat(transferDetail.assetPrecision)))
@ -86,7 +100,7 @@ class EReceiptFragment : Fragment() {
val amount = transferDetail.assetAmount.toDouble() /
Math.pow(10.toDouble(), transferDetail.assetPrecision.toDouble())
val assetAmount = "${df.format(amount)} ${transferDetail.getUIAssetSymbol()}"
tvAmount.text = assetAmount
binding.tvAmount.text = assetAmount
// Fiat equivalent
if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
@ -96,20 +110,21 @@ class EReceiptFragment : Fragment() {
Math.pow(10.0, currency.defaultFractionDigits.toDouble())
val equivalentValue = "${numberFormat.format(fiatEquivalent)} ${currency.currencyCode}"
tvEquivalentValue.text = equivalentValue
binding.tvEquivalentValue.text = equivalentValue
} else {
tvEquivalentValue.text = "-"
binding.tvEquivalentValue.text = "-"
}
// Memo
if (transferDetail.memo != "")
tvMemo.text = getString(R.string.template__memo, transferDetail.memo)
binding.tvMemo.text = getString(R.string.template__memo, transferDetail.memo)
else
tvMemo.visibility = View.GONE
binding.tvMemo.visibility = View.GONE
// Date
val dateFormat = SimpleDateFormat("dd MMM HH:mm:ss z", mLocale)
tvDate.text = getString(R.string.template__date, dateFormat.format(transferDetail.date * 1000))
binding.tvDate.text =
getString(R.string.template__date, dateFormat.format(transferDetail.date * 1000))
// Transaction #
formatTransferTextView(transferDetail.id)
@ -118,11 +133,14 @@ class EReceiptFragment : Fragment() {
/** Formats the transfer TextView to show a link to explore the given transfer
* in a BitShares explorer */
private fun formatTransferTextView(transferId: String) {
val tx = Html.fromHtml(getString(R.string.template__tx,
val tx = Html.fromHtml(
getString(
R.string.template__tx,
"<a href=\"http://bitshares-explorer.io/#/operations/$transferId\">$transferId</a>"
))
tvTransferID.text = tx
tvTransferID.movementMethod = LinkMovementMethod.getInstance()
)
)
binding.tvTransferID.text = tx
binding.tvTransferID.movementMethod = LinkMovementMethod.getInstance()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -146,18 +164,26 @@ class EReceiptFragment : Fragment() {
/** Verifies if the storage permission is already granted, if that is the case then it takes the screenshot and
* shares it but if it is not then it asks the user for that permission */
private fun verifyStoragePermission() {
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ContextCompat
.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED
) {
// Permission is not already granted
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION)
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION
)
} else {
// Permission is already granted
shareEReceiptScreenshot()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) {
@ -174,8 +200,8 @@ class EReceiptFragment : Fragment() {
* sends an intent so the user can select the desired method to share the image. */
private fun shareEReceiptScreenshot() {
// Get Screenshot
tvTransferID.text = getString(R.string.template__tx, args.transferId)
val screenshot = Helper.loadBitmapFromView(container)
binding.tvTransferID.text = getString(R.string.template__tx, args.transferId)
val screenshot = Helper.loadBitmapFromView(binding.container)
formatTransferTextView(args.transferId)
val imageUri = context?.let { Helper.saveTemporalBitmap(it, screenshot) }

View file

@ -6,25 +6,24 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import android.widget.*
import android.widget.TextView
import androidx.core.os.ConfigurationCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
import cy.agorise.bitsybitshareswallet.databinding.DialogFilterOptionsBinding
import cy.agorise.bitsybitshareswallet.models.FilterOptions
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
import cy.agorise.bitsybitshareswallet.views.DatePickerFragment
import kotlinx.android.synthetic.main.dialog_filter_options.*
import java.text.SimpleDateFormat
import java.util.*
import kotlin.ClassCastException
import kotlin.collections.ArrayList
@ -43,12 +42,17 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
const val END_DATE_PICKER = 1
}
private var _binding: DialogFilterOptionsBinding? = null
private val binding get() = _binding!!
private lateinit var mFilterOptions: FilterOptions
private var mCallback: OnFilterOptionsSelectedListener? = null
private var dateFormat: SimpleDateFormat = SimpleDateFormat("d/MMM/yyyy",
ConfigurationCompat.getLocales(Resources.getSystem().configuration)[0])
private var dateFormat: SimpleDateFormat = SimpleDateFormat(
"d/MMM/yyyy",
ConfigurationCompat.getLocales(Resources.getSystem().configuration)[0]
)
private var mBalanceDetails = ArrayList<BalanceDetail>()
@ -85,10 +89,10 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
private fun updateDateTextViews() {
var date = Date(mFilterOptions.startDate)
tvStartDate.text = dateFormat.format(date)
binding.tvStartDate.text = dateFormat.format(date)
date = Date(mFilterOptions.endDate)
tvEndDate.text = dateFormat.format(date)
binding.tvEndDate.text = dateFormat.format(date)
}
// Container Fragment must implement this interface
@ -96,8 +100,18 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
fun onFilterOptionsSelected(filterOptions: FilterOptions)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_filter_options, container, false)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogFilterOptionsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -112,73 +126,81 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
// Initialize Transactions direction
when (mFilterOptions.transactionsDirection) {
0 -> rbTransactionAll.isChecked = true
1 -> rbTransactionSent.isChecked = true
2 -> rbTransactionReceived.isChecked = true
0 -> binding.rbTransactionAll.isChecked = true
1 -> binding.rbTransactionSent.isChecked = true
2 -> binding.rbTransactionReceived.isChecked = true
}
// Initialize Date range
cbDateRange.setOnCheckedChangeListener { _, isChecked ->
llDateRange.visibility = if(isChecked) View.GONE else View.VISIBLE }
cbDateRange.isChecked = mFilterOptions.dateRangeAll
binding.cbDateRange.setOnCheckedChangeListener { _, isChecked ->
binding.llDateRange.visibility = if (isChecked) View.GONE else View.VISIBLE
}
binding.cbDateRange.isChecked = mFilterOptions.dateRangeAll
tvStartDate.setOnClickListener(mDateClickListener)
binding.tvStartDate.setOnClickListener(mDateClickListener)
tvEndDate.setOnClickListener(mDateClickListener)
binding.tvEndDate.setOnClickListener(mDateClickListener)
updateDateTextViews()
// Initialize Asset
cbAsset.setOnCheckedChangeListener { _, isChecked ->
sAsset.visibility = if(isChecked) View.GONE else View.VISIBLE
binding.cbAsset.setOnCheckedChangeListener { _, isChecked ->
binding.sAsset.visibility = if (isChecked) View.GONE else View.VISIBLE
}
cbAsset.isChecked = mFilterOptions.assetAll
binding.cbAsset.isChecked = mFilterOptions.assetAll
// Configure BalanceDetailViewModel to obtain the user's Balances
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
mBalanceDetailViewModel =
ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
mBalanceDetailViewModel.getAll()
.observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
mBalanceDetails.clear()
mBalanceDetails.addAll(balancesDetails)
mBalanceDetails.sortWith(
Comparator { a, b -> a.toString().compareTo(b.toString(), true) }
)
mBalancesDetailsAdapter = BalancesDetailsAdapter(context!!, android.R.layout.simple_spinner_item, mBalanceDetails)
sAsset.adapter = mBalancesDetailsAdapter
mBalancesDetailsAdapter = BalancesDetailsAdapter(
context!!,
android.R.layout.simple_spinner_item,
mBalanceDetails
)
binding.sAsset.adapter = mBalancesDetailsAdapter
// Try to select the selectedAssetSymbol
for (i in 0 until mBalancesDetailsAdapter!!.count) {
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == mFilterOptions.asset) {
sAsset.setSelection(i)
binding.sAsset.setSelection(i)
break
}
}
})
// Initialize Equivalent Value
cbEquivalentValue.setOnCheckedChangeListener { _, isChecked ->
llEquivalentValue.visibility = if(isChecked) View.GONE else View.VISIBLE }
cbEquivalentValue.isChecked = mFilterOptions.equivalentValueAll
binding.cbEquivalentValue.setOnCheckedChangeListener { _, isChecked ->
binding.llEquivalentValue.visibility = if (isChecked) View.GONE else View.VISIBLE
}
binding.cbEquivalentValue.isChecked = mFilterOptions.equivalentValueAll
val currencyCode = Helper.getCoingeckoSupportedCurrency(Locale.getDefault())
mCurrency = Currency.getInstance(currencyCode)
val fromEquivalentValue = mFilterOptions.fromEquivalentValue /
Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong()
etFromEquivalentValue.setText("$fromEquivalentValue", TextView.BufferType.EDITABLE)
binding.etFromEquivalentValue.setText("$fromEquivalentValue", TextView.BufferType.EDITABLE)
val toEquivalentValue = mFilterOptions.toEquivalentValue /
Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong()
etToEquivalentValue.setText("$toEquivalentValue", TextView.BufferType.EDITABLE)
binding.etToEquivalentValue.setText("$toEquivalentValue", TextView.BufferType.EDITABLE)
tvEquivalentValueSymbol.text = currencyCode.toUpperCase(Locale.getDefault())
binding.tvEquivalentValueSymbol.text = currencyCode.toUpperCase(Locale.getDefault())
// Initialize transaction network fees
switchAgoriseFees.isChecked = mFilterOptions.agoriseFees
binding.switchAgoriseFees.isChecked = mFilterOptions.agoriseFees
// Setup cancel and filter buttons
btnCancel.setOnClickListener { dismiss() }
btnFilter.setOnClickListener { validateFields() }
binding.btnCancel.setOnClickListener { dismiss() }
binding.btnFilter.setOnClickListener { validateFields() }
}
override fun onResume() {
@ -187,7 +209,10 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
// Force dialog fragment to use the full width of the screen
// TODO use the same width as standard fragments
val dialogWindow = dialog?.window
dialogWindow?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialogWindow?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
/**
@ -227,17 +252,19 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
private fun validateFields() {
mFilterOptions.transactionsDirection = when {
rbTransactionAll.isChecked -> 0
rbTransactionSent.isChecked -> 1
rbTransactionReceived.isChecked -> 2
else -> { 0 }
binding.rbTransactionAll.isChecked -> 0
binding.rbTransactionSent.isChecked -> 1
binding.rbTransactionReceived.isChecked -> 2
else -> {
0
}
}
mFilterOptions.dateRangeAll = cbDateRange.isChecked
mFilterOptions.dateRangeAll = binding.cbDateRange.isChecked
mFilterOptions.assetAll = cbAsset.isChecked
mFilterOptions.assetAll = binding.cbAsset.isChecked
val symbol = (sAsset.selectedItem as BalanceDetail?)?.symbol
val symbol = (binding.sAsset.selectedItem as BalanceDetail?)?.symbol
// If there are no assets in the spinner (the account has 0 balances or the app has not yet
// fetched the account balances) symbol will be null, make sure that does not create a crash.
if (symbol != null)
@ -245,19 +272,20 @@ class FilterOptionsDialog : DialogFragment(), DatePickerFragment.OnDateSetListen
else
mFilterOptions.assetAll = true
mFilterOptions.equivalentValueAll = cbEquivalentValue.isChecked
mFilterOptions.equivalentValueAll = binding.cbEquivalentValue.isChecked
mFilterOptions.fromEquivalentValue = etFromEquivalentValue.text.toString().toLong() *
mFilterOptions.fromEquivalentValue =
binding.etFromEquivalentValue.text.toString().toLong() *
Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong()
mFilterOptions.toEquivalentValue = etToEquivalentValue.text.toString().toLong() *
mFilterOptions.toEquivalentValue = binding.etToEquivalentValue.text.toString().toLong() *
Math.pow(10.0, mCurrency.defaultFractionDigits.toDouble()).toLong()
// Make sure ToEquivalentValue is at least 50 units bigger than FromEquivalentValue
mFilterOptions.toEquivalentValue =
Math.max(mFilterOptions.toEquivalentValue, mFilterOptions.fromEquivalentValue + 50)
mFilterOptions.agoriseFees = switchAgoriseFees.isChecked
mFilterOptions.agoriseFees = binding.switchAgoriseFees.isChecked
mCallback!!.onFilterOptionsSelected(mFilterOptions)
dismiss()

View file

@ -1,25 +1,24 @@
package cy.agorise.bitsybitshareswallet.fragments
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
import cy.agorise.bitsybitshareswallet.databinding.FragmentHomeBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
import kotlinx.android.synthetic.main.fragment_home.*
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.crashlytics.FirebaseCrashlytics
class HomeFragment : Fragment() {
@ -28,9 +27,16 @@ class HomeFragment : Fragment() {
private const val TAG = "HomeFragment"
}
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var mUserAccountViewModel: UserAccountViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
@ -52,13 +58,21 @@ class HomeFragment : Fragment() {
window?.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
// Sets the status and navigation bars background color to a dark blue or just dark
context?.let { context ->
val statusBarColor = ContextCompat.getColor(context,
if (!nightMode) R.color.colorPrimaryVariant else R.color.colorStatusBarDark)
val statusBarColor = ContextCompat.getColor(
context,
if (!nightMode) R.color.colorPrimaryVariant else R.color.colorStatusBarDark
)
window?.statusBarColor = statusBarColor
window?.navigationBarColor = statusBarColor
}
return inflater.inflate(R.layout.fragment_home, container, false)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -82,48 +96,49 @@ class HomeFragment : Fragment() {
// Configure UserAccountViewModel to show the current account
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)
mUserAccountViewModel.getUserAccount(userId).observe(this, Observer<UserAccount>{ userAccount ->
mUserAccountViewModel.getUserAccount(userId)
.observe(this, Observer<UserAccount> { userAccount ->
if (userAccount != null) {
tvAccountName.text = userAccount.name
binding.tvAccountName.text = userAccount.name
if (userAccount.isLtm) {
// Add the lightning bolt to the start of the account name if it is LTM
tvAccountName.setCompoundDrawablesWithIntrinsicBounds(
binding.tvAccountName.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_ltm_account, 0, 0, 0
)
// Add some padding so that the lightning bolt icon is not too close to the account name text
tvAccountName.compoundDrawablePadding = 12
binding.tvAccountName.compoundDrawablePadding = 12
}
}
})
// Navigate to the Receive Transaction Fragment
fabReceiveTransaction.setOnClickListener (
binding.fabReceiveTransaction.setOnClickListener(
Navigation.createNavigateOnClickListener(R.id.receive_action)
)
// Navigate to the Send Transaction Fragment without activating the camera
fabSendTransaction.setOnClickListener(
binding.fabSendTransaction.setOnClickListener(
Navigation.createNavigateOnClickListener(R.id.send_action)
)
// Navigate to the Send Transaction Fragment using Navigation's SafeArgs to activate the camera
fabSendTransactionCamera.setOnClickListener {
binding.fabSendTransactionCamera.setOnClickListener {
val action = HomeFragmentDirections.sendAction(true)
findNavController().navigate(action)
}
// Configure ViewPager with PagerAdapter and TabLayout to display the Balances/NetWorth section
val pagerAdapter = PagerAdapter(childFragmentManager)
viewPager.adapter = pagerAdapter
tabLayout.setupWithViewPager(viewPager)
binding.viewPager.adapter = pagerAdapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
// Set the pie chart icon for the third tab
tabLayout.getTabAt(2)?.setIcon(R.drawable.ic_pie_chart)
binding.tabLayout.getTabAt(2)?.setIcon(R.drawable.ic_pie_chart)
}
/**
* Pager adapter to create the placeholder fragments
*/
private inner class PagerAdapter internal constructor(fm: FragmentManager) : FragmentPagerAdapter(fm) {
private inner class PagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
// getItem is called to instantiate the fragment for the given page.
@ -133,8 +148,12 @@ class HomeFragment : Fragment() {
NetWorthFragment()
}
override fun getPageTitle(position: Int): CharSequence? {
return listOf(getString(R.string.title_balances), getString(R.string.title_net_worth), "")[position]
override fun getPageTitle(position: Int): CharSequence {
return listOf(
getString(R.string.title_balances),
getString(R.string.title_net_worth),
""
)[position]
}
override fun getCount(): Int {

View file

@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.content.res.ResourcesCompat
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
@ -19,9 +20,12 @@ import com.jakewharton.rxbinding3.widget.textChanges
import cy.agorise.bitsybitshareswallet.BuildConfig
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.FullNodesAdapter
import cy.agorise.bitsybitshareswallet.databinding.FragmentImportBrainkeyBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.toast
import cy.agorise.graphenej.*
import cy.agorise.graphenej.Address
import cy.agorise.graphenej.BrainKey
import cy.agorise.graphenej.UserAccount
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.GetAccounts
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
@ -30,7 +34,6 @@ import cy.agorise.graphenej.models.AccountProperties
import cy.agorise.graphenej.models.DynamicGlobalProperties
import cy.agorise.graphenej.models.JsonRpcResponse
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.*
@ -42,6 +45,9 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
private const val TAG = "ImportBrainkeyFragment"
}
private var _binding: FragmentImportBrainkeyBinding? = null
private val binding get() = _binding!!
/** User account associated with the key derived from the brainkey that the user just typed in */
private var mUserAccount: UserAccount? = null
@ -72,12 +78,22 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
/** Handler that will be used to make recurrent calls to get the latest BitShares block number*/
private val mHandler = Handler()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Remove up navigation icon from the toolbar
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.navigationIcon = null
return inflater.inflate(R.layout.fragment_import_brainkey, container, false)
_binding = FragmentImportBrainkeyBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -88,7 +104,7 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
// Use RxJava Debounce to update the PIN error only after the user stops writing for > 500 ms
mDisposables.add(
tietPin.textChanges()
binding.tietPin.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -100,7 +116,7 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
// Use RxJava Debounce to update the PIN Confirmation error only after the user stops writing for > 500 ms
mDisposables.add(
tietPinConfirmation.textChanges()
binding.tietPinConfirmation.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -112,7 +128,7 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
// Use RxJava Debounce to update the BrainKey error only after the user stops writing for > 500 ms
mDisposables.add(
tietBrainKey.textChanges()
binding.tietBrainKey.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.toString().trim() }
@ -123,14 +139,14 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
)
)
btnImport.isEnabled = false
btnImport.setOnClickListener { verifyBrainKey(false) }
binding.btnImport.isEnabled = false
binding.btnImport.setOnClickListener { verifyBrainKey(false) }
btnCreate.setOnClickListener (
binding.btnCreate.setOnClickListener(
Navigation.createNavigateOnClickListener(R.id.create_account_action)
)
tvNetworkStatus.setOnClickListener { v -> showNodesDialog(v) }
binding.tvNetworkStatus.setOnClickListener { v -> showNodesDialog(v) }
}
private fun showNodesDialog(v: View) {
@ -160,7 +176,13 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
)
mNodesDialog = MaterialDialog(v.context).show {
title(text = String.format("%s v%s", getString(R.string.app_name), BuildConfig.VERSION_NAME))
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)
negativeButton(android.R.string.ok)
@ -170,7 +192,8 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
}
}
mNodesDialogLinearLayoutManager = (mNodesDialog?.getRecyclerView()?.layoutManager as LinearLayoutManager)
mNodesDialogLinearLayoutManager =
(mNodesDialog?.getRecyclerView()?.layoutManager as LinearLayoutManager)
// Registering a recurrent task used to poll for dynamic global properties requests
mHandler.post(mRequestDynamicGlobalPropertiesTask)
@ -178,13 +201,13 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
}
private fun validatePIN() {
val pin = tietPin.text.toString()
val pin = binding.tietPin.text.toString()
if (pin.length < Constants.MIN_PIN_LENGTH) {
tilPin.error = getString(R.string.error__pin_too_short)
binding.tilPin.error = getString(R.string.error__pin_too_short)
isPINValid = false
} else {
tilPin.isErrorEnabled = false
binding.tilPin.isErrorEnabled = false
isPINValid = true
}
@ -192,13 +215,13 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
}
private fun validatePINConfirmation() {
val pinConfirmation = tietPinConfirmation.text.toString()
val pinConfirmation = binding.tietPinConfirmation.text.toString()
if (pinConfirmation != tietPin.text.toString()) {
tilPinConfirmation.error = getString(R.string.error__pin_mismatch)
if (pinConfirmation != binding.tietPin.text.toString()) {
binding.tilPinConfirmation.error = getString(R.string.error__pin_mismatch)
isPINConfirmationValid = false
} else {
tilPinConfirmation.isErrorEnabled = false
binding.tilPinConfirmation.isErrorEnabled = false
isPINConfirmationValid = true
}
@ -207,10 +230,10 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
private fun validateBrainKey(brainKey: String) {
if (brainKey.isEmpty() || !brainKey.contains(" ") || brainKey.split(" ").size !in 12..16) {
tilBrainKey.error = getString(R.string.error__enter_correct_brainkey)
binding.tilBrainKey.error = getString(R.string.error__enter_correct_brainkey)
isBrainKeyValid = false
} else {
tilBrainKey.isErrorEnabled = false
binding.tilBrainKey.isErrorEnabled = false
isBrainKeyValid = true
}
@ -218,7 +241,7 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
}
private fun enableDisableImportButton() {
btnImport.isEnabled = (isPINValid && isPINConfirmationValid && isBrainKeyValid)
binding.btnImport.isEnabled = (isPINValid && isPINConfirmationValid && isBrainKeyValid)
}
/**
@ -242,7 +265,7 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
*/
private fun verifyBrainKey(switchCase: Boolean) {
//showDialog("", getString(R.string.importing_your_wallet))
val brainKey = tietBrainKey.text.toString().trim()
val brainKey = binding.tietBrainKey.text.toString().trim()
// Should we switch the brainkey case?
if (switchCase) {
if (Character.isUpperCase(brainKey.toCharArray()[brainKey.length - 1])) {
@ -270,7 +293,8 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
mBrainKey = BrainKey(brainKey, 0)
val address = Address(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
Log.d(TAG, String.format("Brainkey would generate address: %s", address.toString()))
keyReferencesRequestId = mNetworkService?.sendMessage(GetKeyReferences(address), GetKeyReferences.REQUIRED_API)
keyReferencesRequestId =
mNetworkService?.sendMessage(GetKeyReferences(address), GetKeyReferences.REQUIRED_API)
}
override fun onStart() {
@ -290,8 +314,14 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
} else if (response.result is DynamicGlobalProperties) {
val dynamicGlobalProperties = response.result as DynamicGlobalProperties
if (mNodesDialog != null && mNodesDialog?.isShowing == true) {
val blockNumber = NumberFormat.getInstance().format(dynamicGlobalProperties.head_block_number)
mNodesDialog?.message(text = getString(R.string.title__bitshares_nodes_dialog, blockNumber))
val blockNumber =
NumberFormat.getInstance().format(dynamicGlobalProperties.head_block_number)
mNodesDialog?.message(
text = getString(
R.string.title__bitshares_nodes_dialog,
blockNumber
)
)
}
}
}
@ -308,13 +338,17 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
}
private fun showConnectedState() {
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
resources.getDrawable(R.drawable.ic_connected, null), null)
binding.tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null,
ResourcesCompat.getDrawable(resources, R.drawable.ic_connected, null), null
)
}
private fun showDisconnectedState() {
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
resources.getDrawable(R.drawable.ic_disconnected, null), null)
binding.tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null,
ResourcesCompat.getDrawable(resources, R.drawable.ic_disconnected, null), null
)
}
/**
@ -371,12 +405,18 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
MaterialDialog(context!!)
.title(R.string.dialog__account_candidates_title)
.message(R.string.dialog__account_candidates_content)
.listItemsSingleChoice (items = candidates, initialSelection = -1) { _, index, _ ->
.listItemsSingleChoice(
items = candidates,
initialSelection = -1
) { _, index, _ ->
if (index >= 0) {
// If one account was selected, we keep a reference to it and
// store the account properties
mUserAccount = mUserAccountCandidates!![index]
onAccountSelected(accountPropertiesList[index], tietPin.text.toString())
onAccountSelected(
accountPropertiesList[index],
binding.tietPin.text.toString()
)
}
}
.positiveButton(android.R.string.ok)
@ -386,7 +426,7 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
.cancelable(false)
.show()
} else if (accountPropertiesList.size == 1) {
onAccountSelected(accountPropertiesList[0], tietPin.text.toString())
onAccountSelected(accountPropertiesList[0], binding.tietPin.text.toString())
} else {
context?.toast(getString(R.string.error__try_again))
}
@ -400,7 +440,10 @@ class ImportBrainkeyFragment : BaseAccountFragment() {
override fun run() {
if (mNetworkService != null) {
if (mNetworkService?.isConnected == true) {
mNetworkService?.sendMessage(GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API)
mNetworkService?.sendMessage(
GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API
)
} else {
Log.d(TAG, "NetworkService exists but is not connected")
}

View file

@ -10,8 +10,8 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.databinding.FragmentLicenseBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import kotlinx.android.synthetic.main.fragment_license.*
class LicenseFragment : Fragment() {
@ -19,12 +19,25 @@ class LicenseFragment : Fragment() {
private const val TAG = "LicenseFragment"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
private var _binding: FragmentLicenseBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Remove up navigation icon from the toolbar
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.navigationIcon = null
return inflater.inflate(R.layout.fragment_license, container, false)
_binding = FragmentLicenseBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -41,11 +54,11 @@ class LicenseFragment : Fragment() {
if (agreedLicenseVersion == Constants.CURRENT_LICENSE_VERSION) {
agree()
} else {
wbLA.loadUrl("file:///android_asset/eula.html")
binding.wbLA.loadUrl("file:///android_asset/eula.html")
btnDisagree.setOnClickListener { activity?.finish() }
binding.btnDisagree.setOnClickListener { activity?.finish() }
btnAgree.setOnClickListener { agree() }
binding.btnAgree.setOnClickListener { agree() }
}
}
@ -55,7 +68,8 @@ class LicenseFragment : Fragment() {
*/
private fun agree() {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION).apply()
.putInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION)
.apply()
findNavController().navigate(R.id.import_brainkey_action)
}

View file

@ -39,6 +39,7 @@ import com.jakewharton.rxbinding3.appcompat.queryTextChangeEvents
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.entities.Merchant
import cy.agorise.bitsybitshareswallet.database.entities.Teller
import cy.agorise.bitsybitshareswallet.databinding.FragmentMerchantsBinding
import cy.agorise.bitsybitshareswallet.models.MapObject
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.MerchantClusterRenderer
@ -50,10 +51,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.fragment_merchants.*
import java.math.BigInteger
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestionListener {
@ -74,6 +73,9 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
private const val SUGGEST_COLUMN_IMAGE_RESOURCE = "suggest_image_resource"
}
private var _binding: FragmentMerchantsBinding? = null
private val binding get() = _binding!!
private var mMap: GoogleMap? = null
private lateinit var mMerchantViewModel: MerchantViewModel
@ -108,17 +110,33 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
private var statusBarSize = 0
private var navigationBarSize = 0
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Hide the activity's Toolbar so that we can make the trick of the translucent navigation and status bars
val activityToolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
activityToolbar?.visibility = View.GONE
// Sets the Navigation and Status bars translucent so that the map can be viewed through them
val window = activity?.window
window?.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
window?.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window?.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
)
window?.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
)
return inflater.inflate(R.layout.fragment_merchants, container, false)
_binding = FragmentMerchantsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -132,17 +150,17 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
view.setOnApplyWindowInsetsListener { v, insets ->
statusBarSize = insets.systemWindowInsetTop
navigationBarSize = insets.systemWindowInsetBottom
val layoutParams = toolbar.layoutParams as ViewGroup.MarginLayoutParams
val layoutParams = binding.toolbar.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = statusBarSize
insets
}
// Set the fragment's toolbar as the activity toolbar just for this fragment
(activity as AppCompatActivity).setSupportActionBar(toolbar)
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
(activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
setHasOptionsMenu(true)
toolbar?.setOnClickListener { dismissPopupWindow() }
binding.toolbar.setOnClickListener { dismissPopupWindow() }
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
@ -183,7 +201,11 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
}
}
mPopupWindow = PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
mPopupWindow = PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -192,7 +214,8 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
// Adds listener for the SearchView
val searchItem = menu.findItem(R.id.menu_search)
mSearchView = searchItem.actionView as SearchView
mSearchView?.suggestionsAdapter = SimpleCursorAdapter(context, R.layout.item_merchant_suggestion, null,
mSearchView?.suggestionsAdapter = SimpleCursorAdapter(
context, R.layout.item_merchant_suggestion, null,
arrayOf(SUGGEST_COLUMN_NAME, SUGGEST_COLUMN_ADDRESS, SUGGEST_COLUMN_IMAGE_RESOURCE),
intArrayOf(R.id.tvName, R.id.tvAddress, R.id.ivMarkerPin)
)
@ -273,19 +296,35 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
Log.d(TAG, "list with ${mapObjects.size} elements")
val cursor = MatrixCursor(
arrayOf(
SUGGEST_COLUMN_ID, SUGGEST_COLUMN_LAT, SUGGEST_COLUMN_LON, SUGGEST_COLUMN_NAME,
SUGGEST_COLUMN_ADDRESS, SUGGEST_COLUMN_IS_MERCHANT, SUGGEST_COLUMN_IMAGE_RESOURCE
SUGGEST_COLUMN_ID,
SUGGEST_COLUMN_LAT,
SUGGEST_COLUMN_LON,
SUGGEST_COLUMN_NAME,
SUGGEST_COLUMN_ADDRESS,
SUGGEST_COLUMN_IS_MERCHANT,
SUGGEST_COLUMN_IMAGE_RESOURCE
)
)
for (mapObject in mapObjects) {
cursor.addRow(arrayOf(BigInteger(mapObject._id, 16).toLong(), mapObject.lat, mapObject.lon,
mapObject.name, mapObject.address, mapObject.isMerchant,
if (mapObject.isMerchant == 1) R.drawable.ic_merchant_pin else R.drawable.ic_teller_pin))
cursor.addRow(
arrayOf(
BigInteger(mapObject._id, 16).toLong(),
mapObject.lat,
mapObject.lon,
mapObject.name,
mapObject.address,
mapObject.isMerchant,
if (mapObject.isMerchant == 1) R.drawable.ic_merchant_pin else R.drawable.ic_teller_pin
)
)
}
mSearchView?.suggestionsAdapter?.changeCursor(cursor)
}
},
{error -> Log.e(TAG, "Error while retrieving autocomplete suggestions. Msg: $error")})
{ error ->
val message = "Error while retrieving autocomplete suggestions. Msg: $error"
Log.e(TAG, message)
})
)
}
@ -319,7 +358,7 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
if (item.itemId == R.id.menu_filter) {
// Try to show or dismiss the custom popup window with the merchants and tellers switches
if (mPopupWindow?.isShowing == false) {
mPopupWindow?.showAsDropDown(toolbar, screenWidth, 8)
mPopupWindow?.showAsDropDown(binding.toolbar, screenWidth, 8)
if (mMap?.isMyLocationEnabled == true)
mMap?.uiSettings?.isMyLocationButtonEnabled = false
} else
@ -331,7 +370,11 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
/** Handles the result from the location permission request */
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_LOCATION_PERMISSION) {
@ -357,7 +400,7 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
mMap = googleMap
// Add padding to move the controls out of the toolbar/status bar and navigation bar.
mMap?.setPadding(0, toolbar.height + statusBarSize, 0, navigationBarSize)
mMap?.setPadding(0, binding.toolbar.height + statusBarSize, 0, navigationBarSize)
applyMapTheme()
@ -407,9 +450,13 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
private fun verifyLocationPermission() {
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
!= PackageManager.PERMISSION_GRANTED
) {
// Permission is not already granted
requestPermissions(arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), REQUEST_LOCATION_PERMISSION)
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
} else {
// Permission is already granted
mMap?.isMyLocationEnabled = true
@ -512,8 +559,9 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
inner class MerchantInfoWindowAdapter : GoogleMap.InfoWindowAdapter {
override fun getInfoWindow(marker: Marker?): View {
val infoWindowLayout: View = LayoutInflater.from(context).inflate(
R.layout.marker_merch_info_window, null)
val infoWindowLayout: View = LayoutInflater.from(context)
.inflate(R.layout.marker_merch_info_window, null)
val tvName = infoWindowLayout.findViewById<TextView>(R.id.tvName)
val tvAddress = infoWindowLayout.findViewById<TextView>(R.id.tvAddress)
val tvPhone = infoWindowLayout.findViewById<TextView>(R.id.tvPhone)
@ -557,8 +605,9 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, SearchView.OnSuggestio
inner class TellerInfoWindowAdapter : GoogleMap.InfoWindowAdapter {
override fun getInfoWindow(marker: Marker?): View {
val infoWindowLayout: View = LayoutInflater.from(context).inflate(
R.layout.marker_teller_info_window, null)
val infoWindowLayout: View = LayoutInflater.from(context)
.inflate(R.layout.marker_teller_info_window, null)
val tvName = infoWindowLayout.findViewById<TextView>(R.id.tvName)
val tvAddress = infoWindowLayout.findViewById<TextView>(R.id.tvAddress)
val tvPhone = infoWindowLayout.findViewById<TextView>(R.id.tvPhone)

View file

@ -5,16 +5,25 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.databinding.FragmentNetWorthBinding
class NetWorthFragment : Fragment() {
private var _binding: FragmentNetWorthBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_net_worth, container, false)
_binding = FragmentNetWorthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -10,11 +10,11 @@ import android.view.inputmethod.EditorInfo
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.jakewharton.rxbinding3.widget.textChanges
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.databinding.DialogPinSecurityLockBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
import cy.agorise.bitsybitshareswallet.utils.hideKeyboard
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.dialog_pin_security_lock.*
/**
* Contains all the specific logic to create and confirm a new PIN or verifying the validity of the current one.
@ -25,9 +25,22 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
const val TAG = "PINSecurityLockDialog"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
private var _binding: DialogPinSecurityLockBinding? = null
private val binding get() = _binding!!
return inflater.inflate(R.layout.dialog_pin_security_lock, container, false)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogPinSecurityLockBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private var newPIN = ""
@ -39,13 +52,13 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
crashlytics.setCustomKey(Constants.CRASHLYTICS_KEY_LAST_SCREEN, TAG)
// Request focus to the PIN EditText and automatically show the keyboard when the dialog appears.
tietPIN.requestFocus()
binding.tietPIN.requestFocus()
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
setupScreen()
// Listens to the event when the user clicks the 'Enter' button in the keyboard and acts accordingly
tietPIN.setOnEditorActionListener { v, actionId, _ ->
binding.tietPIN.setOnEditorActionListener { v, actionId, _ ->
var handled = false
if (actionId == EditorInfo.IME_ACTION_GO) {
if (currentStep == STEP_SECURITY_LOCK_VERIFY) {
@ -56,8 +69,8 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
if (hashedPIN == currentHashedPINPattern) {
// PIN is correct, proceed
resetIncorrectSecurityLockAttemptsAndTime()
tietPIN.hideKeyboard()
rootView.requestFocus()
binding.tietPIN.hideKeyboard()
binding.rootView.requestFocus()
dismiss()
mCallback?.onPINPatternEntered(actionIdentifier)
} else {
@ -65,7 +78,7 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
if (incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
// Show the error only when the user has not reached the max attempts limit, because if that
// is the case another error is gonna be shown in the setupScreen() method
tilPIN.error = getString(R.string.error__wrong_pin)
binding.tilPIN.error = getString(R.string.error__wrong_pin)
}
setupScreen()
}
@ -81,7 +94,7 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
} else if (currentStep == STEP_SECURITY_LOCK_CONFIRM) {
val pinConfirm = v.text.toString().trim()
if (pinConfirm != newPIN) {
tvTitle.text = getString(R.string.title__pins_dont_match)
binding.tvTitle.text = getString(R.string.title__pins_dont_match)
} else {
val salt = CryptoUtils.generateSalt()
val hashedPIN = CryptoUtils.createSHA256Hash(salt + pinConfirm)
@ -103,20 +116,21 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
}
mDisposables.add(
tietPIN.textChanges()
binding.tietPIN.textChanges()
.skipInitialValue()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (currentStep == STEP_SECURITY_LOCK_VERIFY &&
incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
incorrectSecurityLockAttempts < Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS
) {
// Make sure the error is removed when the user types again
tilPIN.isErrorEnabled = false
binding.tilPIN.isErrorEnabled = false
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
// Show the min length requirement for the PIN only when it has not been fulfilled
if (it.trim().length >= Constants.MIN_PIN_LENGTH) {
tilPIN.helperText = ""
binding.tilPIN.helperText = ""
} else {
tilPIN.helperText = getString(R.string.msg__min_pin_length)
binding.tilPIN.helperText = getString(R.string.msg__min_pin_length)
}
}
}
@ -126,15 +140,15 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
private fun setupScreen() {
when (currentStep) {
STEP_SECURITY_LOCK_VERIFY -> {
tvTitle.text = getString(R.string.title__re_enter_your_pin)
tvSubTitle.text = getString(R.string.msg__enter_your_pin)
tietPIN.isEnabled = true
binding.tvTitle.text = getString(R.string.title__re_enter_your_pin)
binding.tvSubTitle.text = getString(R.string.msg__enter_your_pin)
binding.tietPIN.isEnabled = true
if (incorrectSecurityLockAttempts >= Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
// User has entered the PIN incorrectly too many times
val now = System.currentTimeMillis()
if (now <= incorrectSecurityLockTime + Constants.INCORRECT_SECURITY_LOCK_COOLDOWN) {
tietPIN.setText("")
tietPIN.isEnabled = false
binding.tietPIN.setText("")
binding.tietPIN.isEnabled = false
startContDownTimer()
} else {
resetIncorrectSecurityLockAttemptsAndTime()
@ -142,28 +156,28 @@ class PINSecurityLockDialog : BaseSecurityLockDialog() {
}
}
STEP_SECURITY_LOCK_CREATE -> {
tvTitle.text = getString(R.string.title__set_bitsy_security_lock)
tvSubTitle.text = getString(R.string.msg__set_a_pin)
tilPIN.helperText = getString(R.string.msg__min_pin_length)
tilPIN.isErrorEnabled = false
binding.tvTitle.text = getString(R.string.title__set_bitsy_security_lock)
binding.tvSubTitle.text = getString(R.string.msg__set_a_pin)
binding.tilPIN.helperText = getString(R.string.msg__min_pin_length)
binding.tilPIN.isErrorEnabled = false
}
STEP_SECURITY_LOCK_CONFIRM -> {
tvTitle.text = getString(R.string.title__re_enter_your_pin)
tvSubTitle.text = ""
tvSubTitle.visibility = View.GONE
tietPIN.setText("")
tilPIN.helperText = ""
tilPIN.isErrorEnabled = false
binding.tvTitle.text = getString(R.string.title__re_enter_your_pin)
binding.tvSubTitle.text = ""
binding.tvSubTitle.visibility = View.GONE
binding.tietPIN.setText("")
binding.tilPIN.helperText = ""
binding.tilPIN.isErrorEnabled = false
}
}
}
override fun onTimerSecondPassed(errorMessage: String) {
tilPIN.error = errorMessage
binding.tilPIN.error = errorMessage
}
override fun onTimerFinished() {
setupScreen()
tilPIN.isErrorEnabled = false
binding.tilPIN.isErrorEnabled = false
}
}

View file

@ -5,11 +5,11 @@ import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import cy.agorise.bitsybitshareswallet.R
import kotlinx.android.synthetic.main.dialog_pattern_security_lock.*
import com.andrognito.patternlockview.PatternLockView
import com.andrognito.patternlockview.listener.PatternLockViewListener
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.databinding.DialogPatternSecurityLockBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
@ -23,9 +23,22 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
const val TAG = "PatternSecurityLockDialog"
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
private var _binding: DialogPatternSecurityLockBinding? = null
private val binding get() = _binding!!
return inflater.inflate(R.layout.dialog_pattern_security_lock, container, false)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogPatternSecurityLockBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private var newPattern = ""
@ -38,9 +51,9 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
setupScreen()
patternLockView.addPatternLockListener(mPatternLockViewListener)
binding.patternLockView.addPatternLockListener(mPatternLockViewListener)
btnClear.setOnClickListener { setupScreen() }
binding.btnClear.setOnClickListener { setupScreen() }
}
private val mPatternLockViewListener = object : PatternLockViewListener {
@ -51,11 +64,11 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
setMessage("")
}
STEP_SECURITY_LOCK_CREATE -> {
btnClear.visibility = View.INVISIBLE
binding.btnClear.visibility = View.INVISIBLE
setMessage(getString(R.string.msg__release_finger))
}
STEP_SECURITY_LOCK_CONFIRM -> {
btnClear.visibility = View.INVISIBLE
binding.btnClear.visibility = View.INVISIBLE
setMessage(getString(R.string.msg__release_finger))
}
}
@ -67,8 +80,10 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
override fun onComplete(pattern: List<PatternLockView.Dot>) {
if (currentStep == STEP_SECURITY_LOCK_VERIFY) {
val hashedPattern = CryptoUtils.createSHA256Hash(currentPINPatternSalt +
getStringPattern(pattern))
val hashedPattern = CryptoUtils.createSHA256Hash(
currentPINPatternSalt +
getStringPattern(pattern)
)
if (hashedPattern == currentHashedPINPattern) {
// Pattern is correct, proceed
resetIncorrectSecurityLockAttemptsAndTime()
@ -84,17 +99,17 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
setupScreen()
}
} else if (currentStep == STEP_SECURITY_LOCK_CREATE) {
btnClear.visibility = View.VISIBLE
binding.btnClear.visibility = View.VISIBLE
if (pattern.size < 4) {
setError(getString(R.string.error__connect_at_least_4_dots))
patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
} else {
setMessage(getString(R.string.text__pattern_recorded))
patternLockView.setViewMode(PatternLockView.PatternViewMode.CORRECT)
patternLockView.isInputEnabled = false
btnNext.isEnabled = true
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.CORRECT)
binding.patternLockView.isInputEnabled = false
binding.btnNext.isEnabled = true
newPattern = getStringPattern(pattern)
btnNext.setOnClickListener {
binding.btnNext.setOnClickListener {
currentStep = STEP_SECURITY_LOCK_CONFIRM
setupScreen()
}
@ -103,14 +118,14 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
val patternConfirm = getStringPattern(pattern)
if (patternConfirm != newPattern) {
setError(getString(R.string.error__wront_pattern))
btnNext.isEnabled = false
patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
binding.btnNext.isEnabled = false
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
} else {
setMessage(getString(R.string.msg__your_new_unlock_pattern))
patternLockView.isEnabled = false
patternLockView.setViewMode(PatternLockView.PatternViewMode.CORRECT)
btnNext.isEnabled = true
btnNext.setOnClickListener {
binding.patternLockView.isEnabled = false
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.CORRECT)
binding.btnNext.isEnabled = true
binding.btnNext.setOnClickListener {
context?.let {
val salt = CryptoUtils.generateSalt()
val hashedPattern = CryptoUtils.createSHA256Hash(salt + patternConfirm)
@ -119,7 +134,8 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
PreferenceManager.getDefaultSharedPreferences(it).edit()
.putString(Constants.KEY_HASHED_PIN_PATTERN, hashedPattern)
.putString(Constants.KEY_PIN_PATTERN_SALT, salt)
.putInt(Constants.KEY_SECURITY_LOCK_SELECTED, 2).apply() // 2 -> Pattern
.putInt(Constants.KEY_SECURITY_LOCK_SELECTED, 2)
.apply() // 2 -> Pattern
dismiss()
mCallback?.onPINPatternChanged()
@ -148,17 +164,17 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
private fun setupScreen() {
when (currentStep) {
STEP_SECURITY_LOCK_VERIFY -> {
tvTitle.text = getString(R.string.title__re_enter_your_pattern)
tvSubTitle.text = getString(R.string.msg__enter_your_pattern)
btnClear.visibility = View.GONE
btnNext.visibility = View.GONE
patternLockView.isInputEnabled = true
patternLockView.isInStealthMode = true
binding.tvTitle.text = getString(R.string.title__re_enter_your_pattern)
binding.tvSubTitle.text = getString(R.string.msg__enter_your_pattern)
binding.btnClear.visibility = View.GONE
binding.btnNext.visibility = View.GONE
binding.patternLockView.isInputEnabled = true
binding.patternLockView.isInStealthMode = true
if (incorrectSecurityLockAttempts >= Constants.MAX_INCORRECT_SECURITY_LOCK_ATTEMPTS) {
// User has entered the Pattern incorrectly too many times
val now = System.currentTimeMillis()
if (now <= incorrectSecurityLockTime + Constants.INCORRECT_SECURITY_LOCK_COOLDOWN) {
patternLockView.isInputEnabled = false
binding.patternLockView.isInputEnabled = false
startContDownTimer()
} else {
resetIncorrectSecurityLockAttemptsAndTime()
@ -166,24 +182,24 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
}
}
STEP_SECURITY_LOCK_CREATE -> {
tvTitle.text = getString(R.string.title__set_bitsy_security_lock)
tvSubTitle.text = getString(R.string.msg__set_a_pattern)
binding.tvTitle.text = getString(R.string.title__set_bitsy_security_lock)
binding.tvSubTitle.text = getString(R.string.msg__set_a_pattern)
setMessage(getString(R.string.text__draw_an_unlock_pattern))
patternLockView.clearPattern()
patternLockView.isInputEnabled = true
btnClear.visibility = View.INVISIBLE
btnNext.isEnabled = false
binding.patternLockView.clearPattern()
binding.patternLockView.isInputEnabled = true
binding.btnClear.visibility = View.INVISIBLE
binding.btnNext.isEnabled = false
}
STEP_SECURITY_LOCK_CONFIRM -> {
tvTitle.text = getString(R.string.title__re_enter_your_pattern)
tvSubTitle.text = ""
binding.tvTitle.text = getString(R.string.title__re_enter_your_pattern)
binding.tvSubTitle.text = ""
setMessage(getString(R.string.msg__draw_pattern_confirm))
tvSubTitle.visibility = View.GONE
patternLockView.clearPattern()
patternLockView.isInputEnabled = true
btnClear.visibility = View.INVISIBLE
btnNext.isEnabled = false
btnNext.text = getString(R.string.button__confirm)
binding.tvSubTitle.visibility = View.GONE
binding.patternLockView.clearPattern()
binding.patternLockView.isInputEnabled = true
binding.btnClear.visibility = View.INVISIBLE
binding.btnNext.isEnabled = false
binding.btnNext.text = getString(R.string.button__confirm)
}
}
}
@ -191,21 +207,21 @@ class PatternSecurityLockDialog : BaseSecurityLockDialog() {
@Suppress("DEPRECATION")
private fun setMessage(message: String) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
tvMessage.setTextAppearance(context, R.style.TextAppearance_Bitsy_Body2)
binding.tvMessage.setTextAppearance(context, R.style.TextAppearance_Bitsy_Body2)
} else {
tvMessage.setTextAppearance(R.style.TextAppearance_Bitsy_Body2)
binding.tvMessage.setTextAppearance(R.style.TextAppearance_Bitsy_Body2)
}
tvMessage.text = message
binding.tvMessage.text = message
}
@Suppress("DEPRECATION")
private fun setError(error: String) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
tvMessage.setTextAppearance(context, R.style.TextAppearance_Bitsy_Body2_Error)
binding.tvMessage.setTextAppearance(context, R.style.TextAppearance_Bitsy_Body2_Error)
} else {
tvMessage.setTextAppearance(R.style.TextAppearance_Bitsy_Body2_Error)
binding.tvMessage.setTextAppearance(R.style.TextAppearance_Bitsy_Body2_Error)
}
tvMessage.text = error
binding.tvMessage.text = error
}
override fun onTimerSecondPassed(errorMessage: String) {

View file

@ -19,6 +19,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.AssetsAdapter
import cy.agorise.bitsybitshareswallet.adapters.AutoSuggestAssetAdapter
import cy.agorise.bitsybitshareswallet.databinding.FragmentReceiveTransactionBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.Helper
import cy.agorise.bitsybitshareswallet.utils.showKeyboard
@ -29,8 +30,6 @@ import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.ListAssets
import cy.agorise.graphenej.models.JsonRpcResponse
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_receive_transaction.*
import java.lang.Exception
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
@ -53,6 +52,9 @@ class ReceiveTransactionFragment : ConnectedFragment() {
private const val OTHER_ASSET = "other_asset"
}
private var _binding: FragmentReceiveTransactionBinding? = null
private val binding get() = _binding!!
private lateinit var mViewModel: ReceiveTransactionViewModel
/** Current user account */
@ -75,7 +77,11 @@ class ReceiveTransactionFragment : ConnectedFragment() {
// 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? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
@ -88,13 +94,21 @@ class ReceiveTransactionFragment : ConnectedFragment() {
// Sets the status and navigation bars background color to a dark green or just dark
val window = activity?.window
context?.let { context ->
val statusBarColor = ContextCompat.getColor(context,
if (!nightMode) R.color.colorReceiveDark else R.color.colorStatusBarDark)
val statusBarColor = ContextCompat.getColor(
context,
if (!nightMode) R.color.colorReceiveDark else R.color.colorStatusBarDark
)
window?.statusBarColor = statusBarColor
window?.navigationBarColor = statusBarColor
}
return inflater.inflate(R.layout.fragment_receive_transaction, container, false)
_binding = FragmentReceiveTransactionBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -121,8 +135,10 @@ class ReceiveTransactionFragment : ConnectedFragment() {
// Add BTS to always show a QR
if (mAssets.isEmpty())
mAssets.add(cy.agorise.bitsybitshareswallet.database.entities.Asset(
"1.3.0", "BTS", 5, "", "")
mAssets.add(
cy.agorise.bitsybitshareswallet.database.entities.Asset(
"1.3.0", "BTS", 5, "", ""
)
)
mAssets.sortWith(
@ -135,33 +151,39 @@ class ReceiveTransactionFragment : ConnectedFragment() {
)
mAssets.add(asset)
mAssetsAdapter = AssetsAdapter(context!!, android.R.layout.simple_spinner_item, mAssets)
spAsset.adapter = mAssetsAdapter
mAssetsAdapter =
AssetsAdapter(context!!, android.R.layout.simple_spinner_item, mAssets)
binding.spAsset.adapter = mAssetsAdapter
// Try to select the selectedAssetSymbol
for (i in 0 until mAssetsAdapter!!.count) {
if (mAssetsAdapter!!.getItem(i)!!.symbol == selectedAssetSymbol) {
spAsset.setSelection(i)
binding.spAsset.setSelection(i)
break
}
}
})
mViewModel.qrCodeBitmap.observe(this, Observer { bitmap ->
ivQR.setImageBitmap(bitmap)
binding.ivQR.setImageBitmap(bitmap)
})
spAsset.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
binding.spAsset.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
mAssetsAdapter?.getItem(position)?.let { asset ->
if (asset.id == OTHER_ASSET) {
tilAsset.visibility = View.VISIBLE
actvAsset.showKeyboard()
binding.tilAsset.visibility = View.VISIBLE
binding.actvAsset.showKeyboard()
mAsset = null
} else {
tilAsset.visibility = View.GONE
binding.tilAsset.visibility = View.GONE
selectedAssetSymbol = asset.symbol
mAsset = Asset(asset.id, asset.toString(), asset.precision)
@ -174,20 +196,21 @@ class ReceiveTransactionFragment : ConnectedFragment() {
// Use RxJava Debounce to create QR code only after the user stopped typing an amount
mDisposables.add(
tietAmount.textChanges()
binding.tietAmount.textChanges()
.debounce(1000, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ updateQR() }, { mAsset = null })
)
// Add adapter to the Assets AutoCompleteTextView
mAutoSuggestAssetAdapter = AutoSuggestAssetAdapter(context!!, android.R.layout.simple_dropdown_item_1line)
actvAsset.setAdapter(mAutoSuggestAssetAdapter)
mAutoSuggestAssetAdapter =
AutoSuggestAssetAdapter(context!!, android.R.layout.simple_dropdown_item_1line)
binding.actvAsset.setAdapter(mAutoSuggestAssetAdapter)
// Use RxJava Debounce to avoid making calls to the NetworkService on every text change event and also avoid
// the first call when the View is created
mDisposables.add(
actvAsset.textChanges()
binding.actvAsset.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.toString().trim().toUpperCase() }
@ -201,15 +224,18 @@ class ReceiveTransactionFragment : ConnectedFragment() {
// Get a list of assets that match the already typed string by the user
if (it.length > 1 && mNetworkService != null) {
val id = mNetworkService?.sendMessage(ListAssets(it, AUTO_SUGGEST_ASSET_LIMIT),
ListAssets.REQUIRED_API)
val id = mNetworkService?.sendMessage(
ListAssets(it, AUTO_SUGGEST_ASSET_LIMIT),
ListAssets.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_LIST_ASSETS)
}
}, { mAsset = null })
)
actvAsset.setOnItemClickListener { parent, _, position, _ ->
val asset = parent.adapter.getItem(position) as cy.agorise.bitsybitshareswallet.database.entities.Asset
binding.actvAsset.setOnItemClickListener { parent, _, position, _ ->
val asset =
parent.adapter.getItem(position) as cy.agorise.bitsybitshareswallet.database.entities.Asset
mAsset = Asset(asset.id, asset.toString(), asset.precision)
selectedInAutoCompleteTextView = true
updateQR()
@ -261,7 +287,7 @@ class ReceiveTransactionFragment : ConnectedFragment() {
private fun updateQR() {
if (mAsset == null) {
ivQR.setImageDrawable(null)
binding.ivQR.setImageDrawable(null)
// TODO clean the please pay and to text at the bottom too
return
}
@ -270,7 +296,7 @@ class ReceiveTransactionFragment : ConnectedFragment() {
// Try to obtain the amount from the Amount Text Field or make it zero otherwise
val amount: Long = try {
val tmpAmount = tietAmount.text.toString().toDouble()
val tmpAmount = binding.tietAmount.text.toString().toDouble()
(tmpAmount * Math.pow(10.0, asset.precision.toDouble())).toLong()
} catch (e: Exception) {
0
@ -279,11 +305,13 @@ class ReceiveTransactionFragment : ConnectedFragment() {
val total = AssetAmount(UnsignedLong.valueOf(amount), asset)
val totalInDouble = Util.fromBase(total)
val items = arrayOf(LineItem("transfer", 1, totalInDouble))
val invoice = Invoice(mUserAccount?.name, "", "",
asset.symbol.replaceFirst("bit", ""), items, "", "")
val invoice = Invoice(
mUserAccount?.name, "", "",
asset.symbol.replaceFirst("bit", ""), items, "", ""
)
Log.d(TAG, "invoice: " + invoice.toJsonString())
try {
mViewModel.updateInvoice(invoice, min(ivQR.width, ivQR.height))
mViewModel.updateInvoice(invoice, min(binding.ivQR.width, binding.ivQR.height))
updateAmountAddressUI(amount, asset.symbol, asset.precision, mUserAccount!!.name)
} catch (e: NullPointerException) {
Log.e(TAG, "NullPointerException. Msg: " + e.message)
@ -294,7 +322,12 @@ class ReceiveTransactionFragment : ConnectedFragment() {
/**
* Updates the UI to show the amount and account to send the payment
*/
private fun updateAmountAddressUI(assetAmount: Long, assetSymbol: String, assetPrecision: Int, account: String) {
private fun updateAmountAddressUI(
assetAmount: Long,
assetSymbol: String,
assetPrecision: Int,
account: String
) {
val txtAmount: String = if (assetAmount == 0L) {
getString(R.string.template__please_send, getString(R.string.text__any_amount), " ")
} else {
@ -309,8 +342,8 @@ class ReceiveTransactionFragment : ConnectedFragment() {
val txtAccount = getString(R.string.template__to, account)
tvPleasePay.text = txtAmount
tvTo.text = txtAccount
binding.tvPleasePay.text = txtAmount
binding.tvTo.text = txtAccount
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -332,18 +365,28 @@ class ReceiveTransactionFragment : ConnectedFragment() {
}
private fun verifyStoragePermission() {
if (ContextCompat.checkSelfPermission(activity!!, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(
activity!!,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
!= PackageManager.PERMISSION_GRANTED
) {
// Permission is not already granted
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION)
requestPermissions(
arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION
)
} else {
// Permission is already granted
shareQRScreenshot()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) {
@ -367,13 +410,13 @@ class ReceiveTransactionFragment : ConnectedFragment() {
return
// Get Screenshot
val screenshot = Helper.loadBitmapFromView(container)
val screenshot = Helper.loadBitmapFromView(binding.container)
val imageUri = Helper.saveTemporalBitmap(context!!, screenshot)
// Prepare information for share intent
val subject = getString(R.string.msg__invoice_subject, mUserAccount?.name)
val content = tvPleasePay.text.toString() + "\n" +
tvTo.text.toString()
val content = binding.tvPleasePay.text.toString() + "\n" +
binding.tvTo.text.toString()
// Create share intent and call it
val shareIntent = Intent()

View file

@ -27,6 +27,7 @@ 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.databinding.FragmentSendTransactionBinding
import cy.agorise.bitsybitshareswallet.utils.*
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.SendTransactionViewModel
@ -43,7 +44,6 @@ 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
@ -74,6 +74,9 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
private const val ACTION_SEND_TRANSFER = 1
}
private var _binding: FragmentSendTransactionBinding? = null
private val binding get() = _binding!!
// Navigation AAC Safe Args
private val args: SendTransactionFragmentArgs by navArgs()
@ -112,7 +115,11 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
/** 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? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
@ -125,13 +132,21 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
// 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)
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)
_binding = FragmentSendTransactionBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -168,37 +183,43 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
}, 500)
}
fabOpenCamera.setOnClickListener { if (isCameraPreviewVisible) stopCameraPreview() else verifyCameraPermission() }
binding.fabOpenCamera.setOnClickListener { if (isCameraPreviewVisible) stopCameraPreview() else verifyCameraPermission() }
// Configure BalanceDetailViewModel to show the current balances
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
mBalanceDetailViewModel =
ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
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
mBalancesDetailsAdapter = BalancesDetailsAdapter(
context!!,
android.R.layout.simple_spinner_item,
mBalancesDetails
)
binding.spAsset.adapter = mBalancesDetailsAdapter
// Try to select the selectedAssetSymbol
for (i in 0 until mBalancesDetailsAdapter!!.count) {
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == selectedAssetSymbol) {
spAsset.setSelection(i)
binding.spAsset.setSelection(i)
break
}
}
})
spAsset.onItemSelectedListener = assetItemSelectedListener
binding.spAsset.onItemSelectedListener = assetItemSelectedListener
fabSendTransaction.setOnClickListener { verifySecurityLockSendTransfer() }
fabSendTransaction.disable(R.color.lightGray)
binding.fabSendTransaction.setOnClickListener { verifySecurityLockSendTransfer() }
binding.fabSendTransaction.disable(R.color.lightGray)
// Use RxJava Debounce to avoid making calls to the NetworkService on every text change event
mDisposables.add(
tietTo.textChanges()
binding.tietTo.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.toString().trim() }
@ -209,7 +230,7 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
// Use RxJava Debounce to update the Amount error only after the user stops writing for > 500 ms
mDisposables.add(
tietAmount.textChanges()
binding.tietAmount.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -218,13 +239,13 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
// Use RxJava Debounce to update the Memo error, to make sure it has the correct length
mDisposables.add(
tietMemo.textChanges()
binding.tietMemo.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.map { it.toString().trim() }
.subscribe {
isMemoCorrect = it.length <= tilMemo.counterMaxLength
isMemoCorrect = it.length <= binding.tilMemo.counterMaxLength
enableDisableSendFAB()
}
)
@ -252,8 +273,12 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
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())
binding.tvAvailableAssetAmount.text =
String.format(
"%." + Math.min(balance.precision, 8) + "f %s",
amount,
balance.toString()
)
validateAmount()
}
@ -285,12 +310,12 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
if (result is AccountProperties) {
mSelectedUserAccount = UserAccount(result.id, result.name)
destinationPublicKey = result.active.keyAuths.keys.iterator().next()
tilTo.isErrorEnabled = false
binding.tilTo.isErrorEnabled = false
isToAccountCorrect = true
} else {
mSelectedUserAccount = null
destinationPublicKey = null
tilTo.error = getString(R.string.error__invalid_account)
binding.tilTo.error = getString(R.string.error__invalid_account)
isToAccountCorrect = false
}
@ -320,7 +345,10 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
val feeAsset = Asset(Constants.CORE_ASSET)
val id = mNetworkService?.sendMessage(GetRequiredFees(transaction!!, feeAsset), GetRequiredFees.REQUIRED_API)
val id = mNetworkService?.sendMessage(
GetRequiredFees(transaction!!, feeAsset),
GetRequiredFees.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_GET_REQUIRED_FEES)
} else {
context?.toast(getString(R.string.msg__transaction_not_sent))
@ -334,7 +362,10 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
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)
val id = mNetworkService?.sendMessage(
BroadcastTransaction(transaction),
BroadcastTransaction.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_BROADCAST_TRANSACTION)
} else {
context?.toast(getString(R.string.msg__transaction_not_sent))
@ -357,9 +388,13 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
/** Verifies if the user has already granted the Camera permission, if not the asks for it */
private fun verifyCameraPermission() {
if (ContextCompat.checkSelfPermission(activity!!, android.Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
!= PackageManager.PERMISSION_GRANTED
) {
// Permission is not already granted
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)
requestPermissions(
arrayOf(android.Manifest.permission.CAMERA),
REQUEST_CAMERA_PERMISSION
)
} else {
// Permission is already granted
startCameraPreview()
@ -367,7 +402,11 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
}
/** Handles the result from the camera permission request */
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CAMERA_PERMISSION) {
@ -381,27 +420,27 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
}
private fun startCameraPreview() {
cameraPreview.visibility = View.VISIBLE
fabOpenCamera.setImageResource(R.drawable.ic_close)
binding.cameraPreview.visibility = View.VISIBLE
binding.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.colorSecondary)
cameraPreview.setMaskColor(R.color.colorSecondary)
cameraPreview.setResultHandler(this)
cameraPreview.startCamera()
binding.cameraPreview.setFormats(listOf(BarcodeFormat.QR_CODE))
binding.cameraPreview.setAspectTolerance(0.5f)
binding.cameraPreview.setAutoFocus(true)
binding.cameraPreview.setLaserColor(R.color.colorSecondary)
binding.cameraPreview.setMaskColor(R.color.colorSecondary)
binding.cameraPreview.setResultHandler(this)
binding.cameraPreview.startCamera()
cameraPreview.scrollY = holderCamera.width / 6
binding.cameraPreview.scrollY = binding.holderCamera.width / 6
}
private fun stopCameraPreview() {
cameraPreview.visibility = View.INVISIBLE
fabOpenCamera.setImageResource(R.drawable.ic_camera)
binding.cameraPreview.visibility = View.INVISIBLE
binding.fabOpenCamera.setImageResource(R.drawable.ic_camera)
isCameraPreviewVisible = false
cameraPreview.stopCamera()
binding.cameraPreview.stopCamera()
}
/** Handles the result of the QR code read from the camera **/
@ -417,12 +456,12 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
Log.d(TAG, "QR Code read: " + invoice.toJsonString())
tietTo.setText(invoice.to)
binding.tietTo.setText(invoice.to)
if (invoice.memo != null) {
tietMemo.setText(invoice.memo)
binding.tietMemo.setText(invoice.memo)
if (invoice.memo.startsWith("PP"))
tietMemo.isEnabled = false
binding.tietMemo.isEnabled = false
}
var balanceDetail: BalanceDetail? = null
@ -432,8 +471,9 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
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)
mBalancesDetailsAdapter?.getItem(i)?.symbol)
) {
binding.spAsset.setSelection(i)
balanceDetail = mBalancesDetailsAdapter?.getItem(i)
break
}
@ -442,8 +482,11 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
// 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()
Snackbar.make(
binding.rootView,
getString(R.string.error__you_dont_own_asset, invoice.currency.toUpperCase()),
Snackbar.LENGTH_INDEFINITE
).setAction(android.R.string.ok) { }.show()
return
}
@ -457,9 +500,9 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
val df = DecimalFormat("####." + "#".repeat(balanceDetail.precision))
df.roundingMode = RoundingMode.CEILING
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
tietAmount.setText(df.format(amount))
binding.tietAmount.setText(df.format(amount))
} else {
tietAmount.setText("")
binding.tietAmount.setText("")
}
} catch (e: Exception) {
@ -473,15 +516,19 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
*/
private fun validateAccount(accountName: String) {
isToAccountCorrect = false
val id = mNetworkService?.sendMessage(GetAccountByName(accountName), GetAccountByName.REQUIRED_API)
val id = mNetworkService?.sendMessage(
GetAccountByName(accountName),
GetAccountByName.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_GET_ACCOUNT_BY_NAME)
}
private fun validateAmount() {
val txtAmount = tietAmount.text.toString().replace(",", ".")
val txtAmount = binding.tietAmount.text.toString().replace(",", ".")
if (mBalancesDetailsAdapter?.isEmpty != false) return
val balance = mBalancesDetailsAdapter?.getItem(spAsset.selectedItemPosition) ?: return
val balance =
mBalancesDetailsAdapter?.getItem(binding.spAsset.selectedItemPosition) ?: return
val currentAmount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
val amount: Double = try {
@ -492,15 +539,15 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
when {
currentAmount < amount -> {
tilAmount.error = getString(R.string.error__not_enough_funds)
binding.tilAmount.error = getString(R.string.error__not_enough_funds)
isAmountCorrect = false
}
amount == 0.0 -> {
tilAmount.isErrorEnabled = false
binding.tilAmount.isErrorEnabled = false
isAmountCorrect = false
}
else -> {
tilAmount.isErrorEnabled = false
binding.tilAmount.isErrorEnabled = false
isAmountCorrect = true
}
}
@ -510,11 +557,11 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
private fun enableDisableSendFAB() {
if (isToAccountCorrect && isAmountCorrect && isMemoCorrect) {
fabSendTransaction.enable(R.color.colorSend)
vSend.setBackgroundResource(R.drawable.send_fab_background)
binding.fabSendTransaction.enable(R.color.colorSend)
binding.vSend.setBackgroundResource(R.drawable.send_fab_background)
} else {
fabSendTransaction.disable(R.color.lightGray)
vSend.setBackgroundResource(R.drawable.send_fab_background_disabled)
binding.fabSendTransaction.disable(R.color.lightGray)
binding.vSend.setBackgroundResource(R.drawable.send_fab_background_disabled)
}
}
@ -528,8 +575,10 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
// 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_STEP_SECURITY_LOCK,
BaseSecurityLockDialog.STEP_SECURITY_LOCK_VERIFY
)
args.putInt(BaseSecurityLockDialog.KEY_ACTION_IDENTIFIER, ACTION_SEND_TRANSFER)
when (securityLockSelected) {
@ -556,15 +605,16 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
}
}
override fun onPINPatternChanged() { /* Do nothing */ }
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().replace(",", ".").toDouble()
val balance = mBalancesDetailsAdapter!!.getItem(binding.spAsset.selectedItemPosition)!!
val amount = (binding.tietAmount.text.toString().replace(",", ".").toDouble()
* Math.pow(10.0, balance.precision.toDouble())).toLong()
val transferAmount = AssetAmount(UnsignedLong.valueOf(amount), Asset(balance.id))
@ -574,13 +624,16 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
.setDestination(mSelectedUserAccount)
.setTransferAmount(transferAmount)
val privateKey = ECKey.fromPrivate(DumpedPrivateKey.fromBase58(null, wifKey).key.privKeyBytes)
val privateKey =
ECKey.fromPrivate(DumpedPrivateKey.fromBase58(null, wifKey).key.privKeyBytes)
// Add memo if it is not empty
val memoMsg = tietMemo.text.toString()
val memoMsg = binding.tietMemo.text.toString()
if (memoMsg.isNotEmpty()) {
val nonce = Math.abs(SecureRandomGenerator.getSecureRandom().nextLong()).toBigInteger()
val encryptedMemo = Memo.encryptMessage(privateKey, destinationPublicKey!!, nonce, memoMsg)
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)
@ -601,8 +654,10 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
transaction = Transaction(privateKey, null, operations)
// Start the send transaction procedure which includes a series of calls
val id = mNetworkService?.sendMessage(GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API)
val id = mNetworkService?.sendMessage(
GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API
)
if (id != null) responseMap.append(id, RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES)
} else
Log.d(TAG, "Network Service is not connected")
@ -634,8 +689,12 @@ class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHand
return null
// 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
if (Constants.assetsWhichSendFeeToAgorise.contains(
transferOperation.assetAmount?.asset?.objectId ?: ""
)
) {
val fee =
transferOperation.assetAmount?.multiplyBy(Constants.FEE_PERCENTAGE) ?: return null
return TransferOperationBuilder()
.setSource(mUserAccount)

View file

@ -1,6 +1,6 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.content.*
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
@ -10,6 +10,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.collection.LongSparseArray
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
@ -22,6 +23,7 @@ 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
@ -36,7 +38,6 @@ import cy.agorise.graphenej.models.JsonRpcResponse
import cy.agorise.graphenej.operations.AccountUpgradeOperationBuilder
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.fragment_settings.*
import org.bitcoinj.core.DumpedPrivateKey
import org.bitcoinj.core.ECKey
import java.text.NumberFormat
@ -59,6 +60,9 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
private const val RESPONSE_BROADCAST_TRANSACTION = 3
}
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private lateinit var mViewModel: SettingsFragmentViewModel
private var mUserAccount: UserAccount? = null
@ -82,7 +86,11 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
private val mHandler = Handler()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
@ -92,7 +100,13 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.setBackgroundResource(if (!nightMode) R.color.colorPrimary else R.color.colorToolbarDark)
return inflater.inflate(R.layout.fragment_settings, container, false)
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -111,7 +125,8 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
androidx.lifecycle.Observer<cy.agorise.bitsybitshareswallet.database.entities.UserAccount> { userAccount ->
if (userAccount != null) {
mUserAccount = UserAccount(userAccount.id, userAccount.name)
btnUpgradeToLTM.isEnabled = !userAccount.isLtm // Disable button if already LTM
binding.btnUpgradeToLTM.isEnabled =
!userAccount.isLtm // Disable button if already LTM
}
})
@ -121,7 +136,10 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
try {
privateKey = CryptoUtils.decrypt(it, encryptedWIF)
} catch (e: AEADBadTagException) {
Log.e(TAG, "AEADBadTagException. Class: " + e.javaClass + ", Msg: " + e.message)
Log.e(
TAG,
"AEADBadTagException. Class: " + e.javaClass + ", Msg: " + e.message
)
} catch (e: IllegalStateException) {
crashlytics.recordException(e)
}
@ -132,7 +150,7 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
initNightModeSwitch()
tvNetworkStatus.setOnClickListener { v -> showNodesDialog(v) }
binding.tvNetworkStatus.setOnClickListener { v -> showNodesDialog(v) }
// Obtain the current Security Lock Option selected and display it in the screen
val securityLockSelected = PreferenceManager.getDefaultSharedPreferences(context)
@ -142,12 +160,13 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
// 1 -> PIN
// 2 -> Pattern
tvSecurityLockSelected.text = resources.getStringArray(R.array.security_lock_options)[securityLockSelected]
binding.tvSecurityLockSelected.text =
resources.getStringArray(R.array.security_lock_options)[securityLockSelected]
tvSecurityLock.setOnClickListener { onSecurityLockTextSelected() }
tvSecurityLockSelected.setOnClickListener { onSecurityLockTextSelected() }
binding.tvSecurityLock.setOnClickListener { onSecurityLockTextSelected() }
binding.tvSecurityLockSelected.setOnClickListener { onSecurityLockTextSelected() }
btnViewBrainKey.setOnClickListener { onShowBrainKeyButtonSelected() }
binding.btnViewBrainKey.setOnClickListener { onShowBrainKeyButtonSelected() }
val lastAccountBackup = PreferenceManager.getDefaultSharedPreferences(context)
.getLong(Constants.KEY_LAST_ACCOUNT_BACKUP, 0L)
@ -155,11 +174,11 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
val now = System.currentTimeMillis()
if (lastAccountBackup + Constants.ACCOUNT_BACKUP_PERIOD < now)
tvBackupWarning.visibility = View.VISIBLE
binding.tvBackupWarning.visibility = View.VISIBLE
btnUpgradeToLTM.setOnClickListener { onUpgradeToLTMButtonSelected() }
binding.btnUpgradeToLTM.setOnClickListener { onUpgradeToLTMButtonSelected() }
btnRemoveAccount.setOnClickListener { onRemoveAccountButtonSelected() }
binding.btnRemoveAccount.setOnClickListener { onRemoveAccountButtonSelected() }
}
private fun showNodesDialog(v: View) {
@ -191,7 +210,13 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
mNodesDialogLinearLayoutManager = LinearLayoutManager(v.context)
mNodesDialog = MaterialDialog(v.context).show {
title(text = String.format("%s v%s", getString(R.string.app_name), BuildConfig.VERSION_NAME))
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)
@ -218,8 +243,12 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
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_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)
@ -238,13 +267,17 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
}
private fun showConnectedState() {
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
resources.getDrawable(R.drawable.ic_connected, null), null)
binding.tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null,
ResourcesCompat.getDrawable(resources, R.drawable.ic_connected, null), null
)
}
private fun showDisconnectedState() {
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
resources.getDrawable(R.drawable.ic_disconnected, null), null)
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
@ -253,7 +286,9 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
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))
mNodesDialog?.message(
text = getString(R.string.title__bitshares_nodes_dialog, blockNumber)
)
}
}
}
@ -266,7 +301,10 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
ltmTransaction?.blockData = BlockData(headBlockNumber, headBlockId, expirationTime)
val id = mNetworkService?.sendMessage(BroadcastTransaction(ltmTransaction), BroadcastTransaction.REQUIRED_API)
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
@ -305,7 +343,10 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
*/
private val mRequestDynamicGlobalPropertiesTask = object : Runnable {
override fun run() {
val id = mNetworkService?.sendMessage(GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API)
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)
@ -320,9 +361,9 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
val autoCloseOn = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, true)
switchAutoClose.isChecked = autoCloseOn
binding.switchAutoClose.isChecked = autoCloseOn
switchAutoClose.setOnCheckedChangeListener { buttonView, isChecked ->
binding.switchAutoClose.setOnCheckedChangeListener { buttonView, isChecked ->
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
.putBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, isChecked).apply()
}
@ -337,9 +378,9 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
val nightModeOn = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
switchNightMode.isChecked = nightModeOn
binding.switchNightMode.isChecked = nightModeOn
switchNightMode.setOnCheckedChangeListener { buttonView, isChecked ->
binding.switchNightMode.setOnCheckedChangeListener { buttonView, isChecked ->
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
.putBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, isChecked).apply()
@ -371,8 +412,10 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
// 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_STEP_SECURITY_LOCK,
BaseSecurityLockDialog.STEP_SECURITY_LOCK_VERIFY
)
args.putInt(BaseSecurityLockDialog.KEY_ACTION_IDENTIFIER, actionIdentifier)
return when (securityLockSelected) {
@ -412,7 +455,8 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
// 1 -> PIN
// 2 -> Pattern
tvSecurityLockSelected.text = resources.getStringArray(R.array.security_lock_options)[securityLockSelected]
binding.tvSecurityLockSelected.text =
resources.getStringArray(R.array.security_lock_options)[securityLockSelected]
}
/**
@ -430,17 +474,23 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
context?.let {
MaterialDialog(it).show {
title(R.string.title__security_dialog)
listItemsSingleChoice(R.array.security_lock_options, initialSelection = securityLockSelected) {_, index, _ ->
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_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).apply() // 0 -> None
.putInt(Constants.KEY_SECURITY_LOCK_SELECTED, 0)
.apply() // 0 -> None
// Call this function to update the UI
onPINPatternChanged()
@ -491,7 +541,8 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
.subscribeOn(Schedulers.io())
.map { authority ->
val plainBrainKey = CryptoUtils.decrypt(it, authority.encryptedBrainKey)
val plainSequenceNumber = CryptoUtils.decrypt(it, authority.encryptedSequenceNumber)
val plainSequenceNumber =
CryptoUtils.decrypt(it, authority.encryptedSequenceNumber)
val sequenceNumber = Integer.parseInt(plainSequenceNumber)
BrainKey(plainBrainKey, sequenceNumber)
}
@ -518,7 +569,7 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
val now = System.currentTimeMillis()
PreferenceManager.getDefaultSharedPreferences(it.context).edit()
.putLong(Constants.KEY_LAST_ACCOUNT_BACKUP, now).apply()
tvBackupWarning.visibility = View.GONE
binding.tvBackupWarning.visibility = View.GONE
}
dialog.show()
@ -541,11 +592,18 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
operations.add(operation)
val currentPrivateKey = ECKey.fromPrivate(
DumpedPrivateKey.fromBase58(null, privateKey).key.privKeyBytes)
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)
val id = mNetworkService?.sendMessage(
GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API
)
if (id != null) responseMap.append(
id,
RESPONSE_GET_DYNAMIC_GLOBAL_PROPERTIES_LTM
)
}
}
}
@ -574,7 +632,8 @@ class SettingsFragment : ConnectedFragment(), BaseSecurityLockDialog.OnPINPatter
// Marks the license as agreed, so that it is not shown to the user again.
pref.edit().putInt(
Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION).apply()
Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION
).apply()
// Restarts the activity, which will restart the whole application since it uses a
// single activity architecture.

View file

@ -21,12 +21,12 @@ import com.jakewharton.rxbinding3.appcompat.queryTextChangeEvents
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.databinding.FragmentTransactionsBinding
import cy.agorise.bitsybitshareswallet.models.FilterOptions
import cy.agorise.bitsybitshareswallet.utils.*
import cy.agorise.bitsybitshareswallet.viewmodels.TransactionsViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_transactions.*
import java.io.File
import java.util.concurrent.TimeUnit
@ -42,14 +42,27 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
}
private var _binding: FragmentTransactionsBinding? = null
private val binding get() = _binding!!
private lateinit var mViewModel: TransactionsViewModel
private var mDisposables = CompositeDisposable()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_transactions, container, false)
_binding = FragmentTransactionsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -62,8 +75,8 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
val transfersDetailsAdapter = TransfersDetailsAdapter(context!!)
rvTransactions.adapter = transfersDetailsAdapter
rvTransactions.layoutManager = LinearLayoutManager(context)
binding.rvTransactions.adapter = transfersDetailsAdapter
binding.rvTransactions.layoutManager = LinearLayoutManager(context)
// Configure TransactionsViewModel to fetch the transaction history
mViewModel = ViewModelProviders.of(this).get(TransactionsViewModel::class.java)
@ -71,11 +84,11 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
mViewModel.getFilteredTransactions(userId).observe(this,
Observer<List<TransferDetail>> { transactions ->
if (transactions.isEmpty()) {
rvTransactions.visibility = View.GONE
tvEmpty.visibility = View.VISIBLE
binding.rvTransactions.visibility = View.GONE
binding.tvEmpty.visibility = View.VISIBLE
} else {
rvTransactions.visibility = View.VISIBLE
tvEmpty.visibility = View.GONE
binding.rvTransactions.visibility = View.VISIBLE
binding.tvEmpty.visibility = View.GONE
val shouldScrollUp = transactions.size - transfersDetailsAdapter.itemCount == 1
transfersDetailsAdapter.replaceAll(transactions)
@ -83,13 +96,13 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
// Scroll to the top only if the difference between old and new items is 1
// which most likely means a new transaction was received/sent.
if (shouldScrollUp)
rvTransactions.scrollToPosition(0)
binding.rvTransactions.scrollToPosition(0)
}
})
// Set custom touch listener to handle bounce/stretch effect
val bounceTouchListener = BounceTouchListener(rvTransactions)
rvTransactions.setOnTouchListener(bounceTouchListener)
val bounceTouchListener = BounceTouchListener(binding.rvTransactions)
binding.rvTransactions.setOnTouchListener(bounceTouchListener)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -118,7 +131,10 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
R.id.menu_filter -> {
val filterOptionsDialog = FilterOptionsDialog()
val args = Bundle()
args.putParcelable(FilterOptionsDialog.KEY_FILTER_OPTIONS, mViewModel.getFilterOptions())
args.putParcelable(
FilterOptionsDialog.KEY_FILTER_OPTIONS,
mViewModel.getFilterOptions()
)
filterOptionsDialog.arguments = args
filterOptionsDialog.show(childFragmentManager, "filter-options-tag")
true
@ -152,11 +168,17 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
/** Verifies that the storage permission has been granted before attempting to generate the export options */
private fun verifyStoragePermission() {
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(
activity!!,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
!= PackageManager.PERMISSION_GRANTED
) {
// Permission is not already granted
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION)
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION
)
} else {
// Permission is already granted
showExportOptionsDialog()
@ -166,7 +188,11 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
/** Received the result of the storage permission request and if it was accepted then shows the export options
* dialog, but if it was not accepted then shows a toast explaining that the permission is necessary to generate
* the export options */
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
@ -181,7 +207,10 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
private fun showExportOptionsDialog() {
MaterialDialog(context!!).show {
title(R.string.title_export_transactions)
listItemsMultiChoice(R.array.export_options, initialSelection = intArrayOf(0,1)) { _, indices, _ ->
listItemsMultiChoice(
R.array.export_options,
initialSelection = intArrayOf(0, 1)
) { _, indices, _ ->
val exportPDF = indices.contains(0)
val exportCSV = indices.contains(1)
exportFilteredTransactions(exportPDF, exportCSV)

View file

@ -1,8 +1,8 @@
package cy.agorise.bitsybitshareswallet.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment
import kotlinx.parcelize.Parcelize
/**
* Model that includes all the options to filter the transactions in the [TransactionsFragment]

View file

@ -16,7 +16,7 @@
android:elevation="4dp"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<fragment
<androidx.fragment.app.FragmentContainerView
android:id="@+id/navHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"