Merge branch 'develop'

This commit is contained in:
Severiano Jaramillo 2024-07-04 20:44:10 -07:00
commit de68a3e0a2
140 changed files with 1057 additions and 16382 deletions

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "graphenej"]
path = graphenejlib
url = git@github.com:Agorise/graphenej.git
url = git@git.agorise.net:agorise/graphenej.git

View file

@ -7,13 +7,14 @@ apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
android {
compileSdkVersion 29
compileSdk 33
defaultConfig {
applicationId "cy.agorise.bitsybitshareswallet"
minSdkVersion 21
targetSdkVersion 29
versionCode 15
versionName "0.17.2-beta"
minSdk 21
targetSdk 33
versionCode 16
versionName "0.18.0-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
@ -55,17 +56,13 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
// Gradle automatically adds 'android.test.runner' as a dependency.
useLibrary 'android.test.runner'
useLibrary 'android.test.base'
useLibrary 'android.test.mock'
}
dependencies {
def lifecycle_version = "2.2.0"
def arch_version = "2.1.0"
def room_version = "2.2.6"
def lifecycle_version = "2.3.1"
def preference_version = "1.1.1"
def room_version = "2.4.3"
def rx_bindings_version = '3.0.0'
def version_coroutine = '1.4.1'
@ -74,17 +71,20 @@ dependencies {
implementation project(':PDFJet')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// AndroidX
implementation 'androidx.activity:activity-ktx:1.2.4'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.fragment:fragment-ktx:1.3.2"
implementation "androidx.preference:preference-ktx:$preference_version"
// Google
implementation 'com.google.zxing:core:3.4.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.material:material:1.1.0-alpha04'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.android.gms:play-services-maps:17.0.0'
implementation 'com.google.maps.android:android-maps-utils:0.5'
// AAC Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // viewModelScope
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// AAC Room
implementation "androidx.room:room-runtime:$room_version"
@ -108,8 +108,10 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
//Firebase
implementation 'com.google.firebase:firebase-analytics:18.0.2'
implementation 'com.google.firebase:firebase-crashlytics:17.3.1'
implementation platform('com.google.firebase:firebase-bom:26.7.0') // Import the BoM for the Firebase platform
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// CSV generation
implementation 'com.opencsv:opencsv:3.7'
// Others
@ -130,7 +132,6 @@ dependencies {
androidTestImplementation "androidx.room:room-testing:$room_version"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'com.jraska.livedata:testing-ktx:1.0.0'
}
// Added to avoid the compilation problem due to a duplicate ListenableFuture library

View file

@ -23,20 +23,18 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
object LiveDataTestUtil {
fun <T> getValue(liveData: LiveData<T>): T {
val data = arrayOfNulls<Any>(1)
fun <T> LiveData<T>.blockingObserve(): T? {
var value: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data[0] = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
@Suppress("UNCHECKED_CAST")
return data[0] as T
val observer = Observer<T> { t ->
value = t
latch.countDown()
}
observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return value
}
}

View file

@ -6,14 +6,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.jraska.livedata.test
import cy.agorise.bitsybitshareswallet.LiveDataTestUtil.blockingObserve
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
import cy.agorise.bitsybitshareswallet.database.entities.EquivalentValue
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.*
import org.junit.runner.RunWith
import java.io.IOException
@ -111,13 +108,12 @@ class TransfersTests {
"1.2.1029856",
98,
"1.3.120",
"",
1000)
"")
db.transferDao().insert(t1)
db.transferDao().insert(t2)
db.transferDao().getTransfersWithMissingBtsValue()
.test()
.assertHasValue()
.assertValue { transfer -> transfer.id == "1.11.702181910" }
// transfer should be t2
val transfer = db.transferDao().getTransfersWithMissingBtsValue().blockingObserve()
Assert.assertEquals("1.11.684483739", transfer?.id)
}
}
}

View file

@ -1,45 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="cy.agorise.bitsybitshareswallet">
xmlns:tools="http://schemas.android.com/tools"
package="cy.agorise.bitsybitshareswallet">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".utils.BitsyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Bitsy"
android:requestLegacyExternalStorage="true"
tools:ignore="GoogleAppIndexingWarning">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key"/>
android:value="@string/google_maps_key" />
<!-- Avoid Crashlytics crash collection for all users/builds -->
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Avoid crashes with Google maps in SDK 28 (Android 9 [Pie]) -->
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
<activity
android:name=".activities.SplashActivity"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.MainActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.Bitsy"
android:windowSoftInputMode="adjustPan">
@ -50,12 +58,11 @@
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="cy.agorise.bitsybitshareswallet.FileProvider"
android:grantUriPermissions="true"
android:exported="false">
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/tmp_image_path" />
</provider>
</application>
</manifest>
</manifest>

View file

@ -4,13 +4,13 @@ import android.content.pm.PackageManager
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cy.agorise.bitsybitshareswallet.database.entities.Balance
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
@ -69,10 +69,11 @@ abstract class ConnectedActivity : AppCompatActivity() {
private const val RESPONSE_GET_MARKET_HISTORY = 6
}
private lateinit var mUserAccountViewModel: UserAccountViewModel
private lateinit var mBalanceViewModel: BalanceViewModel
private lateinit var mTransferViewModel: TransferViewModel
private lateinit var mConnectedActivityViewModel: ConnectedActivityViewModel
// TODO consolidate ViewModels
private val userAccountViewModel: UserAccountViewModel by viewModels()
private val balanceViewModel: BalanceViewModel by viewModels()
private val transferViewModel: TransferViewModel by viewModels()
private val connectedActivityViewModel: ConnectedActivityViewModel by viewModels()
private lateinit var mAssetRepository: AssetRepository
@ -114,42 +115,38 @@ abstract class ConnectedActivity : AppCompatActivity() {
mAssetRepository = AssetRepository(this)
// Configure ConnectedActivityViewModel to obtain missing equivalent values
mConnectedActivityViewModel = ViewModelProviders.of(this).get(ConnectedActivityViewModel::class.java)
val currencyCode = Helper.getCoingeckoSupportedCurrency(Locale.getDefault())
Log.d(TAG, "Using currency: ${currencyCode.toUpperCase(Locale.ROOT)}")
mConnectedActivityViewModel.observeMissingEquivalentValuesIn(currencyCode)
connectedActivityViewModel.observeMissingEquivalentValuesIn(currencyCode)
// Configure UserAccountViewModel to obtain the missing account ids
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)
mUserAccountViewModel.getMissingUserAccountIds().observe(this, Observer<List<String>>{ userAccountIds ->
userAccountViewModel.getMissingUserAccountIds().observe(this, { userAccountIds ->
if (userAccountIds.isNotEmpty()) {
missingUserAccounts.clear()
for (userAccountId in userAccountIds)
missingUserAccounts.add(UserAccount(userAccountId))
mHandler.postDelayed(mRequestMissingUserAccountsTask, Constants.NETWORK_SERVICE_RETRY_PERIOD)
mHandler.postDelayed(
mRequestMissingUserAccountsTask,
Constants.NETWORK_SERVICE_RETRY_PERIOD
)
}
})
// Configure UserAccountViewModel to obtain the missing account ids
mBalanceViewModel = ViewModelProviders.of(this).get(BalanceViewModel::class.java)
mBalanceViewModel.getMissingAssetIds().observe(this, Observer<List<String>>{ assetIds ->
balanceViewModel.getMissingAssetIds().observe(this, { assetIds ->
if (assetIds.isNotEmpty()) {
missingAssets.clear()
for (assetId in assetIds)
missingAssets.add(Asset(assetId))
mHandler.postDelayed(mRequestMissingAssetsTask, Constants.NETWORK_SERVICE_RETRY_PERIOD)
mHandler
.postDelayed(mRequestMissingAssetsTask, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
})
//Configure TransferViewModel to obtain the Transfer's block numbers with missing time information, one by one
mTransferViewModel = ViewModelProviders.of(this).get(TransferViewModel::class.java)
mTransferViewModel.getTransferBlockNumberWithMissingTime().observe(this, Observer<Long>{ blockNumber ->
transferViewModel.getTransferBlockNumberWithMissingTime().observe(this, { blockNumber ->
if (blockNumber != null && blockNumber != blockNumberWithMissingTime) {
blockNumberWithMissingTime = blockNumber
mHandler.post(mRequestBlockMissingTimeTask)
@ -167,17 +164,17 @@ abstract class ConnectedActivity : AppCompatActivity() {
mCompositeDisposable.add(disposable)
val info = this.packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
val info =
this.packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES)
val versionCode = PackageInfoCompat.getLongVersionCode(info)
val hasPurgedEquivalentValues = PreferenceManager.getDefaultSharedPreferences(this)
.getBoolean(Constants.KEY_HAS_PURGED_EQUIVALENT_VALUES, false)
if(versionCode > 11 && !hasPurgedEquivalentValues) {
if (versionCode > 11 && !hasPurgedEquivalentValues) {
thread {
mConnectedActivityViewModel.purgeEquivalentValues()
PreferenceManager.getDefaultSharedPreferences(this)
.edit()
.putBoolean(Constants.KEY_HAS_PURGED_EQUIVALENT_VALUES, true)
.apply()
connectedActivityViewModel.purgeEquivalentValues()
PreferenceManager.getDefaultSharedPreferences(this).edit {
putBoolean(Constants.KEY_HAS_PURGED_EQUIVALENT_VALUES, true)
}
}
}
}
@ -201,7 +198,7 @@ abstract class ConnectedActivity : AppCompatActivity() {
* Error consumer used to handle potential errors caused by the NetworkService while processing
* incoming data.
*/
private fun handleError(throwable: Throwable){
private fun handleError(throwable: Throwable) {
Log.e(TAG, "Error while processing received message. Msg: " + throwable.message)
val stack = throwable.stackTrace
for (e in stack) {
@ -217,24 +214,24 @@ abstract class ConnectedActivity : AppCompatActivity() {
if (message.error == null) {
if (responseMap.containsKey(message.id)) {
when (responseMap[message.id]) {
RESPONSE_GET_FULL_ACCOUNTS ->
RESPONSE_GET_FULL_ACCOUNTS ->
handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails)
RESPONSE_GET_ACCOUNTS ->
RESPONSE_GET_ACCOUNTS ->
handleAccountProperties(message.result as List<AccountProperties>)
RESPONSE_GET_ACCOUNT_BALANCES ->
RESPONSE_GET_ACCOUNT_BALANCES ->
handleBalanceUpdate(message.result as List<AssetAmount>)
RESPONSE_GET_ASSETS ->
RESPONSE_GET_ASSETS ->
handleAssets(message.result as List<Asset>)
RESPONSE_GET_BLOCK_HEADER -> {
RESPONSE_GET_BLOCK_HEADER -> {
val blockNumber = requestIdToBlockNumberMap[message.id] ?: 0L
handleBlockHeader(message.result as BlockHeader, blockNumber)
requestIdToBlockNumberMap.remove(message.id)
}
RESPONSE_GET_MARKET_HISTORY -> handleMarketData(message.result as List<BucketObject>)
RESPONSE_GET_MARKET_HISTORY -> handleMarketData(message.result as List<BucketObject>)
}
responseMap.remove(message.id)
}
@ -259,11 +256,11 @@ abstract class ConnectedActivity : AppCompatActivity() {
responseMap.clear()
} else if (message.updateCode == ConnectionStatusUpdate.API_UPDATE) {
// If we got an API update
if(message.api == ApiAccess.API_HISTORY) {
if (message.api == ApiAccess.API_HISTORY) {
// Starts the procedure that will obtain the missing equivalent values
mTransferViewModel
.getTransfersWithMissingBtsValue().observe(this, Observer<Transfer> {
if(it != null) handleTransfersWithMissingBtsValue(it)
transferViewModel
.getTransfersWithMissingBtsValue().observe(this, {
if (it != null) handleTransfersWithMissingBtsValue(it)
})
}
}
@ -274,13 +271,16 @@ abstract class ConnectedActivity : AppCompatActivity() {
* Method called whenever we get a list of transfers with their bts value missing.
*/
private fun handleTransfersWithMissingBtsValue(transfer: Transfer) {
if(mNetworkService?.isConnected == true){
if (mNetworkService?.isConnected == true) {
val base = Asset(transfer.transferAssetId)
val quote = Asset("1.3.0")
val bucket: Long = TimeUnit.SECONDS.convert(1, TimeUnit.DAYS)
val end: Long = transfer.timestamp * 1000L
val start: Long = (transfer.timestamp - bucket) * 1000L
val id = mNetworkService!!.sendMessage(GetMarketHistory(base, quote, bucket, start, end), GetMarketHistory.REQUIRED_API)
val id = mNetworkService!!.sendMessage(
GetMarketHistory(base, quote, bucket, start, end),
GetMarketHistory.REQUIRED_API
)
responseMap[id] = RESPONSE_GET_MARKET_HISTORY
this.transfer = transfer
}
@ -292,11 +292,17 @@ abstract class ConnectedActivity : AppCompatActivity() {
*/
private fun handleAccountDetails(accountDetails: FullAccountDetails) {
val latestOpCount = accountDetails.statistics.total_ops
Log.d(TAG, "handleAccountDetails. prev count: $storedOpCount, current count: $latestOpCount")
Log.d(
TAG,
"handleAccountDetails. prev count: $storedOpCount, current count: $latestOpCount"
)
if (latestOpCount == 0L) {
Log.d(TAG, "The node returned 0 total_ops for current account and may not have installed the history plugin. " +
"\nAsk the NetworkService to remove the node from the list and connect to another one.")
Log.d(
TAG,
"The node returned 0 total_ops for current account and may not have installed the history plugin. " +
"\nAsk the NetworkService to remove the node from the list and connect to another one."
)
mNetworkService?.reconnectNode()
} else if (storedOpCount == -1L) {
// Initial case when the app starts
@ -317,7 +323,8 @@ abstract class ConnectedActivity : AppCompatActivity() {
* create a list of BiTSy's UserAccount objects and stores them into the database
*/
private fun handleAccountProperties(accountPropertiesList: List<AccountProperties>) {
val userAccounts = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.UserAccount>()
val userAccounts =
ArrayList<cy.agorise.bitsybitshareswallet.database.entities.UserAccount>()
for (accountProperties in accountPropertiesList) {
val userAccount = cy.agorise.bitsybitshareswallet.database.entities.UserAccount(
@ -329,7 +336,7 @@ abstract class ConnectedActivity : AppCompatActivity() {
userAccounts.add(userAccount)
}
mUserAccountViewModel.insertAll(userAccounts)
userAccountViewModel.insertAll(userAccounts)
missingUserAccounts.clear()
}
@ -346,7 +353,7 @@ abstract class ConnectedActivity : AppCompatActivity() {
balances.add(balance)
}
mBalanceViewModel.insertAll(balances)
balanceViewModel.insertAll(balances)
}
/**
@ -381,36 +388,40 @@ abstract class ConnectedActivity : AppCompatActivity() {
dateFormat.timeZone = TimeZone.getTimeZone("GMT")
try {
val date = dateFormat.parse(blockHeader.timestamp)
mTransferViewModel.setBlockTime(blockNumber, date.time / 1000)
dateFormat.parse(blockHeader.timestamp)?.let { date ->
transferViewModel.setBlockTime(blockNumber, date.time / 1000)
}
} catch (e: ParseException) {
Log.e(TAG, "ParseException. Msg: " + e.message)
}
}
private fun handleMarketData(buckets: List<BucketObject>) {
if(buckets.isNotEmpty()){
Log.d(TAG,"handleMarketData. Bucket is not empty")
if (buckets.isNotEmpty()) {
Log.d(TAG, "handleMarketData. Bucket is not empty")
val bucket = buckets[0]
val pair = Pair(transfer, bucket)
val disposable = Observable.just(pair)
.subscribeOn(Schedulers.computation())
.map { mTransferViewModel.updateBtsValue(it.first!!, it.second) }
.subscribe({},{
Log.e(TAG,"Error at updateBtsValue. Msg: ${it.message}")
for(line in it.stackTrace) Log.e(TAG, "${line.className}#${line.methodName}:${line.lineNumber}")
.map { transferViewModel.updateBtsValue(it.first!!, it.second) }
.subscribe({}, {
Log.e(TAG, "Error at updateBtsValue. Msg: ${it.message}")
for (line in it.stackTrace)
Log.e(TAG, "${line.className}#${line.methodName}:${line.lineNumber}")
})
mCompositeDisposable.add(disposable)
}else{
Log.i(TAG,"handleMarketData. Bucket IS empty")
AsyncTask.execute { mTransferViewModel.updateBtsValue(transfer!!, Transfer.ERROR) }
} else {
Log.i(TAG, "handleMarketData. Bucket IS empty")
AsyncTask.execute { transferViewModel.updateBtsValue(transfer!!, Transfer.ERROR) }
}
}
private fun updateBalances() {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
GetAccountBalances.REQUIRED_API)
val id = mNetworkService!!.sendMessage(
GetAccountBalances(mCurrentAccount, ArrayList()),
GetAccountBalances.REQUIRED_API
)
responseMap[id] = RESPONSE_GET_ACCOUNT_BALANCES
}
@ -447,10 +458,13 @@ abstract class ConnectedActivity : AppCompatActivity() {
private val mRequestMissingUserAccountsTask = object : Runnable {
override fun run() {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
val id = mNetworkService!!.sendMessage(
GetAccounts(missingUserAccounts),
GetAccounts.REQUIRED_API
)
responseMap[id] = RESPONSE_GET_ACCOUNTS
} else if (missingUserAccounts.isNotEmpty()){
} else if (missingUserAccounts.isNotEmpty()) {
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
}
@ -462,10 +476,11 @@ abstract class ConnectedActivity : AppCompatActivity() {
private val mRequestMissingAssetsTask = object : Runnable {
override fun run() {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAssets(missingAssets), GetAssets.REQUIRED_API)
val id =
mNetworkService!!.sendMessage(GetAssets(missingAssets), GetAssets.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ASSETS
} else if (missingAssets.isNotEmpty()){
} else if (missingAssets.isNotEmpty()) {
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
}
}
@ -480,13 +495,17 @@ abstract class ConnectedActivity : AppCompatActivity() {
if (mCurrentAccount != null) {
val userAccounts = ArrayList<String>()
userAccounts.add(mCurrentAccount!!.objectId)
val id = mNetworkService!!.sendMessage(GetFullAccounts(userAccounts, false),
GetFullAccounts.REQUIRED_API)
val id = mNetworkService!!.sendMessage(
GetFullAccounts(userAccounts, false),
GetFullAccounts.REQUIRED_API
)
responseMap[id] = RESPONSE_GET_FULL_ACCOUNTS
}
} else {
Log.w(TAG, "NetworkService is null or is not connected. mNetworkService: $mNetworkService")
val msg = "NetworkService is null or is not connected. " +
"mNetworkService: $mNetworkService"
Log.w(TAG, msg)
}
mHandler.postDelayed(this, Constants.MISSING_PAYMENT_CHECK_PERIOD)
@ -500,8 +519,10 @@ abstract class ConnectedActivity : AppCompatActivity() {
override fun run() {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetBlockHeader(blockNumberWithMissingTime),
GetBlockHeader.REQUIRED_API)
val id = mNetworkService!!.sendMessage(
GetBlockHeader(blockNumberWithMissingTime),
GetBlockHeader.REQUIRED_API
)
responseMap[id] = RESPONSE_GET_BLOCK_HEADER
requestIdToBlockNumberMap[id] = blockNumberWithMissingTime
@ -521,7 +542,7 @@ abstract class ConnectedActivity : AppCompatActivity() {
override fun onPause() {
super.onPause()
mNetworkService?.nodeLatencyVerifier?.nodeList?.let { nodes ->
mConnectedActivityViewModel.updateNodeLatencies(nodes as List<FullNode>)
connectedActivityViewModel.updateNodeLatencies(nodes as List<FullNode>)
}
mHandler.removeCallbacks(mCheckMissingPaymentsTask)
@ -532,6 +553,6 @@ abstract class ConnectedActivity : AppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
if(!mCompositeDisposable.isDisposed) mCompositeDisposable.dispose()
if (!mCompositeDisposable.isDisposed) mCompositeDisposable.dispose()
}
}
}

View file

@ -2,7 +2,6 @@ package cy.agorise.bitsybitshareswallet.activities
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.util.Log
import android.view.MenuItem
import androidx.navigation.findNavController
@ -11,6 +10,7 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.preference.PreferenceManager
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.databinding.ActivityMainBinding
import cy.agorise.bitsybitshareswallet.utils.Constants
@ -36,6 +36,7 @@ class MainActivity : ConnectedActivity() {
) {
setTheme(R.style.Theme_Bitsy_Dark)
}
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

View file

@ -19,16 +19,18 @@ data class Transfer (
@ColumnInfo(name = "memo") val memo: String,
@ColumnInfo(name = "bts_value") var btsValue: Long? = NOT_CALCULATED
){
companion object {
// Constant used to specify an uninitialized BTS equivalent value
val NOT_CALCULATED: Long? = -1L
// Constant used to specify a BTS equivalent value whose calculation returned an error
val ERROR: Long? = -2L
}
init {
if(transferAssetId == "1.3.0"){
// If the transferred asset is BTS, we can fill the btsValue field immediately
btsValue = transferAmount
}
}
companion object {
// Constant used to specify an uninitialized BTS equivalent value
const val NOT_CALCULATED: Long = -1L
// Constant used to specify a BTS equivalent value whose calculation returned an error
const val ERROR: Long = -2L
}
}

View file

@ -0,0 +1,86 @@
package cy.agorise.bitsybitshareswallet.domain.usecase
import android.content.Context
import android.util.Log
import androidx.core.os.ConfigurationCompat
import androidx.documentfile.provider.DocumentFile
import com.opencsv.CSVWriter
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
class ExportTransactionsToCsvUseCase(private val applicationContext: Context) {
suspend operator fun invoke(
transactions: List<TransferDetail>,
folderDocumentFile: DocumentFile
) = withContext(Dispatchers.IO) { // TODO Inject Dispatcher
// Create the PDF file name
val fileName = applicationContext.resources.let {
"${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}.csv"
}
// Obtains the path to store the PDF
val csvDocUri = folderDocumentFile.createFile("text/csv", fileName)?.uri
val contentResolver = applicationContext.contentResolver
val csvOutputStream = contentResolver.openOutputStream(csvDocUri!!, "w")
val csvWriter = CSVWriter(OutputStreamWriter(csvOutputStream))
// Add the table header
csvWriter.writeNext(
arrayOf(
R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date,
R.string.title_time, R.string.title_amount, R.string.title_equivalent_value
).map { columnNameId -> applicationContext.getString(columnNameId) }.toTypedArray()
)
// Configure date and time formats to reuse in all the transfers
val locale = ConfigurationCompat.getLocales(applicationContext.resources.configuration)[0]
val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale)
val timeFormat = SimpleDateFormat("HH:mm:ss", locale)
// Save all the transfers information
val row = Array(7) { "" } // Array initialized with empty strings
for ((index, transferDetail) in transactions.withIndex()) {
val date = Date(transferDetail.date * 1000)
row[0] = transferDetail.from ?: "" // From
row[1] = transferDetail.to ?: "" // To
row[2] = transferDetail.memo // Memo
row[3] = dateFormat.format(date) // Date
row[4] = timeFormat.format(date) // Time
// Asset Amount
val assetPrecision = transferDetail.assetPrecision
val assetAmount = transferDetail.assetAmount / 10.0.pow(assetPrecision)
row[5] =
String.format("%.${assetPrecision}f %s", assetAmount, transferDetail.assetSymbol)
// Fiat Equivalent
row[6] = if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
val currency = Currency.getInstance(transferDetail.fiatSymbol)
val fiatAmount = transferDetail.fiatAmount / 10.0.pow(currency.defaultFractionDigits)
String.format(
"%.${currency.defaultFractionDigits}f %s",
fiatAmount,
currency.currencyCode
)
} else {
""
}
csvWriter.writeNext(row)
// TODO update progress
}
csvWriter.close()
Log.d("ExportTransactionsToCsv", "CSV generated and saved")
}
}

View file

@ -0,0 +1,166 @@
package cy.agorise.bitsybitshareswallet.domain.usecase
import android.content.Context
import android.util.Log
import androidx.core.os.ConfigurationCompat
import androidx.documentfile.provider.DocumentFile
import com.pdfjet.*
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedOutputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
suspend operator fun invoke(
transactions: List<TransferDetail>,
folderDocumentFile: DocumentFile
) = withContext(Dispatchers.IO) { // TODO Inject Dispatcher
// Create the PDF file name
val fileName = applicationContext.resources.let {
"${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}.pdf"
}
// Obtains the path to store the PDF
val pdfDocUri = folderDocumentFile.createFile("application/pdf", fileName)?.uri
val contentResolver = applicationContext.contentResolver
val pdfOutputStream = contentResolver.openOutputStream(pdfDocUri!!, "w")
// Creates a new PDF object
val pdf = PDF(BufferedOutputStream(pdfOutputStream), Compliance.PDF_A_1B)
// Font used for the table headers
val f1 = Font(pdf, CoreFont.HELVETICA_BOLD).apply { size = 7f }
// Font used for the table contents
val f2 = Font(pdf, CoreFont.HELVETICA).apply { size = 7f }
// Creates a new PDF table
val table = Table()
// 2D array of cells used to populate the PDF table
val tableData = mutableListOf<List<Cell>>()
// Add column names/headers
val columnNames = intArrayOf(
R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date,
R.string.title_time, R.string.title_amount, R.string.title_equivalent_value
)
val header = columnNames.map { columnName ->
Cell(f1, applicationContext.getString(columnName)).apply { setPadding(2f) }
}
// Add the table headers
tableData.add(header)
// Add the table contents
val locale = ConfigurationCompat.getLocales(applicationContext.resources.configuration)[0]
tableData.addAll(getData(transactions, f2, locale))
// Configure the PDF table
table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS)
table.setCellBordersWidth(0.2f)
// The A4 size has 595 points of width, with the below we are trying to assign the same
// width to all cells and also keep them centered.
for (i in 0..6) {
table.setColumnWidth(i, 65f)
}
table.wrapAroundCellText()
table.mergeOverlaidBorders()
// Populate the PDF table
while (table.hasMoreData()) {
// Configures the PDF page
val page = Page(pdf, Letter.PORTRAIT)
table.setLocation(45f, 30f)
table.drawOn(page)
}
pdf.close()
Log.d("ExportTransactionsToPdf", "PDF generated and saved")
}
private suspend fun getData(
transferDetails: List<TransferDetail>,
font: Font,
locale: Locale
): List<List<Cell>> = withContext(Dispatchers.IO) { // TODO Inject Dispatcher
val tableData = mutableListOf<List<Cell>>()
// Configure date and time formats to reuse in all the transfers
val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale)
val timeFormat = SimpleDateFormat("HH:mm:ss", locale)
var date: Date
// Save all the transfers information
for ((index, transferDetail) in transferDetails.withIndex()) {
val cols = mutableListOf<String>()
date = Date(transferDetail.date * 1000)
cols.add(transferDetail.from ?: "") // From
cols.add(transferDetail.to ?: "") // To
cols.add(transferDetail.memo) // Memo
cols.add(dateFormat.format(date)) // Date
cols.add(timeFormat.format(date)) // Time
// Asset Amount
val assetPrecision = transferDetail.assetPrecision
val assetAmount = transferDetail.assetAmount / 10.0.pow(assetPrecision)
cols.add(
String.format(
"%.${assetPrecision}f %s",
assetAmount,
transferDetail.assetSymbol
)
)
// Fiat Equivalent
if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
val currency = Currency.getInstance(transferDetail.fiatSymbol)
val fiatAmount =
transferDetail.fiatAmount / 10.0.pow(currency.defaultFractionDigits)
cols.add(
String.format(
"%.${currency.defaultFractionDigits}f %s",
fiatAmount, currency.currencyCode
)
)
}
val row = cols.map { col ->
Cell(font, col).apply { setPadding(2f) }
}
tableData.add(row)
appendMissingCells(tableData, font)
// TODO update progress
}
tableData
}
private fun appendMissingCells(tableData: List<List<Cell>>, font: Font) {
val firstRow = tableData[0]
val numOfColumns = firstRow.size
for (i in tableData.indices) {
val dataRow = tableData[i] as ArrayList<Cell>
val dataRowColumns = dataRow.size
if (dataRowColumns < numOfColumns) {
for (j in 0 until numOfColumns - dataRowColumns) {
dataRow.add(Cell(font))
}
dataRow[dataRowColumns - 1].colSpan = numOfColumns - dataRowColumns + 1
}
}
}
}

View file

@ -5,22 +5,20 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
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
class BalancesFragment : Fragment() {
private val viewModel: BalanceDetailViewModel by viewModels()
private var _binding: FragmentBalancesBinding? = null
private val binding get() = _binding!!
private lateinit var mBalanceDetailViewModel: BalanceDetailViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -40,19 +38,15 @@ class BalancesFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
// Configure BalanceDetailViewModel to show the current balances
mBalanceDetailViewModel =
ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
val balancesAdapter = BalancesAdapter(context!!)
val balancesAdapter = BalancesAdapter(requireContext())
binding.rvBalances.adapter = balancesAdapter
binding.rvBalances.layoutManager = LinearLayoutManager(context!!)
binding.rvBalances.layoutManager = LinearLayoutManager(requireContext())
binding.rvBalances.addItemDecoration(
DividerItemDecoration(context!!, DividerItemDecoration.VERTICAL)
DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)
)
mBalanceDetailViewModel.getAll()
.observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
balancesAdapter.replaceAll(balancesDetails)
})
viewModel.getAll().observe(viewLifecycleOwner, { balancesDetails ->
balancesAdapter.replaceAll(balancesDetails)
})
}
}

View file

@ -1,8 +1,10 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.preference.PreferenceManager
import androidx.core.content.edit
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.activities.ConnectedActivity
import cy.agorise.bitsybitshareswallet.database.entities.Authority
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
@ -13,7 +15,6 @@ import cy.agorise.graphenej.BrainKey
import cy.agorise.graphenej.PublicKey
import cy.agorise.graphenej.models.AccountProperties
import org.bitcoinj.core.ECKey
import cy.agorise.bitsybitshareswallet.activities.ConnectedActivity
abstract class BaseAccountFragment : ConnectedFragment() {
@ -36,23 +37,26 @@ abstract class BaseAccountFragment : ConnectedFragment() {
val hashedPIN = CryptoUtils.createSHA256Hash(salt + pin)
// Stores the user selected PIN, hashed
PreferenceManager.getDefaultSharedPreferences(context!!).edit()
.putString(Constants.KEY_HASHED_PIN_PATTERN, hashedPIN)
.putString(Constants.KEY_PIN_PATTERN_SALT, salt)
.putInt(Constants.KEY_SECURITY_LOCK_SELECTED, 1).apply() // 1 -> PIN
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit {
putString(Constants.KEY_HASHED_PIN_PATTERN, hashedPIN)
putString(Constants.KEY_PIN_PATTERN_SALT, salt)
putInt(Constants.KEY_SECURITY_LOCK_SELECTED, 1) // 1 -> PIN
}
// Stores the accounts this key refers to
val id = accountProperties.id
val name = accountProperties.name
val isLTM = accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
val isLTM =
accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
val userAccount = cy.agorise.bitsybitshareswallet.database.entities.UserAccount(id, name, isLTM)
val userAccount =
cy.agorise.bitsybitshareswallet.database.entities.UserAccount(id, name, isLTM)
val userAccountRepository = UserAccountRepository(context!!.applicationContext)
val userAccountRepository = UserAccountRepository(requireContext().applicationContext)
userAccountRepository.insert(userAccount)
// Stores the id of the currently active user account
PreferenceManager.getDefaultSharedPreferences(context!!).edit()
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
.putString(Constants.KEY_CURRENT_ACCOUNT_ID, accountProperties.id).apply()
// Trying to store all possible authorities (owner, active and memo) into the database
@ -65,13 +69,25 @@ abstract class BaseAccountFragment : ConnectedFragment() {
val publicKey = PublicKey(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
if (ownerAuthority.keyAuths.keys.contains(publicKey)) {
addAuthorityToDatabase(accountProperties.id, AuthorityType.OWNER.ordinal, mBrainKey!!)
addAuthorityToDatabase(
accountProperties.id,
AuthorityType.OWNER.ordinal,
mBrainKey!!
)
}
if (activeAuthority.keyAuths.keys.contains(publicKey)) {
addAuthorityToDatabase(accountProperties.id, AuthorityType.ACTIVE.ordinal, mBrainKey!!)
addAuthorityToDatabase(
accountProperties.id,
AuthorityType.ACTIVE.ordinal,
mBrainKey!!
)
}
if (options.memoKey == publicKey) {
addAuthorityToDatabase(accountProperties.id, AuthorityType.MEMO.ordinal, mBrainKey!!)
addAuthorityToDatabase(
accountProperties.id,
AuthorityType.MEMO.ordinal,
mBrainKey!!
)
}
}
@ -91,13 +107,21 @@ abstract class BaseAccountFragment : ConnectedFragment() {
val wif = brainKey.walletImportFormat
val sequenceNumber = brainKey.sequenceNumber
val encryptedBrainKey = CryptoUtils.encrypt(context!!, brainKeyWords)
val encryptedSequenceNumber = CryptoUtils.encrypt(context!!, sequenceNumber.toString())
val encryptedWIF = CryptoUtils.encrypt(context!!, wif)
val encryptedBrainKey = CryptoUtils.encrypt(requireContext(), brainKeyWords)
val encryptedSequenceNumber =
CryptoUtils.encrypt(requireContext(), sequenceNumber.toString())
val encryptedWIF = CryptoUtils.encrypt(requireContext(), wif)
val authority = Authority(0, userId, authorityType, encryptedWIF, encryptedBrainKey, encryptedSequenceNumber)
val authority = Authority(
0,
userId,
authorityType,
encryptedWIF,
encryptedBrainKey,
encryptedSequenceNumber
)
val authorityRepository = AuthorityRepository(context!!)
val authorityRepository = AuthorityRepository(requireContext())
authorityRepository.insert(authority)
}
}

View file

@ -2,11 +2,12 @@ package cy.agorise.bitsybitshareswallet.fragments
import android.os.Bundle
import android.os.CountDownTimer
import android.preference.PreferenceManager
import android.view.View
import android.view.ViewGroup
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.utils.Constants
import io.reactivex.disposables.CompositeDisposable
@ -118,10 +119,10 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
incorrectSecurityLockTime = now
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, ++incorrectSecurityLockAttempts)
.putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, now)
.apply()
PreferenceManager.getDefaultSharedPreferences(context).edit {
putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, ++incorrectSecurityLockAttempts)
putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, now)
}
}
/**
@ -132,10 +133,10 @@ abstract class BaseSecurityLockDialog : DialogFragment() {
incorrectSecurityLockTime = 0
incorrectSecurityLockAttempts = 0
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, incorrectSecurityLockAttempts)
.putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, incorrectSecurityLockTime)
.apply()
PreferenceManager.getDefaultSharedPreferences(context).edit {
putInt(Constants.KEY_INCORRECT_SECURITY_LOCK_ATTEMPTS, incorrectSecurityLockAttempts)
putLong(Constants.KEY_INCORRECT_SECURITY_LOCK_TIME, incorrectSecurityLockTime)
}
}
protected fun startContDownTimer() {

View file

@ -362,8 +362,9 @@ class CreateAccountFragment : BaseAccountFragment() {
var reader: BufferedReader? = null
val dictionary: String
try {
reader =
BufferedReader(InputStreamReader(context!!.assets.open(BRAINKEY_FILE), "UTF-8"))
reader = BufferedReader(
InputStreamReader(requireContext().assets.open(BRAINKEY_FILE), "UTF-8")
)
dictionary = reader.readLine()
val brainKeySuggestion = BrainKey.suggest(dictionary)

View file

@ -1,27 +1,23 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.preference.PreferenceManager
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.*
import androidx.core.content.ContextCompat
import androidx.core.os.ConfigurationCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
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 java.math.RoundingMode
import java.text.DecimalFormat
@ -29,21 +25,17 @@ import java.text.DecimalFormatSymbols
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow
class EReceiptFragment : Fragment() {
companion object {
private const val TAG = "EReceiptFragment"
private val args: EReceiptFragmentArgs by navArgs()
private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
}
private val viewModel: EReceiptViewModel by viewModels()
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(
@ -75,12 +67,9 @@ class EReceiptFragment : Fragment() {
val transferId = args.transferId
mEReceiptViewModel = ViewModelProviders.of(this).get(EReceiptViewModel::class.java)
mEReceiptViewModel.get(userId, transferId)
.observe(this, Observer<TransferDetail> { transferDetail ->
bindTransferDetail(transferDetail)
})
viewModel.get(userId, transferId).observe(viewLifecycleOwner) { transferDetail ->
bindTransferDetail(transferDetail)
}
}
private fun bindTransferDetail(transferDetail: TransferDetail) {
@ -98,7 +87,7 @@ class EReceiptFragment : Fragment() {
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
val amount = transferDetail.assetAmount.toDouble() /
Math.pow(10.toDouble(), transferDetail.assetPrecision.toDouble())
10.toDouble().pow(transferDetail.assetPrecision.toDouble())
val assetAmount = "${df.format(amount)} ${transferDetail.getUIAssetSymbol()}"
binding.tvAmount.text = assetAmount
@ -107,7 +96,7 @@ class EReceiptFragment : Fragment() {
val numberFormat = NumberFormat.getNumberInstance()
val currency = Currency.getInstance(transferDetail.fiatSymbol)
val fiatEquivalent = transferDetail.fiatAmount.toDouble() /
Math.pow(10.0, currency.defaultFractionDigits.toDouble())
10.0.pow(currency.defaultFractionDigits.toDouble())
val equivalentValue = "${numberFormat.format(fiatEquivalent)} ${currency.currencyCode}"
binding.tvEquivalentValue.text = equivalentValue
@ -133,12 +122,11 @@ 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,
"<a href=\"http://bitshares-explorer.io/#/operations/$transferId\">$transferId</a>"
)
val html = getString(
R.string.template__tx,
"<a href=\"http://blocksights.info/#/operations/$transferId\">$transferId</a>"
)
val tx = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
binding.tvTransferID.text = tx
binding.tvTransferID.movementMethod = LinkMovementMethod.getInstance()
}
@ -155,47 +143,13 @@ class EReceiptFragment : Fragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_share) {
verifyStoragePermission()
shareEReceiptScreenshot()
return true
}
return super.onOptionsItemSelected(item)
}
/** 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
) {
// Permission is not already granted
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
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
shareEReceiptScreenshot()
} else {
context?.toast(getString(R.string.msg__storage_permission_necessary_share))
}
return
}
}
/** Takes a screenshot as a bitmap (