commit
09fd15ecce
146 changed files with 7211 additions and 727 deletions
|
@ -1,7 +1,10 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: "androidx.navigation.safeargs"
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
@ -15,31 +18,69 @@ android {
|
|||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
// TODO enabling minify breaks the debugger breakpoints, find a way to fix it and enable minify again
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
resValue("string", "PORT_NUMBER", "8082")
|
||||
}
|
||||
}
|
||||
android.packagingOptions {
|
||||
exclude 'lib/x86_64/darwin/libscrypt.dylib'
|
||||
exclude 'lib/x86_64/freebsd/libscrypt.so'
|
||||
exclude 'lib/x86_64/linux/libscrypt.so'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def lifecycle_version = "2.0.0"
|
||||
def room_version = "2.1.0-alpha02"
|
||||
def room_version = "2.1.0-alpha03"
|
||||
def nav_version = "1.0.0-alpha09"
|
||||
def rx_bindings_version = "3.0.0-alpha2"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(':graphenejlib:graphenej')
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
// AndroidX
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
// Google
|
||||
implementation 'com.google.zxing:core:3.3.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.5'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.android.gms:play-services-maps:16.0.0'
|
||||
// AAC Lifecycle
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
||||
|
||||
// AAC Room
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-rxjava2:$room_version" // RxJava support for Room
|
||||
// AAC Navigation
|
||||
implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "android.arch.navigation:navigation-ui-ktx:$nav_version"
|
||||
// RxBindings
|
||||
implementation "com.jakewharton.rxbinding3:rxbinding:$rx_bindings_version"
|
||||
implementation "com.jakewharton.rxbinding3:rxbinding-material:$rx_bindings_version" // Material Components widgets
|
||||
implementation "com.jakewharton.rxbinding3:rxbinding-appcompat:$rx_bindings_version" // AndroidX appcompat widgets
|
||||
// Retrofit
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
|
||||
//Firebase
|
||||
implementation 'com.google.firebase:firebase-core:16.0.6'
|
||||
implementation 'com.google.firebase:firebase-crash:16.2.1'
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.7'
|
||||
// Others
|
||||
implementation 'org.bitcoinj:bitcoinj-core:0.14.3'
|
||||
implementation 'com.moldedbits.r2d2:r2d2:1.0.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation 'com.afollestad.material-dialogs:core:2.0.0-rc3'
|
||||
// Android Debug Database
|
||||
debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'
|
||||
|
||||
// TODO enable and make proper testing
|
||||
// testImplementation 'junit:junit:4.12'
|
||||
|
|
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
|
@ -19,3 +19,14 @@
|
|||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontwarn sun.misc.Unsafe
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn org.bitcoinj.store**
|
||||
-dontwarn org.slf4j.**
|
||||
-dontwarn okhttp3.internal.platform.*
|
||||
|
||||
# Firabase Crashlytics
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep public class * extends java.lang.Exception
|
24
app/src/debug/res/values/google_maps_api.xml
Normal file
24
app/src/debug/res/values/google_maps_api.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<resources>
|
||||
<!--
|
||||
Before you run your application, you need a Google Maps API key.
|
||||
|
||||
To get one, follow this link, follow the directions and press "Create" at the end:
|
||||
|
||||
https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=08:07:B8:EC:7B:1E:00:82:29:24:A8:08:A6:AD:84:76:1C:D2:69:1A%3Bcy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
You can also add your credentials to an existing key, using these values:
|
||||
|
||||
Package name:
|
||||
08:07:B8:EC:7B:1E:00:82:29:24:A8:08:A6:AD:84:76:1C:D2:69:1A
|
||||
|
||||
SHA-1 certificate fingerprint:
|
||||
08:07:B8:EC:7B:1E:00:82:29:24:A8:08:A6:AD:84:76:1C:D2:69:1A
|
||||
|
||||
Alternatively, follow the directions here:
|
||||
https://developers.google.com/maps/documentation/android/start#get-key
|
||||
|
||||
Once you have your key (it starts with "AIza"), replace the "google_maps_key"
|
||||
string in this file.
|
||||
-->
|
||||
<string name="google_maps_key" translatable="false" templateMergeStrategy="preserve">AIzaSyDIYbjdkZqbLUINQXrAzNSjNwep5jGNjKA</string>
|
||||
</resources>
|
|
@ -3,42 +3,64 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cy.agorise.bitsybitshareswallet">
|
||||
|
||||
<!--
|
||||
The ACCESS_COARSE/FINE_LOCATION permissions are not required to use
|
||||
Google Maps Android API v2, but you must specify either coarse or fine
|
||||
location permissions for the 'MyLocation' functionality.
|
||||
-->
|
||||
<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"/>
|
||||
|
||||
<application
|
||||
android:name=".utils.BitsyApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/bts_logo"
|
||||
android:icon="@drawable/ic_bitsy_logo"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/bts_logo"
|
||||
android:roundIcon="@drawable/ic_bitsy_logo"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/Theme.Bitsy"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<!--
|
||||
The API key for Google Maps-based APIs is defined as a string resource.
|
||||
(See the file "res/values/google_maps_api.xml").
|
||||
Note that the API key is linked to the encryption key used to sign the APK.
|
||||
You need a different API key for each encryption key, including the release key that is used to
|
||||
sign the APK for publishing.
|
||||
You can define the keys for the debug and release targets in src/debug/ and src/release/.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key"/>
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
android:name=".activities.SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".activities.LicenseActivity"/>
|
||||
<activity android:name=".activities.ImportBrainkeyActivity"/>
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/title_settings">
|
||||
</activity>
|
||||
<activity android:name=".activities.SendTransactionActivity">
|
||||
</activity>
|
||||
<activity android:name=".activities.ReceiveTransactionActivity">
|
||||
</activity>
|
||||
|
||||
<!--<activity
|
||||
android:name="de.bitshares_munich.smartcoinswallet.ReceiveActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme" />-->
|
||||
|
||||
<activity
|
||||
android:name=".activities.RequestActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme" />
|
||||
android:name=".activities.MainActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Bitsy.NoActionBar"
|
||||
android:windowSoftInputMode="adjustPan"/>
|
||||
<!-- Used to share QR code on QRCodeActivity -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="cy.agorise.bitsybitshareswallet.FileProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/tmp_image_path" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,420 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Balance
|
||||
import cy.agorise.bitsybitshareswallet.processors.TransfersLoader
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceViewModel
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.TransferViewModel
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
|
||||
import cy.agorise.graphenej.Asset
|
||||
import cy.agorise.graphenej.AssetAmount
|
||||
import cy.agorise.graphenej.UserAccount
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.api.calls.*
|
||||
import cy.agorise.graphenej.models.AccountProperties
|
||||
import cy.agorise.graphenej.models.BlockHeader
|
||||
import cy.agorise.graphenej.models.FullAccountDetails
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
/**
|
||||
* Class in charge of managing the connection to graphenej's NetworkService
|
||||
*/
|
||||
abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
private val RESPONSE_GET_FULL_ACCOUNTS = 1
|
||||
private val RESPONSE_GET_ACCOUNTS = 2
|
||||
private val RESPONSE_GET_ACCOUNT_BALANCES = 3
|
||||
private val RESPONSE_GET_ASSETS = 4
|
||||
private val RESPONSE_GET_BLOCK_HEADER = 5
|
||||
|
||||
private lateinit var mUserAccountViewModel: UserAccountViewModel
|
||||
private lateinit var mBalanceViewModel: BalanceViewModel
|
||||
private lateinit var mTransferViewModel: TransferViewModel
|
||||
|
||||
private lateinit var mAssetRepository: AssetRepository
|
||||
|
||||
/* Current user account */
|
||||
protected var mCurrentAccount: UserAccount? = null
|
||||
|
||||
private val mHandler = Handler()
|
||||
|
||||
// Disposable returned at the bus subscription
|
||||
private var mDisposable: Disposable? = null
|
||||
|
||||
private var storedOpCount: Long = -1
|
||||
|
||||
private var missingUserAccounts = ArrayList<UserAccount>()
|
||||
private var missingAssets = ArrayList<Asset>()
|
||||
|
||||
/* Network service connection */
|
||||
protected var mNetworkService: NetworkService? = null
|
||||
|
||||
// Map used to keep track of request and response id pairs
|
||||
private val responseMap = HashMap<Long, Int>()
|
||||
|
||||
/** Map used to keep track of request id and block number pairs */
|
||||
private val requestIdToBlockNumberMap = HashMap<Long, Long>()
|
||||
|
||||
private var blockNumberWithMissingTime = 0L
|
||||
|
||||
/**
|
||||
* Flag used to keep track of the NetworkService binding state
|
||||
*/
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
|
||||
if (userId != "")
|
||||
mCurrentAccount = UserAccount(userId)
|
||||
|
||||
mAssetRepository = AssetRepository(this)
|
||||
|
||||
// Configure UserAccountViewModel to obtain the missing account ids
|
||||
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)
|
||||
|
||||
mUserAccountViewModel.getMissingUserAccountIds().observe(this, Observer<List<String>>{ userAccountIds ->
|
||||
if (userAccountIds.isNotEmpty()) {
|
||||
missingUserAccounts.clear()
|
||||
for (userAccountId in userAccountIds)
|
||||
missingUserAccounts.add(UserAccount(userAccountId))
|
||||
|
||||
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 ->
|
||||
if (assetIds.isNotEmpty()) {
|
||||
missingAssets.clear()
|
||||
for (assetId in assetIds)
|
||||
missingAssets.add(Asset(assetId))
|
||||
|
||||
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 ->
|
||||
if (blockNumber != null && blockNumber != blockNumberWithMissingTime) {
|
||||
blockNumberWithMissingTime = blockNumber
|
||||
Log.d(TAG, "Block number: $blockNumber, Time: ${System.currentTimeMillis()}")
|
||||
mHandler.postDelayed(mRequestBlockMissingTimeTask, 10)
|
||||
}
|
||||
})
|
||||
|
||||
mDisposable = RxBus.getBusInstance()
|
||||
.asFlowable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { handleIncomingMessage(it) }
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(message: Any?) {
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
// Generic processing taken care by subclasses
|
||||
handleJsonRpcResponse(message)
|
||||
|
||||
if (message.error == null) {
|
||||
if (responseMap.containsKey(message.id)) {
|
||||
val responseType = responseMap[message.id]
|
||||
when (responseType) {
|
||||
RESPONSE_GET_FULL_ACCOUNTS ->
|
||||
handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails)
|
||||
|
||||
RESPONSE_GET_ACCOUNTS ->
|
||||
handleAccountProperties(message.result as List<AccountProperties>)
|
||||
|
||||
RESPONSE_GET_ACCOUNT_BALANCES ->
|
||||
handleBalanceUpdate(message.result as List<AssetAmount>)
|
||||
|
||||
RESPONSE_GET_ASSETS ->
|
||||
handleAssets(message.result as List<Asset>)
|
||||
|
||||
RESPONSE_GET_BLOCK_HEADER -> {
|
||||
val blockNumber = requestIdToBlockNumberMap[message.id] ?: 0L
|
||||
handleBlockHeader(message.result as BlockHeader, blockNumber)
|
||||
requestIdToBlockNumberMap.remove(message.id)
|
||||
}
|
||||
}
|
||||
responseMap.remove(message.id)
|
||||
}
|
||||
} else {
|
||||
// In case of error
|
||||
Log.e(TAG, "Got error message from full node. Msg: " + message.error.message)
|
||||
Toast.makeText(
|
||||
this@ConnectedActivity,
|
||||
String.format("Error from full node. Msg: %s", message.error.message),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} else if (message is ConnectionStatusUpdate) {
|
||||
handleConnectionStatusUpdate(message)
|
||||
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
|
||||
// If we got a disconnection notification, we should clear our response map, since
|
||||
// all its stored request ids will now be reset
|
||||
responseMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called whenever a response to the 'get_full_accounts' API call has been detected.
|
||||
* @param accountDetails De-serialized account details object
|
||||
*/
|
||||
private fun handleAccountDetails(accountDetails: FullAccountDetails) {
|
||||
val latestOpCount = accountDetails.statistics.total_ops
|
||||
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.")
|
||||
mNetworkService!!.removeCurrentNodeAndReconnect()
|
||||
} else if (storedOpCount == -1L) {
|
||||
// Initial case when the app starts
|
||||
storedOpCount = latestOpCount
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit().putLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, latestOpCount).apply()
|
||||
TransfersLoader(this)
|
||||
updateBalances()
|
||||
} else if (latestOpCount > storedOpCount) {
|
||||
storedOpCount = latestOpCount
|
||||
TransfersLoader(this)
|
||||
updateBalances()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a list of missing [AccountProperties] from which it extracts the required information to
|
||||
* 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>()
|
||||
|
||||
for (accountProperties in accountPropertiesList) {
|
||||
val userAccount = cy.agorise.bitsybitshareswallet.database.entities.UserAccount(
|
||||
accountProperties.id,
|
||||
accountProperties.name,
|
||||
accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
|
||||
)
|
||||
|
||||
userAccounts.add(userAccount)
|
||||
}
|
||||
|
||||
mUserAccountViewModel.insertAll(userAccounts)
|
||||
missingUserAccounts.clear()
|
||||
}
|
||||
|
||||
private fun handleBalanceUpdate(assetAmountList: List<AssetAmount>) {
|
||||
Log.d(TAG, "handleBalanceUpdate")
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val balances = ArrayList<Balance>()
|
||||
for (assetAmount in assetAmountList) {
|
||||
val balance = Balance(
|
||||
assetAmount.asset.objectId,
|
||||
assetAmount.amount.toLong(),
|
||||
now
|
||||
)
|
||||
|
||||
balances.add(balance)
|
||||
}
|
||||
|
||||
mBalanceViewModel.insertAll(balances)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a list of missing [Asset] from which it extracts the required information to
|
||||
* create a list of BiTSy's Asset objects and stores them into the database
|
||||
*/
|
||||
private fun handleAssets(_assets: List<Asset>) {
|
||||
val assets = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.Asset>()
|
||||
|
||||
for (_asset in _assets) {
|
||||
val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset(
|
||||
_asset.objectId,
|
||||
_asset.symbol,
|
||||
_asset.precision,
|
||||
_asset.description ?: "",
|
||||
_asset.bitassetId ?: ""
|
||||
)
|
||||
|
||||
assets.add(asset)
|
||||
}
|
||||
|
||||
mAssetRepository.insertAll(assets)
|
||||
missingAssets.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives the [BlockHeader] related to a Transfer's missing time and saves it into the database.
|
||||
*/
|
||||
private fun handleBlockHeader(blockHeader: BlockHeader, blockNumber: Long) {
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("GMT")
|
||||
|
||||
try {
|
||||
val date = dateFormat.parse(blockHeader.timestamp)
|
||||
mTransferViewModel.setBlockTime(blockNumber, date.time / 1000)
|
||||
} catch (e: ParseException) {
|
||||
Log.e(TAG, "ParseException. Msg: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBalances() {
|
||||
if (mNetworkService!!.isConnected) {
|
||||
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
|
||||
GetAccountBalances.REQUIRED_API)
|
||||
|
||||
responseMap[id] = RESPONSE_GET_ACCOUNT_BALANCES
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to obtain the missing UserAccounts from Graphenej's NetworkService.
|
||||
*/
|
||||
private val mRequestMissingUserAccountsTask = object : Runnable {
|
||||
override fun run() {
|
||||
if (mNetworkService!!.isConnected) {
|
||||
val id = mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
|
||||
|
||||
responseMap[id] = RESPONSE_GET_ACCOUNTS
|
||||
} else if (missingUserAccounts.isNotEmpty()){
|
||||
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to obtain the missing Assets from Graphenej's NetworkService.
|
||||
*/
|
||||
private val mRequestMissingAssetsTask = object : Runnable {
|
||||
override fun run() {
|
||||
if (mNetworkService!!.isConnected) {
|
||||
val id = mNetworkService!!.sendMessage(GetAssets(missingAssets), GetAssets.REQUIRED_API)
|
||||
|
||||
responseMap[id] = RESPONSE_GET_ASSETS
|
||||
} else if (missingAssets.isNotEmpty()){
|
||||
mHandler.postDelayed(this, Constants.NETWORK_SERVICE_RETRY_PERIOD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to perform a redundant payment check.
|
||||
*/
|
||||
private val mCheckMissingPaymentsTask = object : Runnable {
|
||||
override fun run() {
|
||||
if (mNetworkService != null && mNetworkService!!.isConnected) {
|
||||
if (mCurrentAccount != null) {
|
||||
val userAccounts = ArrayList<String>()
|
||||
userAccounts.add(mCurrentAccount!!.objectId)
|
||||
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")
|
||||
}
|
||||
mHandler.postDelayed(this, Constants.MISSING_PAYMENT_CHECK_PERIOD)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to obtain the missing time from a block from Graphenej's NetworkService.
|
||||
*/
|
||||
private val mRequestBlockMissingTimeTask = object : Runnable {
|
||||
override fun run() {
|
||||
|
||||
if (mNetworkService != null && mNetworkService!!.isConnected) {
|
||||
val id = mNetworkService!!.sendMessage(GetBlockHeader(blockNumberWithMissingTime),
|
||||
GetBlockHeader.REQUIRED_API)
|
||||
|
||||
responseMap[id] = RESPONSE_GET_BLOCK_HEADER
|
||||
requestIdToBlockNumberMap[id] = blockNumberWithMissingTime
|
||||
} else {
|
||||
mHandler.postDelayed(this, Constants.MISSING_PAYMENT_CHECK_PERIOD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
val binder = service as NetworkService.LocalBinder
|
||||
mNetworkService = binder.service
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) { }
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// Unbinding from network service
|
||||
if (mShouldUnbindNetwork) {
|
||||
unbindService(this)
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
mHandler.removeCallbacks(mCheckMissingPaymentsTask)
|
||||
mHandler.removeCallbacks(mRequestMissingUserAccountsTask)
|
||||
mHandler.removeCallbacks(mRequestMissingAssetsTask)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val intent = Intent(this, NetworkService::class.java)
|
||||
if (bindService(intent, this, Context.BIND_AUTO_CREATE)) {
|
||||
mShouldUnbindNetwork = true
|
||||
} else {
|
||||
Log.e(TAG, "Binding to the network service failed.")
|
||||
}
|
||||
mHandler.postDelayed(mCheckMissingPaymentsTask, Constants.MISSING_PAYMENT_CHECK_PERIOD)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be implemented by all subclasses in order to be notified of JSON-RPC responses.
|
||||
* @param response
|
||||
*/
|
||||
internal abstract fun handleJsonRpcResponse(response: JsonRpcResponse<*>)
|
||||
|
||||
/**
|
||||
* Method to be implemented by all subclasses in order to be notified of connection status updates
|
||||
* @param connectionStatusUpdate
|
||||
*/
|
||||
internal abstract fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate)
|
||||
}
|
|
@ -0,0 +1,357 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Authority
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
|
||||
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
|
||||
import cy.agorise.graphenej.*
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.calls.GetAccounts
|
||||
import cy.agorise.graphenej.api.calls.GetKeyReferences
|
||||
import cy.agorise.graphenej.models.AccountProperties
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.activity_import_brainkey.*
|
||||
import org.bitcoinj.core.ECKey
|
||||
import java.util.ArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// TODO Add method to load the 20? most important assets
|
||||
// TODO add progress bar or something while the user waits for the import response from the node
|
||||
|
||||
class ImportBrainkeyActivity : ConnectedActivity() {
|
||||
private val TAG = "ImportBrainkeyActivity"
|
||||
|
||||
/**
|
||||
* Private variable that will hold an instance of the [BrainKey] class
|
||||
*/
|
||||
private var mBrainKey: BrainKey? = null
|
||||
|
||||
/**
|
||||
* User account associated with the key derived from the brainkey that the user just typed in
|
||||
*/
|
||||
private var mUserAccount: UserAccount? = null
|
||||
|
||||
/**
|
||||
* List of user account candidates, this is required in order to allow the user to select a single
|
||||
* user account in case one key (derived from the brainkey) controls more than one account.
|
||||
*/
|
||||
private var mUserAccountCandidates: List<UserAccount>? = null
|
||||
|
||||
private var mKeyReferencesAttempts = 0
|
||||
|
||||
private var keyReferencesRequestId: Long = 0
|
||||
private var getAccountsRequestId: Long = 0
|
||||
|
||||
private var mDisposables = CompositeDisposable()
|
||||
|
||||
private var isPINValid = false
|
||||
private var isPINConfirmationValid = false
|
||||
private var isBrainKeyValid = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_import_brainkey)
|
||||
|
||||
// Use RxJava Debounce to update the PIN error only after the user stops writing for > 500 ms
|
||||
mDisposables.add(
|
||||
tietPin.textChanges()
|
||||
.skipInitialValue()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { validatePIN() }
|
||||
)
|
||||
|
||||
// Use RxJava Debounce to update the PIN Confirmation error only after the user stops writing for > 500 ms
|
||||
mDisposables.add(
|
||||
tietPinConfirmation.textChanges()
|
||||
.skipInitialValue()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { validatePINConfirmation() }
|
||||
)
|
||||
|
||||
// Use RxJava Debounce to update the BrainKey error only after the user stops writing for > 500 ms
|
||||
mDisposables.add(
|
||||
tietBrainKey.textChanges()
|
||||
.skipInitialValue()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.map { it.toString().trim() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { validateBrainKey(it) }
|
||||
)
|
||||
|
||||
btnImport.isEnabled = false
|
||||
btnImport.setOnClickListener { verifyBrainKey(false) }
|
||||
}
|
||||
|
||||
private fun validatePIN() {
|
||||
val pin = tietPin.text.toString()
|
||||
|
||||
if (pin.length < Constants.MIN_PIN_LENGTH) {
|
||||
tilPin.error = getString(R.string.error__pin_too_short)
|
||||
isPINValid = false
|
||||
} else {
|
||||
tilPin.isErrorEnabled = false
|
||||
isPINValid = true
|
||||
}
|
||||
|
||||
validatePINConfirmation()
|
||||
}
|
||||
|
||||
private fun validatePINConfirmation() {
|
||||
val pinConfirmation = tietPinConfirmation.text.toString()
|
||||
|
||||
if (pinConfirmation != tietPin.text.toString()) {
|
||||
tilPinConfirmation.error = getString(R.string.error__pin_mismatch)
|
||||
isPINConfirmationValid = false
|
||||
} else {
|
||||
tilPinConfirmation.isErrorEnabled = false
|
||||
isPINConfirmationValid = true
|
||||
}
|
||||
|
||||
enableDisableImportButton()
|
||||
}
|
||||
|
||||
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)
|
||||
isBrainKeyValid = false
|
||||
} else {
|
||||
tilBrainKey.isErrorEnabled = false
|
||||
isBrainKeyValid = true
|
||||
}
|
||||
|
||||
enableDisableImportButton()
|
||||
}
|
||||
|
||||
private fun enableDisableImportButton() {
|
||||
btnImport.isEnabled = (isPINValid && isPINConfirmationValid && isBrainKeyValid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that will verify the provided brain key, and if valid will retrieve the account information
|
||||
* associated to the user id.
|
||||
*
|
||||
* This method will perform a network lookup to look which accounts use the public key associated
|
||||
* with the user provided brainkey.
|
||||
*
|
||||
* Some sources use brainkeys in capital letters, while others use lowercase. The activity should
|
||||
* initially call this method with the 'switchCase' parameter as false, in order to try the
|
||||
* brainkey as it was provided by the user.
|
||||
*
|
||||
* But in case this lookup fails, it is expected that the activity makes another attempt. This time
|
||||
* with the 'switchCase' argument set to true.
|
||||
*
|
||||
* If both attempts fail, then we can be certain that the provided brainkey is not currently
|
||||
* associated with any account.
|
||||
*
|
||||
* @param switchCase Whether to switch the case used in the brainkey or not.
|
||||
*/
|
||||
private fun verifyBrainKey(switchCase: Boolean) {
|
||||
//showDialog("", getString(R.string.importing_your_wallet))
|
||||
val brainKey = tietBrainKey.text.toString()
|
||||
// Should we switch the brainkey case?
|
||||
if (switchCase) {
|
||||
if (Character.isUpperCase(brainKey.toCharArray()[brainKey.length - 1])) {
|
||||
// If the last character is an uppercase, we assume the whole brainkey
|
||||
// was given in capital letters and turn it to lowercase
|
||||
getAccountFromBrainkey(brainKey.toLowerCase())
|
||||
} else {
|
||||
// Otherwise we turn the whole brainkey to capital letters
|
||||
getAccountFromBrainkey(brainKey.toUpperCase())
|
||||
}
|
||||
} else {
|
||||
// If no case switching should take place, we perform the network call with
|
||||
// the brainkey as it was provided to us.
|
||||
getAccountFromBrainkey(brainKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that will send a network request asking for all the accounts that make use of the
|
||||
* key derived from a give brain key.
|
||||
*
|
||||
* @param brainKey The brain key the user has just typed
|
||||
*/
|
||||
private fun getAccountFromBrainkey(brainKey: String) {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
|
||||
Log.d(TAG, "handleResponse.Thread: " + Thread.currentThread().name)
|
||||
if (response.id == keyReferencesRequestId) {
|
||||
val resp = response.result as List<List<UserAccount>>
|
||||
val accountList: List<UserAccount> = resp[0].distinct()
|
||||
if (accountList.isEmpty() && mKeyReferencesAttempts == 0) {
|
||||
mKeyReferencesAttempts++
|
||||
verifyBrainKey(true)
|
||||
} else {
|
||||
if (accountList.isEmpty()) {
|
||||
//hideDialog()
|
||||
Toast.makeText(applicationContext, R.string.error__invalid_brainkey, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
if (accountList.size == 1) {
|
||||
// If we only found one account linked to this key, then we just proceed
|
||||
// trying to find out the account name
|
||||
mUserAccount = accountList[0]
|
||||
getAccountsRequestId =
|
||||
mNetworkService!!.sendMessage(GetAccounts(mUserAccount), GetAccounts.REQUIRED_API)
|
||||
} else {
|
||||
// If we found more than one account linked to this key, we must also
|
||||
// find out the account names, but the procedure is a bit different in
|
||||
// that after having those, we must still ask the user to decide which
|
||||
// account should be imported.
|
||||
mUserAccountCandidates = accountList
|
||||
getAccountsRequestId = mNetworkService!!.sendMessage(
|
||||
GetAccounts(mUserAccountCandidates),
|
||||
GetAccounts.REQUIRED_API
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (response.id == getAccountsRequestId) {
|
||||
val accountPropertiesList = response.result as List<AccountProperties>
|
||||
if (accountPropertiesList.size > 1) {
|
||||
val candidates = ArrayList<String>()
|
||||
for (accountProperties in accountPropertiesList) {
|
||||
candidates.add(accountProperties.name)
|
||||
}
|
||||
// hideDialog()
|
||||
MaterialDialog(this)
|
||||
.title(R.string.dialog__account_candidates_title)
|
||||
.message(R.string.dialog__account_candidates_content)
|
||||
.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])
|
||||
}
|
||||
}
|
||||
.positiveButton(android.R.string.ok)
|
||||
.negativeButton(android.R.string.cancel) {
|
||||
mKeyReferencesAttempts = 0
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
} else if (accountPropertiesList.size == 1) {
|
||||
onAccountSelected(accountPropertiesList[0])
|
||||
} else {
|
||||
Toast.makeText(applicationContext, R.string.error__try_again, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
|
||||
Log.d(TAG, "handleConnectionStatusUpdate. code: " + connectionStatusUpdate.updateCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called internally once an account has been detected. This method will store internally
|
||||
* the following details:
|
||||
*
|
||||
* - Account name in the database
|
||||
* - Account authorities in the database
|
||||
* - The current account id in the shared preferences
|
||||
*
|
||||
* @param accountProperties Account properties object
|
||||
*/
|
||||
private fun onAccountSelected(accountProperties: AccountProperties) {
|
||||
mUserAccount!!.name = accountProperties.name
|
||||
|
||||
val encryptedPIN = CryptoUtils.encrypt(this, tietPin.text!!.toString())
|
||||
|
||||
// Stores the user selected PIN encrypted
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putString(Constants.KEY_ENCRYPTED_PIN, encryptedPIN)
|
||||
.apply()
|
||||
|
||||
// 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 userAccount = cy.agorise.bitsybitshareswallet.database.entities.UserAccount(id, name, isLTM)
|
||||
|
||||
val userAccountRepository = UserAccountRepository(application)
|
||||
userAccountRepository.insert(userAccount)
|
||||
|
||||
// Stores the id of the currently active user account
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId)
|
||||
.apply()
|
||||
|
||||
// Trying to store all possible authorities (owner, active and memo) into the database
|
||||
val ownerAuthority = accountProperties.owner
|
||||
val activeAuthority = accountProperties.active
|
||||
val options = accountProperties.options
|
||||
|
||||
for (i in 0..2) {
|
||||
mBrainKey!!.sequenceNumber = i
|
||||
val publicKey = PublicKey(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
|
||||
|
||||
if (ownerAuthority.keyAuths.keys.contains(publicKey)) {
|
||||
addAuthorityToDatabase(accountProperties.id, AuthorityType.OWNER.ordinal, mBrainKey!!)
|
||||
}
|
||||
if (activeAuthority.keyAuths.keys.contains(publicKey)) {
|
||||
addAuthorityToDatabase(accountProperties.id, AuthorityType.ACTIVE.ordinal, mBrainKey!!)
|
||||
}
|
||||
if (options.memoKey == publicKey) {
|
||||
addAuthorityToDatabase(accountProperties.id, AuthorityType.MEMO.ordinal, mBrainKey!!)
|
||||
}
|
||||
}
|
||||
|
||||
// Stores a flag into the SharedPreferences to tell the app there is an active account and there is no need
|
||||
// to show this activity again, until the account is removed.
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putBoolean(Constants.KEY_INITIAL_SETUP_DONE, true)
|
||||
.apply()
|
||||
|
||||
// Send the user to the MainActivity
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given BrainKey encrypted as AuthorityType of userId.
|
||||
*/
|
||||
private fun addAuthorityToDatabase(userId: String, authorityType: Int, brainKey: BrainKey) {
|
||||
val brainKeyWords = brainKey.brainKey
|
||||
val wif = brainKey.walletImportFormat
|
||||
val sequenceNumber = brainKey.sequenceNumber
|
||||
|
||||
val encryptedBrainKey = CryptoUtils.encrypt(this, brainKeyWords)
|
||||
val encryptedSequenceNumber = CryptoUtils.encrypt(this, sequenceNumber.toString())
|
||||
val encryptedWIF = CryptoUtils.encrypt(this, wif)
|
||||
|
||||
val authority = Authority(0, userId, authorityType, encryptedWIF, encryptedBrainKey, encryptedSequenceNumber)
|
||||
|
||||
val authorityRepository = AuthorityRepository(this)
|
||||
authorityRepository.insert(authority)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import kotlinx.android.synthetic.main.activity_license.*
|
||||
|
||||
class LicenseActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_license)
|
||||
|
||||
// Get version number of the last agreed license version
|
||||
val agreedLicenseVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, 0)
|
||||
|
||||
// If the last agreed license version is the actual one then proceed to the following Activities
|
||||
if (agreedLicenseVersion == Constants.CURRENT_LICENSE_VERSION) {
|
||||
agree()
|
||||
} else {
|
||||
wbLA.loadData(getString(R.string.licence_html), "text/html", "UTF-8")
|
||||
|
||||
btnDisagree.setOnClickListener { finish() }
|
||||
|
||||
btnAgree.setOnClickListener { agree() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function stores the version of the current accepted license version into the Shared Preferences and
|
||||
* sends the user to import/create account if there is no active account or to the MainActivity otherwise.
|
||||
*/
|
||||
private fun agree() {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION).apply()
|
||||
|
||||
val intent : Intent?
|
||||
|
||||
val initialSetupDone = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(Constants.KEY_INITIAL_SETUP_DONE, false)
|
||||
|
||||
intent = if (!initialSetupDone)
|
||||
Intent(this, ImportBrainkeyActivity::class.java)
|
||||
else
|
||||
Intent(this, MainActivity::class.java)
|
||||
|
||||
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -1,90 +1,114 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import cy.agorise.bitsybitshareswallet.BuildConfig
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.MenuItem
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.onNavDestinationSelected
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.fragments.BalancesFragment
|
||||
import cy.agorise.bitsybitshareswallet.fragments.MerchantsFragment
|
||||
import cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment
|
||||
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : ConnectedActivity() {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
/**
|
||||
* The [androidx.fragment.app.FragmentPagerAdapter] that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentPagerAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory. If this becomes too memory intensive, it
|
||||
* may be best to switch to a
|
||||
* [androidx.fragment.app.FragmentStatePagerAdapter].
|
||||
*/
|
||||
private var mSectionsPagerAdapter: SectionsPagerAdapter? = null
|
||||
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
|
||||
private lateinit var mHandler: Handler
|
||||
private lateinit var mRunnable: Runnable
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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)) {
|
||||
setTheme(R.style.Theme_Bitsy_Dark_NoActionBar)
|
||||
}
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
viewPager.adapter = mSectionsPagerAdapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
val host: NavHostFragment = supportFragmentManager
|
||||
.findFragmentById(R.id.navHostFragment) as NavHostFragment? ?: return
|
||||
|
||||
// Force first tab to show BTS icon
|
||||
tabLayout.getTabAt(0)?.setIcon(R.drawable.tab_home_selector)
|
||||
// Set up Action Bar with Navigation's controller
|
||||
val navController = host.navController
|
||||
|
||||
initBottomBar()
|
||||
appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||
|
||||
ivSettings.setOnClickListener {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
// Sets up the ActionBar with the navigation controller so that it automatically responds to clicks on toolbar
|
||||
// menu items and shows the up navigation button on all fragments except home (Balances)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
|
||||
mHandler = Handler()
|
||||
|
||||
// When this runnable finishes it first verifies if the auto close feature is enabled and if it is then it
|
||||
// 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))
|
||||
finish()
|
||||
else
|
||||
restartHandler()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initBottomBar() {
|
||||
// Show app version number in bottom bar
|
||||
tvBuildVersion.text = String.format("v%s", BuildConfig.VERSION_NAME)
|
||||
|
||||
// Show block number in bottom bar
|
||||
tvBlockNumber.text = getString(R.string.block_number_bottom_bar, "-----")
|
||||
|
||||
// TODO add listener to update block number
|
||||
startHandler()
|
||||
}
|
||||
|
||||
/**
|
||||
* A [FragmentPagerAdapter] that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
* Restarts the Handler (timer) each time there is user's interaction
|
||||
*/
|
||||
inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
|
||||
override fun onUserInteraction() {
|
||||
super.onUserInteraction()
|
||||
restartHandler()
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> BalancesFragment()
|
||||
1 -> TransactionsFragment()
|
||||
else -> MerchantsFragment()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Stops and then restarts the Handler
|
||||
*/
|
||||
private fun restartHandler() {
|
||||
stopHandler()
|
||||
startHandler()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
// Show 3 total pages.
|
||||
return 3
|
||||
}
|
||||
private fun stopHandler() {
|
||||
mHandler.removeCallbacks(mRunnable)
|
||||
}
|
||||
|
||||
private fun startHandler() {
|
||||
mHandler.postDelayed(mRunnable, 3 * 60 * 1000) //for 3 minutes
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Have the NavigationUI look for an action or destination matching the menu
|
||||
// item id and navigate there if found.
|
||||
// Otherwise, bubble up to the parent.
|
||||
return item.onNavDestinationSelected(findNavController(R.id.navHostFragment))
|
||||
|| super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
// Allows NavigationUI to support proper up navigation or the drawer layout
|
||||
// drawer menu, depending on the situation
|
||||
return findNavController(R.id.navHostFragment).navigateUp(appBarConfiguration)
|
||||
}
|
||||
|
||||
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method called whenever there's an update to the connection status
|
||||
* @param connectionStatusUpdate Connection status update.
|
||||
*/
|
||||
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence? {
|
||||
return when (position) {
|
||||
0 -> ""
|
||||
1 -> getString(R.string.title_transactions)
|
||||
else -> getString(R.string.title_merchants)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
|
||||
class SendTransactionActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_send_transaction)
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
|
||||
/**
|
||||
* A simple activity for the user to select his preferences
|
||||
*/
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// TODO move the below to a BaseActivity to apply it to all activities
|
||||
// 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)
|
||||
) {
|
||||
setTheme(R.style.AppTheme_Dark)
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
setupActionBar()
|
||||
|
||||
initNightModeSwitch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the [android.app.ActionBar], if the API is available.
|
||||
*/
|
||||
private fun setupActionBar() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
private fun initNightModeSwitch() {
|
||||
val nightModeOn = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
|
||||
|
||||
switchNightMode.isChecked = nightModeOn
|
||||
|
||||
switchNightMode.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
|
||||
.putBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, isChecked).apply()
|
||||
|
||||
// Recreates the activity to apply the selected theme
|
||||
this.recreate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class ReceiveTransactionActivity : AppCompatActivity() {
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_receive_transaction)
|
||||
|
||||
val intent = Intent(this, LicenseActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Asset
|
||||
|
||||
class AssetsAdapter(context: Context, resource: Int, data: List<Asset>) :
|
||||
ArrayAdapter<Asset>(context, resource, data) {
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var cv = convertView
|
||||
|
||||
if (cv == null) {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
cv = inflater.inflate(android.R.layout.simple_spinner_item, parent, false)
|
||||
}
|
||||
|
||||
val text: TextView = cv!!.findViewById(android.R.id.text1)
|
||||
|
||||
val asset = getItem(position)
|
||||
text.text = asset!!.symbol
|
||||
|
||||
return cv
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val v = inflater.inflate(android.R.layout.simple_spinner_dropdown_item, parent, false)
|
||||
val text: TextView = v.findViewById(android.R.id.text1)
|
||||
|
||||
val asset = getItem(position)
|
||||
text.text = asset!!.symbol
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.ArrayAdapter
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Asset
|
||||
|
||||
class AutoSuggestAssetAdapter(context: Context, resource: Int):
|
||||
ArrayAdapter<Asset>(context, resource) {
|
||||
|
||||
private var mAssets = ArrayList<Asset>()
|
||||
|
||||
fun setData(assets: List<Asset>) {
|
||||
mAssets.clear()
|
||||
mAssets.addAll(assets)
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return mAssets.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Asset? {
|
||||
return mAssets[position]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||
|
||||
class BalancesAdapter(private val context: Context) :
|
||||
RecyclerView.Adapter<BalancesAdapter.ViewHolder>() {
|
||||
|
||||
private val mComparator =
|
||||
Comparator<BalanceDetail> { a, b -> a.symbol.compareTo(b.symbol) }
|
||||
|
||||
private val mSortedList =
|
||||
SortedList<BalanceDetail>(BalanceDetail::class.java, object : SortedList.Callback<BalanceDetail>() {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int) {
|
||||
notifyItemRangeChanged(position, count)
|
||||
}
|
||||
|
||||
override fun compare(a: BalanceDetail, b: BalanceDetail): Int {
|
||||
return mComparator.compare(a, b)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: BalanceDetail, newItem: BalanceDetail): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: BalanceDetail, item2: BalanceDetail): Boolean {
|
||||
return item1.id == item2.id
|
||||
}
|
||||
})
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val tvSymbol: TextView = itemView.findViewById(R.id.tvSymbol)
|
||||
val tvAmount: TextView = itemView.findViewById(R.id.tvAmount)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BalancesAdapter.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
val balanceView = inflater.inflate(R.layout.item_balance, parent, false)
|
||||
|
||||
return ViewHolder(balanceView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val balance = mSortedList.get(position)
|
||||
|
||||
viewHolder.tvSymbol.text = balance.symbol
|
||||
|
||||
val amount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
|
||||
viewHolder.tvAmount.text = String.format("%." + Math.min(balance.precision, 8) + "f", amount)
|
||||
}
|
||||
|
||||
fun add(balance: BalanceDetail) {
|
||||
mSortedList.add(balance)
|
||||
}
|
||||
|
||||
fun remove(balance: BalanceDetail) {
|
||||
mSortedList.remove(balance)
|
||||
}
|
||||
|
||||
fun add(balances: List<BalanceDetail>) {
|
||||
mSortedList.addAll(balances)
|
||||
}
|
||||
|
||||
fun remove(balances: List<BalanceDetail>) {
|
||||
mSortedList.beginBatchedUpdates()
|
||||
for (balance in balances) {
|
||||
mSortedList.remove(balance)
|
||||
}
|
||||
mSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
fun replaceAll(balances: List<BalanceDetail>) {
|
||||
mSortedList.beginBatchedUpdates()
|
||||
for (i in mSortedList.size() - 1 downTo 0) {
|
||||
val balance = mSortedList.get(i)
|
||||
if (!balances.contains(balance)) {
|
||||
mSortedList.remove(balance)
|
||||
}
|
||||
}
|
||||
mSortedList.addAll(balances)
|
||||
mSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mSortedList.size()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||
|
||||
|
||||
|
||||
class BalancesDetailsAdapter(context: Context, resource: Int, data: List<BalanceDetail>) :
|
||||
ArrayAdapter<BalanceDetail>(context, resource, data) {
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var cv = convertView
|
||||
|
||||
if (cv == null) {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
cv = inflater.inflate(android.R.layout.simple_spinner_item, parent, false)
|
||||
}
|
||||
|
||||
val text: TextView = cv!!.findViewById(android.R.id.text1)
|
||||
|
||||
val balanceDetail = getItem(position)
|
||||
text.text = balanceDetail!!.symbol
|
||||
|
||||
return cv
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val v = inflater.inflate(android.R.layout.simple_spinner_dropdown_item, parent, false)
|
||||
val text: TextView = v.findViewById(android.R.id.text1)
|
||||
|
||||
val balanceDetail = getItem(position)
|
||||
text.text = balanceDetail!!.symbol
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.graphenej.network.FullNode
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* Adapter used to populate the elements of the Bitshares nodes dialog in order to show a list of
|
||||
* nodes with their latency.
|
||||
*/
|
||||
class FullNodesAdapter(private val context: Context) : RecyclerView.Adapter<FullNodesAdapter.ViewHolder>() {
|
||||
val TAG: String = this.javaClass.name
|
||||
|
||||
private val mComparator =
|
||||
Comparator<FullNode> { a, b -> java.lang.Double.compare(a.latencyValue, b.latencyValue) }
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val tvNodeName: TextView = itemView.findViewById(R.id.tvNodeName)
|
||||
val ivNodeStatus: ImageView = itemView.findViewById(R.id.ivNodeStatus)
|
||||
}
|
||||
|
||||
private val mSortedList = SortedList(FullNode::class.java, object : SortedList.Callback<FullNode>() {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int) {
|
||||
notifyItemRangeChanged(position, count)
|
||||
}
|
||||
|
||||
override fun compare(a: FullNode, b: FullNode): Int {
|
||||
return mComparator.compare(a, b)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: FullNode, newItem: FullNode): Boolean {
|
||||
return oldItem.latencyValue == newItem.latencyValue
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: FullNode, item2: FullNode): Boolean {
|
||||
return item1.url == item2.url
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FullNodesAdapter.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
val transactionView = inflater.inflate(R.layout.item_node, parent, false)
|
||||
|
||||
return ViewHolder(transactionView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val fullNode = mSortedList[position]
|
||||
|
||||
// Show the green check mark before the node name if that node is the one being used
|
||||
if (fullNode.isConnected)
|
||||
viewHolder.ivNodeStatus.setImageResource(R.drawable.ic_connected)
|
||||
else
|
||||
viewHolder.ivNodeStatus.setImageDrawable(null)
|
||||
|
||||
val latency = fullNode.latencyValue
|
||||
|
||||
// Select correct color span according to the latency value
|
||||
val colorSpan = when {
|
||||
latency < 400 -> ForegroundColorSpan(ContextCompat.getColor(context, R.color.ppGreen))
|
||||
latency < 800 -> ForegroundColorSpan(Color.rgb(255,136,0)) // Holo orange
|
||||
else -> ForegroundColorSpan(Color.rgb(204,0,0)) // Holo red
|
||||
}
|
||||
|
||||
// Create a string with the latency number colored according to their amount
|
||||
val ssb = SpannableStringBuilder()
|
||||
ssb.append(fullNode.url.replace("wss://", ""), StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
ssb.append(" (")
|
||||
|
||||
// 2000 ms is the timeout of the websocket used to calculate the latency, therefore if the
|
||||
// received latency is greater than such value we can assume the node was not reachable.
|
||||
val ms = if(latency < 2000) "%.0f ms".format(latency) else "??"
|
||||
|
||||
ssb.append(ms, colorSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
ssb.append(")")
|
||||
|
||||
viewHolder.tvNodeName.text = ssb
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions that adds/updates a FullNode to the SortedList
|
||||
*/
|
||||
fun add(fullNode: FullNode) {
|
||||
// Remove the old instance of the FullNode before adding a new one. My understanding is that
|
||||
// the sorted list should be able to automatically find repeated elements and update them
|
||||
// instead of adding duplicates but it wasn't working so I opted for manually removing old
|
||||
// instances of FullNodes before adding the updated ones.
|
||||
var removed = 0
|
||||
for (i in 0 until mSortedList.size())
|
||||
if (mSortedList[i-removed].url == (fullNode.url))
|
||||
mSortedList.removeItemAt(i-removed++)
|
||||
|
||||
mSortedList.add(fullNode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that adds a whole list of nodes to the SortedList. It should only be used at the
|
||||
* moment of populating the SortedList for the first time.
|
||||
*/
|
||||
fun add(fullNodes: List<FullNode>) {
|
||||
mSortedList.addAll(fullNodes)
|
||||
}
|
||||
|
||||
fun remove(fullNode: FullNode) {
|
||||
mSortedList.remove(fullNode)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mSortedList.size()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
package cy.agorise.bitsybitshareswallet.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class TransfersDetailsAdapter(private val context: Context) :
|
||||
RecyclerView.Adapter<TransfersDetailsAdapter.ViewHolder>() {
|
||||
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")!!
|
||||
|
||||
private val mComparator =
|
||||
Comparator<TransferDetail> { a, b -> b.id.compareTo(a.id) }
|
||||
|
||||
private val mSortedList =
|
||||
SortedList<TransferDetail>(TransferDetail::class.java, object : SortedList.Callback<TransferDetail>() {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int) {
|
||||
notifyItemRangeChanged(position, count)
|
||||
}
|
||||
|
||||
override fun compare(a: TransferDetail, b: TransferDetail): Int {
|
||||
return mComparator.compare(a, b)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: TransferDetail, newItem: TransferDetail): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: TransferDetail, item2: TransferDetail): Boolean {
|
||||
return item1.id == item2.id
|
||||
}
|
||||
})
|
||||
|
||||
private val dateFormat: SimpleDateFormat
|
||||
private val timeFormat: SimpleDateFormat
|
||||
|
||||
init {
|
||||
val locale = context.resources.configuration.locale
|
||||
dateFormat = SimpleDateFormat("dd MMM", locale)
|
||||
timeFormat = SimpleDateFormat("HH:mm:ss z", locale)
|
||||
}
|
||||
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val rootView: ConstraintLayout = itemView.findViewById(R.id.rootView)
|
||||
val vPaymentDirection: View = itemView.findViewById(R.id.vPaymentDirection)
|
||||
val tvFrom: TextView = itemView.findViewById(R.id.tvFrom)
|
||||
val ivDirectionArrow: ImageView = itemView.findViewById(R.id.ivDirectionArrow)
|
||||
val tvTo: TextView = itemView.findViewById(R.id.tvTo)
|
||||
val llMemo: LinearLayout = itemView.findViewById(R.id.llMemo)
|
||||
val tvMemo: TextView = itemView.findViewById(R.id.tvMemo)
|
||||
val tvDate: TextView = itemView.findViewById(R.id.tvDate)
|
||||
val tvTime: TextView = itemView.findViewById(R.id.tvTime)
|
||||
val tvCryptoAmount: TextView = itemView.findViewById(R.id.tvCryptoAmount)
|
||||
val tvFiatEquivalent: TextView = itemView.findViewById(R.id.tvFiatEquivalent)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransfersDetailsAdapter.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
val transactionView = inflater.inflate(R.layout.item_transaction, parent, false)
|
||||
|
||||
return ViewHolder(transactionView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val transferDetail = mSortedList.get(position)
|
||||
|
||||
viewHolder.vPaymentDirection.setBackgroundColor(context.resources.getColor(
|
||||
if(transferDetail.direction) R.color.colorReceive else R.color.colorSend
|
||||
))
|
||||
|
||||
viewHolder.tvFrom.text = transferDetail.from ?: ""
|
||||
viewHolder.tvTo.text = transferDetail.to ?: ""
|
||||
|
||||
// Shows memo if available
|
||||
val memo = transferDetail.memo
|
||||
if (memo == "") {
|
||||
viewHolder.tvMemo.text = ""
|
||||
viewHolder.llMemo.visibility = View.GONE
|
||||
} else {
|
||||
viewHolder.tvMemo.text = memo
|
||||
viewHolder.llMemo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Format date and time
|
||||
val date = Date(transferDetail.date * 1000)
|
||||
|
||||
viewHolder.tvDate.text = dateFormat.format(date)
|
||||
viewHolder.tvTime.text = timeFormat.format(date)
|
||||
|
||||
// Show the crypto amount correctly formatted
|
||||
// TODO lift the DecimalFormat declaration to other place to make things more efficient
|
||||
val df = DecimalFormat("####."+("#".repeat(transferDetail.cryptoPrecision)))
|
||||
df.roundingMode = RoundingMode.CEILING
|
||||
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
|
||||
val amount = transferDetail.cryptoAmount.toDouble() /
|
||||
Math.pow(10.toDouble(), transferDetail.cryptoPrecision.toDouble())
|
||||
val cryptoAmount = "${df.format(amount)} ${transferDetail.cryptoSymbol}"
|
||||
viewHolder.tvCryptoAmount.text = cryptoAmount
|
||||
|
||||
viewHolder.tvFiatEquivalent.text = "$4119.75"
|
||||
|
||||
viewHolder.ivDirectionArrow.setImageDrawable(context.getDrawable(
|
||||
if(transferDetail.direction) R.drawable.ic_arrow_receive else R.drawable.ic_arrow_send
|
||||
))
|
||||
}
|
||||
|
||||
fun add(transferDetail: TransferDetail) {
|
||||
mSortedList.add(transferDetail)
|
||||
}
|
||||
|
||||
fun remove(transferDetail: TransferDetail) {
|
||||
mSortedList.remove(transferDetail)
|
||||
}
|
||||
|
||||
fun add(transfersDetails: List<TransferDetail>) {
|
||||
mSortedList.addAll(transfersDetails)
|
||||
}
|
||||
|
||||
fun remove(transfersDetails: List<TransferDetail>) {
|
||||
mSortedList.beginBatchedUpdates()
|
||||
for (transferDetail in transfersDetails) {
|
||||
mSortedList.remove(transferDetail)
|
||||
}
|
||||
mSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
fun replaceAll(transfersDetails: List<TransferDetail>) {
|
||||
mSortedList.beginBatchedUpdates()
|
||||
for (i in mSortedList.size() - 1 downTo 0) {
|
||||
val transferDetail = mSortedList.get(i)
|
||||
if (!transfersDetails.contains(transferDetail)) {
|
||||
mSortedList.remove(transferDetail)
|
||||
}
|
||||
}
|
||||
mSortedList.addAll(transfersDetails)
|
||||
mSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mSortedList.size()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package cy.agorise.bitsybitshareswallet.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.daos.*
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.*
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetailDao
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetailDao
|
||||
|
||||
@Database(entities = [
|
||||
Asset::class,
|
||||
Authority::class,
|
||||
Balance::class,
|
||||
EquivalentValue::class,
|
||||
Transfer::class,
|
||||
UserAccount::class
|
||||
],
|
||||
version = 1, exportSchema = false)
|
||||
abstract class BitsyDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun assetDao(): AssetDao
|
||||
abstract fun authorityDao(): AuthorityDao
|
||||
abstract fun balanceDao(): BalanceDao
|
||||
abstract fun equivalentValueDao(): EquivalentValueDao
|
||||
abstract fun transferDao(): TransferDao
|
||||
abstract fun userAccountDao(): UserAccountDao
|
||||
abstract fun balanceDetailDao(): BalanceDetailDao
|
||||
abstract fun transferDetailDao(): TransferDetailDao
|
||||
|
||||
companion object {
|
||||
|
||||
// To make sure there is always only one instance of the database open
|
||||
@Volatile
|
||||
private var INSTANCE: BitsyDatabase? = null
|
||||
|
||||
internal fun getDatabase(context: Context): BitsyDatabase? {
|
||||
if (INSTANCE == null) {
|
||||
synchronized(BitsyDatabase::class.java) {
|
||||
INSTANCE = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
BitsyDatabase::class.java, "BiTSyWallet.db"
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
return INSTANCE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Asset
|
||||
|
||||
@Dao
|
||||
interface AssetDao {
|
||||
@Insert
|
||||
fun insert(asset: Asset)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(assets: List<Asset>)
|
||||
|
||||
@Query("SELECT * FROM assets")
|
||||
fun getAll(): LiveData<List<Asset>>
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Authority
|
||||
import io.reactivex.Single
|
||||
|
||||
@Dao
|
||||
interface AuthorityDao {
|
||||
@Insert
|
||||
fun insert(authority: Authority)
|
||||
|
||||
@Query("SELECT * FROM authorities WHERE user_id=:userId LIMIT 1")
|
||||
fun get(userId: String): Single<Authority>
|
||||
|
||||
@Query("SELECT * FROM authorities")
|
||||
fun getAll(): LiveData<List<Authority>>
|
||||
|
||||
@Query("SELECT encrypted_wif FROM authorities WHERE user_id=:userId AND authority_type=:authorityType")
|
||||
fun getWIF(userId: String, authorityType: Int): Single<String>
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Balance
|
||||
|
||||
@Dao
|
||||
interface BalanceDao {
|
||||
@Insert
|
||||
fun insert(balance: Balance)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(balances: List<Balance>)
|
||||
|
||||
@Query("SELECT * FROM balances")
|
||||
fun getAll(): LiveData<List<Balance>>
|
||||
|
||||
// TODO not sure if this is the best place for this query as it involves two entities
|
||||
@Query("SELECT DISTINCT asset_id FROM balances WHERE asset_id NOT IN (SELECT id FROM assets)")
|
||||
fun getMissingAssetIds(): LiveData<List<String>>
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.EquivalentValue
|
||||
|
||||
@Dao
|
||||
interface EquivalentValueDao {
|
||||
@Insert
|
||||
fun insert(equivalentValue: EquivalentValue)
|
||||
|
||||
@Query("SELECT * FROM equivalent_values")
|
||||
fun getAllEquivalentValues(): LiveData<List<EquivalentValue>>
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
|
||||
import io.reactivex.Single
|
||||
|
||||
@Dao
|
||||
interface TransferDao {
|
||||
@Insert
|
||||
fun insert(transfer: Transfer)
|
||||
|
||||
// TODO find a way to return number of added rows
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertAll(transfers: List<Transfer>)
|
||||
|
||||
@Query("UPDATE transfers SET timestamp=:timestamp WHERE block_number=:blockNumber")
|
||||
fun setBlockTime(blockNumber: Long, timestamp: Long)
|
||||
|
||||
@Query("SELECT * FROM transfers")
|
||||
fun getAll(): LiveData<List<Transfer>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM transfers")
|
||||
fun getCount(): Single<Int>
|
||||
|
||||
@Query("SELECT block_number FROM transfers WHERE timestamp='0' LIMIT 1")
|
||||
fun getTransferBlockNumberWithMissingTime(): LiveData<Long>
|
||||
|
||||
@Query("DELETE FROM transfers")
|
||||
fun deleteAll()
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
|
||||
|
||||
@Dao
|
||||
interface UserAccountDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(userAccount: UserAccount)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertAll(userAccounts: List<UserAccount>)
|
||||
|
||||
@Query("SELECT * FROM user_accounts WHERE user_accounts.id = :id")
|
||||
fun getUserAccount(id: String): LiveData<UserAccount>
|
||||
|
||||
@Query("SELECT * FROM user_accounts")
|
||||
fun getAll(): LiveData<List<UserAccount>>
|
||||
|
||||
// TODO not sure if this is the best place for this query as it involves two entities
|
||||
@Query("SELECT DISTINCT destination FROM transfers WHERE destination NOT IN (SELECT id FROM user_accounts) UNION SELECT DISTINCT source FROM transfers WHERE source NOT IN (SELECT id FROM user_accounts)")
|
||||
fun getMissingAccountIds(): LiveData<List<String>>
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "assets")
|
||||
data class Asset(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id") val id: String,
|
||||
@ColumnInfo(name = "symbol") val symbol: String,
|
||||
@ColumnInfo(name = "precision") val precision: Int,
|
||||
@ColumnInfo(name = "description") val description: String,
|
||||
@ColumnInfo(name = "bit_asset_id") val bitAssetId: String
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return symbol
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "authorities")
|
||||
data class Authority (
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "id") val id: Long,
|
||||
@ColumnInfo(name = "user_id") val userId: String,
|
||||
@ColumnInfo(name = "authority_type") val authorityType: Int,
|
||||
@ColumnInfo(name = "encrypted_wif") val encryptedWIF: String,
|
||||
@ColumnInfo(name = "encrypted_brain_key") val encryptedBrainKey: String,
|
||||
@ColumnInfo(name = "encrypted_sequence_number") val encryptedSequenceNumber: String
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "balances")
|
||||
data class Balance(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "asset_id") val assetId: String, // TODO should be foreign key?
|
||||
@ColumnInfo(name = "asset_amount") val assetAmount: Long,
|
||||
@ColumnInfo(name = "last_update") val lastUpdate: Long
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "equivalent_values",foreignKeys =
|
||||
[ForeignKey(
|
||||
entity = Transfer::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["transfer_id"]
|
||||
), ForeignKey(
|
||||
entity = Asset::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["asset_id"]
|
||||
)]
|
||||
)
|
||||
data class EquivalentValue (
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "id") val id: Long,
|
||||
@ColumnInfo(name = "transfer_id") val transferId: String,
|
||||
@ColumnInfo(name = "value") val value: Long,
|
||||
@ColumnInfo(name = "asset_id") val assetId: String
|
||||
)
|
|
@ -0,0 +1,20 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "transfers")
|
||||
data class Transfer (
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id") val id: String,
|
||||
@ColumnInfo(name = "block_number") val blockNumber: Long,
|
||||
@ColumnInfo(name = "timestamp") val timestamp: Long,
|
||||
@ColumnInfo(name = "fee_amount") val feeAmount: Long,
|
||||
@ColumnInfo(name = "fee_asset_id") val feeAssetId: String, // TODO should be foreign key to Asset
|
||||
@ColumnInfo(name = "source") val source: String, // TODO should be foreign key to UserAccount
|
||||
@ColumnInfo(name = "destination") val destination: String, // TODO should be foreign key to UserAccount
|
||||
@ColumnInfo(name = "transfer_amount") val transferAmount: Long,
|
||||
@ColumnInfo(name = "transfer_asset_id") val transferAssetId: String, // TODO should be foreign key to Asset
|
||||
@ColumnInfo(name = "memo") val memo: String
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "user_accounts")
|
||||
data class UserAccount (
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id") val id: String,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "is_ltm") val isLtm: Boolean
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.joins
|
||||
|
||||
data class BalanceDetail(
|
||||
val id: String,
|
||||
val amount: Long,
|
||||
val precision: Int,
|
||||
val symbol: String
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.joins
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface BalanceDetailDao {
|
||||
@Query("SELECT assets.id AS id, balances.asset_amount AS amount, assets.precision, assets.symbol " +
|
||||
"FROM balances INNER JOIN assets on balances.asset_id = assets.id")
|
||||
fun getAll(): LiveData<List<BalanceDetail>>
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.joins
|
||||
|
||||
data class TransferDetail(
|
||||
val id: String,
|
||||
val from: String?,
|
||||
val to: String?,
|
||||
val direction: Boolean, // True -> Received, False -> Sent
|
||||
val memo: String,
|
||||
val date: Long,
|
||||
val cryptoAmount: Long,
|
||||
val cryptoPrecision: Int,
|
||||
val cryptoSymbol: String
|
||||
// val fiatAmount: Long,
|
||||
// val fiatCurrency: String
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package cy.agorise.bitsybitshareswallet.database.joins
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface TransferDetailDao {
|
||||
|
||||
@Query("SELECT transfers.id, (SELECT name FROM user_accounts WHERE user_accounts.id=transfers.source) AS `from`, (SELECT name FROM user_accounts WHERE user_accounts.id=transfers.destination) AS `to`, (CASE WHEN destination=:userId THEN 1 ELSE 0 END) AS `direction`, transfers.memo AS `memo`, transfers.timestamp AS `date`, transfers.transfer_amount AS `cryptoAmount`, assets.precision AS `cryptoPrecision`, assets.symbol AS cryptoSymbol FROM transfers INNER JOIN assets WHERE transfers.transfer_asset_id = assets.id")
|
||||
fun getAll(userId: String): LiveData<List<TransferDetail>>
|
||||
}
|
|
@ -1,52 +1,55 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
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.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.activities.ReceiveTransactionActivity
|
||||
import cy.agorise.bitsybitshareswallet.activities.SendTransactionActivity
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.BalancesViewModel
|
||||
import cy.agorise.bitsybitshareswallet.adapters.BalancesAdapter
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_balances.*
|
||||
|
||||
class BalancesFragment : Fragment() {
|
||||
class BalancesFragment: Fragment() {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
companion object {
|
||||
fun newInstance() = BalancesFragment()
|
||||
}
|
||||
|
||||
private lateinit var viewModel: BalancesViewModel
|
||||
private lateinit var mBalanceDetailViewModel: BalanceDetailViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_balances, container, false)
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(this).get(BalancesViewModel::class.java)
|
||||
// TODO: Use the ViewModel
|
||||
return inflater.inflate(R.layout.fragment_balances, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
btnReceive.setOnClickListener {
|
||||
val intent = Intent(view.context, ReceiveTransactionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
// Configure BalanceDetailViewModel to show the current balances
|
||||
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
|
||||
|
||||
btnSend.setOnClickListener {
|
||||
val intent = Intent(view.context, SendTransactionActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
val balancesAdapter = BalancesAdapter(context!!)
|
||||
rvBalances.adapter = balancesAdapter
|
||||
rvBalances.layoutManager = LinearLayoutManager(context!!)
|
||||
rvBalances.addItemDecoration(DividerItemDecoration(context!!, DividerItemDecoration.VERTICAL))
|
||||
|
||||
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
|
||||
balancesAdapter.replaceAll(balancesDetails)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
if (isVisibleToUser) {
|
||||
// TODO find a better way to recreate the fragment, that does it only when the theme has been changed
|
||||
fragmentManager!!.beginTransaction().detach(this).attach(this).commit()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Message
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.ClassCastException
|
||||
|
||||
|
||||
/**
|
||||
* Creates a Dialog that communicates with {@link TransactionsActivity} to give it parameters about
|
||||
* how to filter the list of Transactions
|
||||
*/
|
||||
class FilterOptionsDialog : DialogFragment() {
|
||||
|
||||
// Widgets TODO use android-kotlin-extensions {onViewCreated}
|
||||
lateinit var rbTransactionAll: RadioButton
|
||||
lateinit var rbTransactionSent: RadioButton
|
||||
lateinit var rbTransactionReceived: RadioButton
|
||||
lateinit var cbDateRange: CheckBox
|
||||
lateinit var llDateRange: LinearLayout
|
||||
lateinit var tvStartDate: TextView
|
||||
lateinit var tvEndDate: TextView
|
||||
lateinit var cbCryptocurrency: CheckBox
|
||||
lateinit var sCryptocurrency: Spinner
|
||||
lateinit var cbFiatAmount: CheckBox
|
||||
lateinit var llFiatAmount: LinearLayout
|
||||
// lateinit var etFromFiatAmount: CurrencyEditText
|
||||
// lateinit var etToFiatAmount: CurrencyEditText
|
||||
|
||||
private var mCallback: OnFilterOptionsSelectedListener? = null
|
||||
|
||||
private var mDatePickerHandler: DatePickerHandler? = null
|
||||
|
||||
private var dateFormat: SimpleDateFormat = SimpleDateFormat("d/MMM/yyyy",
|
||||
Resources.getSystem().configuration.locale)
|
||||
|
||||
private var startDate: Long = 0
|
||||
private var endDate: Long = 0
|
||||
|
||||
// /**
|
||||
// * Variable used to keep track of the current user's currency
|
||||
// */
|
||||
// private val mUserCurrency = RuntimeData.EXTERNAL_CURRENCY
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_FILTER_TRANSACTION_DIRECTION = "key_filter_transaction_direction"
|
||||
const val KEY_FILTER_DATE_RANGE_ALL = "key_filter_date_range_all"
|
||||
const val KEY_FILTER_START_DATE = "key_filter_start_date"
|
||||
const val KEY_FILTER_END_DATE = "key_filter_end_date"
|
||||
const val KEY_FILTER_CRYPTOCURRENCY_ALL = "key_filter_cryptocurrency_all"
|
||||
const val KEY_FILTER_CRYPTOCURRENCY = "key_filter_cryptocurrency"
|
||||
const val KEY_FILTER_FIAT_AMOUNT_ALL = "key_filter_fiat_amount_all"
|
||||
const val KEY_FILTER_FROM_FIAT_AMOUNT = "filter_from_fiat_amount"
|
||||
const val KEY_FILTER_TO_FIAT_AMOUNT = "filter_to_fiat_amount"
|
||||
|
||||
const val KEY_TIMESTAMP = "key_timestamp"
|
||||
|
||||
const val START_DATE_PICKER = 0
|
||||
const val END_DATE_PICKER = 1
|
||||
|
||||
fun newInstance(filterTransactionsDirection: Int, filterDateRangeAll: Boolean,
|
||||
filterStartDate: Long, filterEndDate: Long, filterCryptocurrencyAll: Boolean,
|
||||
filterCryptocurrency: String, filterFiatAmountAll: Boolean,
|
||||
filterFromFiatAmount: Long, filterToFiatAmount: Long): FilterOptionsDialog {
|
||||
val frag = FilterOptionsDialog()
|
||||
val args = Bundle()
|
||||
args.putInt(KEY_FILTER_TRANSACTION_DIRECTION, filterTransactionsDirection)
|
||||
args.putBoolean(KEY_FILTER_DATE_RANGE_ALL, filterDateRangeAll)
|
||||
args.putLong(KEY_FILTER_START_DATE, filterStartDate)
|
||||
args.putLong(KEY_FILTER_END_DATE, filterEndDate)
|
||||
args.putBoolean(KEY_FILTER_CRYPTOCURRENCY_ALL, filterCryptocurrencyAll)
|
||||
args.putString(KEY_FILTER_CRYPTOCURRENCY, filterCryptocurrency)
|
||||
args.putBoolean(KEY_FILTER_FIAT_AMOUNT_ALL, filterFiatAmountAll)
|
||||
args.putLong(KEY_FILTER_FROM_FIAT_AMOUNT, filterFromFiatAmount)
|
||||
args.putLong(KEY_FILTER_TO_FIAT_AMOUNT, filterToFiatAmount)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* DatePicker message handler.
|
||||
*/
|
||||
inner class DatePickerHandler : Handler() {
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
super.handleMessage(msg)
|
||||
val bundle = msg.data
|
||||
val timestamp = bundle.get(KEY_TIMESTAMP) as Long
|
||||
//Log.d(TAG, "timestamp: $timestamp")
|
||||
when (msg.arg1) {
|
||||
START_DATE_PICKER -> {
|
||||
startDate = timestamp
|
||||
|
||||
updateDateTextViews()
|
||||
}
|
||||
END_DATE_PICKER -> {
|
||||
endDate = timestamp
|
||||
|
||||
// Make sure there is at least one moth difference between start and end time
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = endDate
|
||||
calendar.add(Calendar.MONTH, -1)
|
||||
|
||||
val tmpTime = calendar.timeInMillis
|
||||
|
||||
if (tmpTime < startDate)
|
||||
startDate = tmpTime
|
||||
|
||||
updateDateTextViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDateTextViews() {
|
||||
var date = Date(startDate)
|
||||
tvStartDate.text = dateFormat.format(date)
|
||||
|
||||
date = Date(endDate)
|
||||
tvEndDate.text = dateFormat.format(date)
|
||||
}
|
||||
|
||||
// Container Activity must implement this interface
|
||||
interface OnFilterOptionsSelectedListener {
|
||||
fun onFilterOptionsSelected(filterTransactionsDirection: Int,
|
||||
filterDateRangeAll: Boolean,
|
||||
filterStartDate: Long,
|
||||
filterEndDate: Long,
|
||||
filterCryptocurrencyAll: Boolean,
|
||||
filterCryptocurrency: String,
|
||||
filterFiatAmountAll: Boolean,
|
||||
filterFromFiatAmount: Long,
|
||||
filterToFiatAmount: Long)
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
onAttachToParentFragment(parentFragment!!)
|
||||
|
||||
// Initialize handler for communication with the DatePicker
|
||||
mDatePickerHandler = DatePickerHandler()
|
||||
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
.setTitle("Filter options")
|
||||
.setPositiveButton("Filter") { _, _ -> validateFields() }
|
||||
.setNegativeButton("Cancel") { _, _ -> dismiss() }
|
||||
|
||||
// Inflate layout
|
||||
val inflater = activity!!.layoutInflater
|
||||
val view = inflater.inflate(R.layout.dialog_filter_options, null)
|
||||
|
||||
// Initialize Transactions direction
|
||||
rbTransactionAll = view.findViewById(R.id.rbTransactionAll)
|
||||
rbTransactionSent = view.findViewById(R.id.rbTransactionSent)
|
||||
rbTransactionReceived = view.findViewById(R.id.rbTransactionReceived)
|
||||
val radioButtonChecked = arguments!!.getInt(KEY_FILTER_TRANSACTION_DIRECTION, 0)
|
||||
when (radioButtonChecked) {
|
||||
0 -> rbTransactionAll.isChecked = true
|
||||
1 -> rbTransactionSent.isChecked = true
|
||||
2 -> rbTransactionReceived.isChecked = true
|
||||
}
|
||||
|
||||
// Initialize Date range
|
||||
cbDateRange = view.findViewById(R.id.cbDateRange)
|
||||
// llDateRange = view.findViewById(R.id.llDateRange)
|
||||
// cbDateRange.setOnCheckedChangeListener { _, isChecked ->
|
||||
// llDateRange.visibility = if(isChecked) View.GONE else View.VISIBLE }
|
||||
cbDateRange.isChecked = arguments!!.getBoolean(KEY_FILTER_DATE_RANGE_ALL, true)
|
||||
//
|
||||
// tvStartDate = view.findViewById(R.id.tvStartDate)
|
||||
// tvEndDate = view.findViewById(R.id.tvEndDate)
|
||||
//
|
||||
// startDate = arguments!!.getLong(KEY_FILTER_START_DATE, 0)
|
||||
// tvStartDate.setOnClickListener(mDateClickListener)
|
||||
//
|
||||
// endDate = arguments!!.getLong(KEY_FILTER_END_DATE, 0)
|
||||
// tvEndDate.setOnClickListener(mDateClickListener)
|
||||
//
|
||||
// updateDateTextViews()
|
||||
|
||||
// Initialize Cryptocurrency
|
||||
cbCryptocurrency = view.findViewById(R.id.cbCryptocurrency)
|
||||
// sCryptocurrency = view.findViewById(R.id.sCryptocurrency)
|
||||
// cbCryptocurrency.setOnCheckedChangeListener { _, isChecked ->
|
||||
// sCryptocurrency.visibility = if(isChecked) View.GONE else View.VISIBLE }
|
||||
cbCryptocurrency.isChecked = arguments!!.getBoolean(KEY_FILTER_CRYPTOCURRENCY_ALL, true)
|
||||
|
||||
// sCryptocurrency = view.findViewById(R.id.sCryptocurrency)
|
||||
// initializeCryptocurrencySpinner()
|
||||
|
||||
|
||||
// Initialize Fiat amount
|
||||
cbFiatAmount = view.findViewById(R.id.cbFiatAmount)
|
||||
// llFiatAmount = view.findViewById(R.id.llFiatAmount)
|
||||
// cbFiatAmount.setOnCheckedChangeListener { _, isChecked ->
|
||||
// llFiatAmount.visibility = if(isChecked) View.GONE else View.VISIBLE }
|
||||
cbFiatAmount.isChecked = arguments!!.getBoolean(KEY_FILTER_FIAT_AMOUNT_ALL, true)
|
||||
|
||||
// val locale = Resources.getSystem().configuration.locale
|
||||
//
|
||||
// etFromFiatAmount = view.findViewById(R.id.etFromFiatAmount)
|
||||
// etFromFiatAmount.locale = locale
|
||||
// val fromFiatAmount = arguments!!.getLong(KEY_FILTER_FROM_FIAT_AMOUNT, 0)
|
||||
// etFromFiatAmount.setText("$fromFiatAmount", TextView.BufferType.EDITABLE)
|
||||
//
|
||||
// etToFiatAmount = view.findViewById(R.id.etToFiatAmount)
|
||||
// etToFiatAmount.locale = locale
|
||||
// val toFiatAmount = arguments!!.getLong(KEY_FILTER_TO_FIAT_AMOUNT, 0)
|
||||
// etToFiatAmount.setText("$toFiatAmount", TextView.BufferType.EDITABLE)
|
||||
|
||||
builder.setView(view)
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the current [DialogFragment] to its [Fragment] parent, to initialize the
|
||||
* [OnFilterOptionsSelectedListener] interface
|
||||
*/
|
||||
private fun onAttachToParentFragment(fragment: Fragment) {
|
||||
try {
|
||||
mCallback = fragment as OnFilterOptionsSelectedListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(fragment.toString() + " must implement OnFilterOptionsSelectedListener")
|
||||
}
|
||||
}
|
||||
|
||||
// private fun initializeCryptocurrencySpinner() {
|
||||
// val cryptoCurrencyList = database!!.getSortedCryptoCurrencies(false,
|
||||
// SortType.DESCENDING, true)
|
||||
//
|
||||
// val cryptocurrencySpinnerAdapter = CryptocurrencySpinnerAdapter(context!!,
|
||||
// R.layout.item_cryptocurrency,
|
||||
// R.id.tvCryptocurrencyName,
|
||||
// cryptoCurrencyList)
|
||||
//
|
||||
// sCryptocurrency.adapter = cryptocurrencySpinnerAdapter
|
||||
//
|
||||
// val cryptocurrencySelected = arguments!!.getString(KEY_FILTER_CRYPTOCURRENCY)
|
||||
//
|
||||
// val index = Math.max(cryptocurrencySpinnerAdapter.getPosition(database!!.getCryptocurrencyBySymbol(
|
||||
// cryptocurrencySelected)), 0)
|
||||
//
|
||||
// sCryptocurrency.setSelection(index)
|
||||
// }
|
||||
|
||||
// private val mDateClickListener = View.OnClickListener { v ->
|
||||
// val calendar = Calendar.getInstance()
|
||||
//
|
||||
// // Variable used to select that date on the calendar
|
||||
// var currentTime = calendar.timeInMillis
|
||||
// var maxTime = currentTime
|
||||
//
|
||||
// var which = -1
|
||||
// if (v.id == R.id.tvStartDate) {
|
||||
// which = START_DATE_PICKER
|
||||
// currentTime = startDate
|
||||
// calendar.timeInMillis = endDate
|
||||
// calendar.add(Calendar.MONTH, -1)
|
||||
// maxTime = calendar.timeInMillis
|
||||
// } else if (v.id == R.id.tvEndDate) {
|
||||
// which = END_DATE_PICKER
|
||||
// currentTime = endDate
|
||||
// }
|
||||
//
|
||||
// val datePickerFragment = DatePickerFragment.newInstance(which, currentTime,
|
||||
// maxTime, mDatePickerHandler)
|
||||
// datePickerFragment.show(activity!!.supportFragmentManager, "date-picker")
|
||||
// }
|
||||
|
||||
private fun validateFields() {
|
||||
val filterTransactionsDirection = when {
|
||||
rbTransactionAll.isChecked -> 0
|
||||
rbTransactionSent.isChecked -> 1
|
||||
rbTransactionReceived.isChecked -> 2
|
||||
else -> { 0 }
|
||||
}
|
||||
|
||||
val filterDateRangeAll = cbDateRange.isChecked
|
||||
|
||||
val filterCryptocurrencyAll = cbCryptocurrency.isChecked
|
||||
|
||||
val filterCryptocurrency = "" //(sCryptocurrency.selectedItem as CryptoCurrency).symbol
|
||||
|
||||
val filterFiatAmountAll = cbFiatAmount.isChecked
|
||||
|
||||
val filterFromFiatAmount = 0L//(etFromFiatAmount.currencyDouble *
|
||||
// Math.pow(10.0, mUserCurrency.defaultFractionDigits.toDouble())).toLong()
|
||||
|
||||
var filterToFiatAmount = 1L//(etToFiatAmount.currencyDouble *
|
||||
// Math.pow(10.0, mUserCurrency.defaultFractionDigits.toDouble())).toLong()
|
||||
|
||||
// Make sure ToFiatAmount is at least 50 units bigger than FromFiatAmount
|
||||
// if (!filterFiatAmountAll && filterToFiatAmount <= filterFromFiatAmount) {
|
||||
// filterToFiatAmount = filterFromFiatAmount + 50 *
|
||||
// Math.pow(10.0, mUserCurrency.defaultFractionDigits.toDouble()).toLong()
|
||||
// }
|
||||
|
||||
mCallback!!.onFilterOptionsSelected(filterTransactionsDirection, filterDateRangeAll,
|
||||
startDate, endDate, filterCryptocurrencyAll, filterCryptocurrency, filterFiatAmountAll,
|
||||
filterFromFiatAmount, filterToFiatAmount)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private lateinit var mUserAccountViewModel: UserAccountViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
|
||||
toolbar?.navigationIcon = resources.getDrawable(R.drawable.ic_bitsy_logo_2, null)
|
||||
|
||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Configure UserAccountViewModel to show the current account
|
||||
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)
|
||||
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
|
||||
|
||||
mUserAccountViewModel.getUserAccount(userId!!).observe(this, Observer<UserAccount>{ user ->
|
||||
tvAccountName.text = user.name
|
||||
})
|
||||
|
||||
// Navigate to the Receive Transaction Fragment
|
||||
fabReceiveTransaction.setOnClickListener (
|
||||
Navigation.createNavigateOnClickListener(R.id.receive_action)
|
||||
)
|
||||
|
||||
// Navigate to the Send Transaction Fragment without activating the camera
|
||||
fabSendTransaction.setOnClickListener(
|
||||
Navigation.createNavigateOnClickListener(R.id.send_action)
|
||||
)
|
||||
|
||||
// Navigate to the Send Transaction Fragment using Navigation's SafeArgs to activate the camera
|
||||
fabSendTransactionCamera.setOnClickListener {
|
||||
val action = HomeFragmentDirections.sendActionCamera()
|
||||
action.setOpenCamera(true)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
// Configure ViewPager with PagerAdapter and TabLayout to display the Balances/NetWorth section
|
||||
val pagerAdapter = PagerAdapter(fragmentManager!!)
|
||||
viewPager.adapter = pagerAdapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
// Set the pie chart icon for the third tab
|
||||
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) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
// getItem is called to instantiate the fragment for the given page.
|
||||
return if (position == 0)
|
||||
BalancesFragment()
|
||||
else
|
||||
NetWorthFragment()
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence? {
|
||||
return listOf(getString(R.string.title_balances), getString(R.string.title_net_worth), "")[position]
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_home, menu)
|
||||
}
|
||||
}
|
|
@ -1,22 +1,42 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.gms.maps.GoogleMap
|
||||
import com.google.android.gms.maps.OnMapReadyCallback
|
||||
import com.google.android.gms.maps.SupportMapFragment
|
||||
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.gms.maps.model.MarkerOptions
|
||||
import com.google.gson.GsonBuilder
|
||||
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.MerchantsViewModel
|
||||
import cy.agorise.bitsybitshareswallet.models.Merchant
|
||||
import cy.agorise.bitsybitshareswallet.network.AmbassadorService
|
||||
import cy.agorise.bitsybitshareswallet.network.FeathersResponse
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.io.IOException
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.google.android.gms.maps.model.BitmapDescriptor
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
|
||||
class MerchantsFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun newInstance() = MerchantsFragment()
|
||||
}
|
||||
class MerchantsFragment : Fragment(), OnMapReadyCallback, retrofit2.Callback<FeathersResponse<Merchant>> {
|
||||
|
||||
private lateinit var viewModel: MerchantsViewModel
|
||||
private lateinit var mMap: GoogleMap
|
||||
|
||||
private var merchants: List<Merchant>? = null
|
||||
|
||||
private lateinit var merchantIcon: BitmapDescriptor
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
|
@ -25,10 +45,73 @@ class MerchantsFragment : Fragment() {
|
|||
return inflater.inflate(R.layout.fragment_merchants, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(this).get(MerchantsViewModel::class.java)
|
||||
// TODO: Use the ViewModel
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
|
||||
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
|
||||
mapFragment.getMapAsync(this)
|
||||
|
||||
merchantIcon = getMarkerIconFromDrawable(resources.getDrawable(R.drawable.ic_pin_merchants, null))
|
||||
|
||||
// TODO https://github.com/Agorise/bitsy-wallet/blob/feat_merchants/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/MapFragment.kt
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates the map once available.
|
||||
* This callback is triggered when the map is ready to be used.
|
||||
* This is where we can add markers or lines, add listeners or move the camera. In this case,
|
||||
* we just add a marker near Sydney, Australia.
|
||||
* If Google Play services is not installed on the device, the user will be prompted to install
|
||||
* it inside the SupportMapFragment. This method will only be triggered once the user has
|
||||
* installed Google Play services and returned to the app.
|
||||
*/
|
||||
override fun onMapReady(googleMap: GoogleMap) {
|
||||
mMap = googleMap
|
||||
|
||||
val gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(Constants.MERCHANTS_WEBSERVICE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
|
||||
val ambassadorService = retrofit.create<AmbassadorService>(AmbassadorService::class.java)
|
||||
val call = ambassadorService.allMerchants
|
||||
call.enqueue(this)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<FeathersResponse<Merchant>>, response: Response<FeathersResponse<Merchant>>) {
|
||||
if (response.isSuccessful) {
|
||||
val res: FeathersResponse<Merchant>? = response.body()
|
||||
merchants = res!!.data
|
||||
for (mer in merchants!!) {
|
||||
val location = LatLng(mer.lat.toDouble(), mer.lon.toDouble())
|
||||
mMap.addMarker(
|
||||
MarkerOptions().position(location).title(mer.name).snippet(mer.address).icon(merchantIcon)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Log.e("error_bitsy", response.errorBody()?.string())
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<FeathersResponse<Merchant>>, t: Throwable) {
|
||||
|
||||
}
|
||||
|
||||
private fun getMarkerIconFromDrawable(drawable: Drawable): BitmapDescriptor {
|
||||
val canvas = Canvas()
|
||||
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
canvas.setBitmap(bitmap)
|
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
drawable.draw(canvas)
|
||||
return BitmapDescriptorFactory.fromBitmap(bitmap)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
|
||||
class NetWorthFragment: Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
return inflater.inflate(R.layout.fragment_net_worth, container, false)
|
||||
}
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
if (isVisibleToUser) {
|
||||
// TODO find a better way to recreate the fragment, that does it only when the theme has been changed
|
||||
fragmentManager!!.beginTransaction().detach(this).attach(this).commit()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,444 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.common.primitives.UnsignedLong
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
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.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.utils.Helper
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.AssetViewModel
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel
|
||||
import cy.agorise.graphenej.*
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.api.calls.ListAssets
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.fragment_receive_transaction.*
|
||||
import java.lang.Exception
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class ReceiveTransactionFragment : Fragment(), ServiceConnection {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
private val RESPONSE_LIST_ASSETS = 1
|
||||
private val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
|
||||
|
||||
/** Number of assets to request from the NetworkService to show as suggestions in the AutoCompleteTextView */
|
||||
private val AUTO_SUGGEST_ASSET_LIMIT = 5
|
||||
|
||||
private val OTHER_ASSET = "other_asset"
|
||||
|
||||
private lateinit var mUserAccountViewModel: UserAccountViewModel
|
||||
private lateinit var mAssetViewModel: AssetViewModel
|
||||
|
||||
/** Current user account */
|
||||
private var mUserAccount: UserAccount? = null
|
||||
|
||||
private var mDisposables = CompositeDisposable()
|
||||
|
||||
private var mAsset: Asset? = null
|
||||
|
||||
private var mAssetsAdapter: AssetsAdapter? = null
|
||||
|
||||
private lateinit var mAutoSuggestAssetAdapter: AutoSuggestAssetAdapter
|
||||
|
||||
private var mAssets = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.Asset>()
|
||||
|
||||
private var selectedAssetSymbol = ""
|
||||
|
||||
/** Used to avoid erasing the QR code when the user selects an item from the AutoComplete suggestions */
|
||||
private var selectedInAutoCompleteTextView = false
|
||||
|
||||
// Map used to keep track of request and response id pairs
|
||||
private val responseMap = HashMap<Long, Int>()
|
||||
|
||||
/* Network service connection */
|
||||
private var mNetworkService: NetworkService? = null
|
||||
|
||||
/** Flag used to keep track of the NetworkService binding state */
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
return inflater.inflate(R.layout.fragment_receive_transaction, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Configure UserAccountViewModel to show the current account
|
||||
mUserAccountViewModel = ViewModelProviders.of(this).get(UserAccountViewModel::class.java)
|
||||
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
|
||||
|
||||
mUserAccountViewModel.getUserAccount(userId!!).observe(this,
|
||||
Observer<cy.agorise.bitsybitshareswallet.database.entities.UserAccount>{ user ->
|
||||
mUserAccount = UserAccount(user.id, user.name)
|
||||
})
|
||||
|
||||
// Configure Assets spinner to show Assets already saved into the db
|
||||
mAssetViewModel = ViewModelProviders.of(this).get(AssetViewModel::class.java)
|
||||
|
||||
mAssetViewModel.getAll().observe(this,
|
||||
Observer<List<cy.agorise.bitsybitshareswallet.database.entities.Asset>> { assets ->
|
||||
mAssets.clear()
|
||||
mAssets.addAll(assets)
|
||||
|
||||
// Add an option at the end so the user can search for an asset other than the ones saved in the db
|
||||
val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset(
|
||||
OTHER_ASSET, "Other...", 0, "", ""
|
||||
)
|
||||
mAssets.add(asset)
|
||||
|
||||
mAssetsAdapter = AssetsAdapter(context!!, android.R.layout.simple_spinner_item, mAssets)
|
||||
spAsset.adapter = mAssetsAdapter
|
||||
|
||||
// Try to select the selectedAssetSymbol
|
||||
for (i in 0 until mAssetsAdapter!!.count) {
|
||||
if (mAssetsAdapter!!.getItem(i)!!.symbol == selectedAssetSymbol) {
|
||||
spAsset.setSelection(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
spAsset.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) { }
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
val asset = mAssetsAdapter!!.getItem(position)!!
|
||||
|
||||
if (asset.id == OTHER_ASSET) {
|
||||
tilAsset.visibility = View.VISIBLE
|
||||
mAsset = null
|
||||
} else {
|
||||
tilAsset.visibility = View.GONE
|
||||
selectedAssetSymbol = asset.symbol
|
||||
|
||||
mAsset = Asset(asset.id, asset.symbol, asset.precision)
|
||||
}
|
||||
updateQR()
|
||||
}
|
||||
}
|
||||
|
||||
// Use RxJava Debounce to create QR code only after the user stopped typing an amount
|
||||
mDisposables.add(
|
||||
tietAmount.textChanges()
|
||||
.debounce(1000, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { updateQR() }
|
||||
)
|
||||
|
||||
// Add adapter to the Assets AutoCompleteTextView
|
||||
mAutoSuggestAssetAdapter = AutoSuggestAssetAdapter(context!!, android.R.layout.simple_dropdown_item_1line)
|
||||
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()
|
||||
.skipInitialValue()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.map { it.toString().trim().toUpperCase() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (!selectedInAutoCompleteTextView) {
|
||||
mAsset = null
|
||||
updateQR()
|
||||
}
|
||||
selectedInAutoCompleteTextView = false
|
||||
|
||||
// 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)
|
||||
responseMap[id] = RESPONSE_LIST_ASSETS
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
actvAsset.setOnItemClickListener { parent, _, position, _ ->
|
||||
val asset = parent.adapter.getItem(position) as cy.agorise.bitsybitshareswallet.database.entities.Asset
|
||||
mAsset = Asset(asset.id, asset.symbol, asset.precision)
|
||||
selectedInAutoCompleteTextView = true
|
||||
updateQR()
|
||||
}
|
||||
|
||||
// Connect to the RxBus, which receives events from the NetworkService
|
||||
mDisposables.add(
|
||||
RxBus.getBusInstance()
|
||||
.asFlowable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { handleIncomingMessage(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(message: Any?) {
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
if (responseMap.containsKey(message.id)) {
|
||||
val responseType = responseMap[message.id]
|
||||
when (responseType) {
|
||||
RESPONSE_LIST_ASSETS -> handleListAssets(message.result as List<Asset>)
|
||||
}
|
||||
responseMap.remove(message.id)
|
||||
}
|
||||
} else if (message is ConnectionStatusUpdate) {
|
||||
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
|
||||
// If we got a disconnection notification, we should clear our response map, since
|
||||
// all its stored request ids will now be reset
|
||||
responseMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleListAssets(assetList: List<Asset>) {
|
||||
Log.d(TAG, "handleListAssets")
|
||||
val assets = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.Asset>()
|
||||
for (_asset in assetList) {
|
||||
val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset(
|
||||
_asset.objectId,
|
||||
_asset.symbol,
|
||||
_asset.precision,
|
||||
_asset.description ?: "",
|
||||
_asset.bitassetId ?: ""
|
||||
)
|
||||
|
||||
assets.add(asset)
|
||||
}
|
||||
mAutoSuggestAssetAdapter.setData(assets)
|
||||
mAutoSuggestAssetAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updateQR() {
|
||||
if (mAsset == null) {
|
||||
ivQR.setImageDrawable(null)
|
||||
// TODO clean the please pay and to text at the bottom too
|
||||
return
|
||||
}
|
||||
|
||||
// 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()
|
||||
(tmpAmount * Math.pow(10.0, mAsset!!.precision.toDouble())).toLong()
|
||||
}catch (e: Exception) {
|
||||
0
|
||||
}
|
||||
|
||||
val total = AssetAmount(UnsignedLong.valueOf(amount), mAsset!!)
|
||||
val totalInDouble = Util.fromBase(total)
|
||||
val items = arrayOf(LineItem("transfer", 1, totalInDouble))
|
||||
val invoice = Invoice(mUserAccount!!.name, "", "#bitsy", mAsset!!.symbol, items, "", "")
|
||||
Log.d(TAG, "invoice: " + invoice.toJsonString())
|
||||
try {
|
||||
val bitmap = encodeAsBitmap(Invoice.toQrCode(invoice), "#139657") // PalmPay green
|
||||
ivQR.setImageBitmap(bitmap)
|
||||
updateAmountAddressUI(total, mUserAccount!!.name)
|
||||
} catch (e: WriterException) {
|
||||
Log.e(TAG, "WriterException. Msg: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the provided data as a QR-code. Used to provide payment requests.
|
||||
* @param data: Data containing payment request data as the recipient's address and the requested amount.
|
||||
* @param color: The color used for the QR-code
|
||||
* @return Bitmap with the QR-code encoded data
|
||||
* @throws WriterException if QR Code cannot be generated
|
||||
*/
|
||||
@Throws(WriterException::class)
|
||||
internal fun encodeAsBitmap(data: String, color: String): Bitmap? {
|
||||
val result: BitMatrix
|
||||
|
||||
// Get measured width and height of the ImageView where the QR code will be placed
|
||||
var w = ivQR.width
|
||||
var h = ivQR.height
|
||||
|
||||
// Gets minimum side length and sets both width and height to that value so the final
|
||||
// QR code has a squared shape
|
||||
val minSide = if (w < h) w else h
|
||||
h = minSide
|
||||
w = h
|
||||
|
||||
try {
|
||||
val hints = HashMap<EncodeHintType, Any>()
|
||||
hints[EncodeHintType.MARGIN] = 0
|
||||
result = MultiFormatWriter().encode(
|
||||
data,
|
||||
BarcodeFormat.QR_CODE, w, h, hints
|
||||
)
|
||||
} catch (iae: IllegalArgumentException) {
|
||||
// Unsupported format
|
||||
return null
|
||||
}
|
||||
|
||||
val pixels = IntArray(w * h)
|
||||
for (y in 0 until h) {
|
||||
val offset = y * w
|
||||
for (x in 0 until w) {
|
||||
pixels[offset + x] = if (result.get(x, y)) Color.parseColor(color) else Color.WHITE
|
||||
}
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
bitmap.setPixels(pixels, 0, w, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI to show amount and account to send the payment
|
||||
*
|
||||
* @param total Total Amount in crypto to be paid
|
||||
* @param account Account to pay total
|
||||
*/
|
||||
private fun updateAmountAddressUI(total: AssetAmount, account: String) {
|
||||
val df = DecimalFormat("####."+("#".repeat(total.asset.precision)))
|
||||
df.roundingMode = RoundingMode.CEILING
|
||||
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
|
||||
val amount = total.amount.toDouble() / Math.pow(10.toDouble(), total.asset.precision.toDouble())
|
||||
val strAmount = df.format(amount)
|
||||
|
||||
val txtAmount = getString(R.string.template__please_pay, strAmount, total.asset.symbol)
|
||||
val txtAccount = getString(R.string.template__to, account)
|
||||
|
||||
tvPleasePay.text = txtAmount
|
||||
tvTo.text = txtAccount
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_receive_transaction, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
if (item?.itemId == R.id.menu_share) {
|
||||
verifyStoragePermission()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun verifyStoragePermission() {
|
||||
if (ContextCompat.checkSelfPermission(activity!!, 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)
|
||||
} else {
|
||||
// Permission is already granted
|
||||
shareQRScreenshot()
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
shareQRScreenshot()
|
||||
} else {
|
||||
// TODO extract string resource
|
||||
Toast.makeText(context!!, "Storage permission is necessary to share QR codes.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes a screenshot as a bitmap, saves it into a temporal cache image and then
|
||||
* sends an intent so the user can select the desired method to share the image.
|
||||
*/
|
||||
private fun shareQRScreenshot() {
|
||||
// TODO improve, show errors where necessary so the user can fix it
|
||||
// Avoid sharing the QR code image if the fields are not filled correctly
|
||||
if (mAsset == null)
|
||||
return
|
||||
|
||||
// Get Screenshot
|
||||
val screenshot = Helper.loadBitmapFromView(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()
|
||||
|
||||
// Create share intent and call it
|
||||
val shareIntent = Intent()
|
||||
shareIntent.action = Intent.ACTION_SEND
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // temp permission for receiving app to read this file
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri)
|
||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, content)
|
||||
shareIntent.type = "*/*"
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.text__share_with)))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val intent = Intent(context, NetworkService::class.java)
|
||||
if (context?.bindService(intent, this, Context.BIND_AUTO_CREATE) == true) {
|
||||
mShouldUnbindNetwork = true
|
||||
} else {
|
||||
Log.e(TAG, "Binding to the network service failed.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
// Unbinding from network service
|
||||
if (mShouldUnbindNetwork) {
|
||||
context?.unbindService(this)
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) { }
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
val binder = service as NetworkService.LocalBinder
|
||||
mNetworkService = binder.service
|
||||
}
|
||||
}
|
|
@ -0,0 +1,476 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.google.common.primitives.UnsignedLong
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.Result
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
|
||||
import cy.agorise.graphenej.*
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.api.calls.BroadcastTransaction
|
||||
import cy.agorise.graphenej.api.calls.GetAccountByName
|
||||
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
|
||||
import cy.agorise.graphenej.api.calls.GetRequiredFees
|
||||
import cy.agorise.graphenej.models.AccountProperties
|
||||
import cy.agorise.graphenej.models.DynamicGlobalProperties
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import cy.agorise.graphenej.operations.TransferOperationBuilder
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.fragment_send_transaction.*
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
import org.bitcoinj.core.DumpedPrivateKey
|
||||
import org.bitcoinj.core.ECKey
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.crypto.AEADBadTagException
|
||||
|
||||
class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, ServiceConnection {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
// Camera Permission
|
||||
private val REQUEST_CAMERA_PERMISSION = 1
|
||||
|
||||
private val RESPONSE_GET_ACCOUNT_BY_NAME = 1
|
||||
private val RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS = 2
|
||||
private val RESPONSE_GET_REQUIRED_FEES = 3
|
||||
private val RESPONSE_BROADCAST_TRANSACTION = 4
|
||||
|
||||
private var isCameraPreviewVisible = false
|
||||
private var isToAccountCorrect = false
|
||||
private var isAmountCorrect = false
|
||||
|
||||
private var mBalancesDetails: List<BalanceDetail>? = null
|
||||
|
||||
private lateinit var mBalanceDetailViewModel: BalanceDetailViewModel
|
||||
|
||||
private var mBalancesDetailsAdapter: BalancesDetailsAdapter? = null
|
||||
|
||||
private var selectedAssetSymbol = ""
|
||||
|
||||
/** Current user account */
|
||||
private var mUserAccount: UserAccount? = null
|
||||
|
||||
/** User account to which send the funds */
|
||||
private var mSelectedUserAccount: UserAccount? = null
|
||||
|
||||
private var mDisposables = CompositeDisposable()
|
||||
|
||||
/* Network service connection */
|
||||
private var mNetworkService: NetworkService? = null
|
||||
|
||||
/** Flag used to keep track of the NetworkService binding state */
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
// Map used to keep track of request and response id pairs
|
||||
private val responseMap = HashMap<Long, Int>()
|
||||
|
||||
private var transaction: Transaction? = null
|
||||
|
||||
/** Variable holding the current user's private key in the WIF format */
|
||||
private var wifKey: String? = null
|
||||
|
||||
/** Repository to access and update Authorities */
|
||||
private var authorityRepository: AuthorityRepository? = null
|
||||
|
||||
/* This is one of the 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? {
|
||||
return inflater.inflate(R.layout.fragment_send_transaction, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
|
||||
|
||||
if (userId != "")
|
||||
mUserAccount = UserAccount(userId)
|
||||
|
||||
// Use Navigation SafeArgs to decide if we should activate or not the camera feed
|
||||
val safeArgs = SendTransactionFragmentArgs.fromBundle(arguments)
|
||||
if (safeArgs.openCamera)
|
||||
verifyCameraPermission()
|
||||
|
||||
fabOpenCamera.setOnClickListener { if (isCameraPreviewVisible) stopCameraPreview() else verifyCameraPermission() }
|
||||
|
||||
// Configure BalanceDetailViewModel to show the current balances
|
||||
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
|
||||
|
||||
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
|
||||
mBalancesDetails = balancesDetails
|
||||
mBalancesDetailsAdapter = BalancesDetailsAdapter(context!!, android.R.layout.simple_spinner_item, mBalancesDetails!!)
|
||||
spAsset.adapter = mBalancesDetailsAdapter
|
||||
|
||||
// Try to select the selectedAssetSymbol
|
||||
for (i in 0 until mBalancesDetailsAdapter!!.count) {
|
||||
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == selectedAssetSymbol) {
|
||||
spAsset.setSelection(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
spAsset.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) { }
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
val balance = mBalancesDetailsAdapter!!.getItem(position)!!
|
||||
selectedAssetSymbol = balance.symbol
|
||||
|
||||
val amount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
|
||||
|
||||
tvAvailableAssetAmount.text =
|
||||
String.format("%." + Math.min(balance.precision, 8) + "f %s", amount, balance.symbol)
|
||||
}
|
||||
}
|
||||
|
||||
fabSendTransaction.setOnClickListener { startSendTransferOperation() }
|
||||
fabSendTransaction.hide()
|
||||
|
||||
authorityRepository = AuthorityRepository(context!!)
|
||||
|
||||
mDisposables.add(
|
||||
authorityRepository!!.getWIF(userId!!, AuthorityType.ACTIVE.ordinal)
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { encryptedWIF ->
|
||||
try {
|
||||
wifKey = CryptoUtils.decrypt(context!!, encryptedWIF)
|
||||
} catch (e: AEADBadTagException) {
|
||||
Log.e(TAG, "AEADBadTagException. Class: " + e.javaClass + ", Msg: " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
// Use RxJava Debounce to avoid making calls to the NetworkService on every text change event
|
||||
mDisposables.add(
|
||||
tietTo.textChanges()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.map { it.toString().trim() }
|
||||
.filter { it.length > 1 }
|
||||
.subscribe {
|
||||
val id = mNetworkService!!.sendMessage(GetAccountByName(it!!), GetAccountByName.REQUIRED_API)
|
||||
responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME
|
||||
}
|
||||
)
|
||||
|
||||
// Use RxJava Debounce to update the Amount error only after the user stops writing for > 500 ms
|
||||
mDisposables.add(
|
||||
tietAmount.textChanges()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { it.toString().trim().toDouble() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { validateAmount(it!!) }
|
||||
)
|
||||
|
||||
// Connect to the RxBus, which receives events from the NetworkService
|
||||
mDisposables.add(
|
||||
RxBus.getBusInstance()
|
||||
.asFlowable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { handleIncomingMessage(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(message: Any?) {
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
if (responseMap.containsKey(message.id)) {
|
||||
val responseType = responseMap[message.id]
|
||||
when (responseType) {
|
||||
RESPONSE_GET_ACCOUNT_BY_NAME -> handleAccountName(message.result)
|
||||
RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS -> handleDynamicGlobalProperties(message.result)
|
||||
RESPONSE_GET_REQUIRED_FEES -> handleRequiredFees(message.result)
|
||||
RESPONSE_BROADCAST_TRANSACTION -> handleBroadcastTransaction(message)
|
||||
}
|
||||
responseMap.remove(message.id)
|
||||
}
|
||||
} else if (message is ConnectionStatusUpdate) {
|
||||
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
|
||||
// If we got a disconnection notification, we should clear our response map, since
|
||||
// all its stored request ids will now be reset
|
||||
responseMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAccountName(result: Any?) {
|
||||
if (result is AccountProperties) {
|
||||
mSelectedUserAccount = UserAccount(result.id, result.name)
|
||||
destinationPublicKey = result.active.keyAuths.keys.iterator().next()
|
||||
tilTo.isErrorEnabled = false
|
||||
isToAccountCorrect = true
|
||||
} else {
|
||||
mSelectedUserAccount = null
|
||||
destinationPublicKey = null
|
||||
tilTo.error = "Invalid account"
|
||||
isToAccountCorrect = false
|
||||
}
|
||||
|
||||
enableDisableSendFAB()
|
||||
}
|
||||
|
||||
private fun handleDynamicGlobalProperties(result: Any?) {
|
||||
if (result is DynamicGlobalProperties) {
|
||||
val expirationTime = (result.time.time / 1000) + Transaction.DEFAULT_EXPIRATION_TIME
|
||||
val headBlockId = result.head_block_id
|
||||
val headBlockNumber = result.head_block_number
|
||||
|
||||
transaction!!.blockData = BlockData(headBlockNumber, headBlockId, expirationTime)
|
||||
|
||||
val asset = Asset(mBalancesDetailsAdapter!!.getItem(spAsset.selectedItemPosition)!!.id)
|
||||
|
||||
val id = mNetworkService!!.sendMessage(GetRequiredFees(transaction!!, asset), GetRequiredFees.REQUIRED_API)
|
||||
responseMap[id] = RESPONSE_GET_REQUIRED_FEES
|
||||
} else {
|
||||
// TODO unableToSendTransactionError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRequiredFees(result: Any?) {
|
||||
if (result is List<*> && result[0] is AssetAmount) {
|
||||
Log.d(TAG, "GetRequiredFees: " + transaction.toString())
|
||||
transaction!!.setFees(result as List<AssetAmount>) // TODO find how to remove this warning
|
||||
|
||||
val id = mNetworkService!!.sendMessage(BroadcastTransaction(transaction), BroadcastTransaction.REQUIRED_API)
|
||||
responseMap[id] = RESPONSE_BROADCAST_TRANSACTION
|
||||
} else {
|
||||
// TODO unableToSendTransactionError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBroadcastTransaction(message: JsonRpcResponse<*>) {
|
||||
if (message.result == null && message.error == null) {
|
||||
// TODO extract string resources
|
||||
Toast.makeText(context!!, "Transaction sent!", Toast.LENGTH_SHORT).show()
|
||||
|
||||
// Remove information from the text fields and disable send button
|
||||
tietTo.setText("")
|
||||
tietAmount.setText("")
|
||||
tietMemo.setText("")
|
||||
isToAccountCorrect = false
|
||||
isAmountCorrect = false
|
||||
enableDisableSendFAB()
|
||||
} else {
|
||||
// TODO extract error messages to show a better explanation to the user
|
||||
Toast.makeText(context!!, message.error.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyCameraPermission() {
|
||||
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission is not already granted
|
||||
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)
|
||||
} else {
|
||||
// Permission is already granted
|
||||
startCameraPreview()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||
startCameraPreview()
|
||||
} else {
|
||||
// TODO extract string resource
|
||||
Toast.makeText(context!!, "Camera permission is necessary to read QR codes.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCameraPreview() {
|
||||
cameraPreview.visibility = View.VISIBLE
|
||||
fabOpenCamera.setImageResource(R.drawable.ic_close)
|
||||
isCameraPreviewVisible = true
|
||||
|
||||
// Configure QR scanner
|
||||
cameraPreview.setFormats(listOf(BarcodeFormat.QR_CODE))
|
||||
cameraPreview.setAspectTolerance(0.5f)
|
||||
cameraPreview.setAutoFocus(true)
|
||||
cameraPreview.setLaserColor(R.color.colorAccent)
|
||||
cameraPreview.setMaskColor(R.color.colorAccent)
|
||||
cameraPreview.setResultHandler(this)
|
||||
cameraPreview.startCamera()
|
||||
}
|
||||
|
||||
private fun stopCameraPreview() {
|
||||
cameraPreview.visibility = View.INVISIBLE
|
||||
fabOpenCamera.setImageResource(R.drawable.ic_camera)
|
||||
isCameraPreviewVisible = false
|
||||
cameraPreview.stopCamera()
|
||||
}
|
||||
|
||||
override fun handleResult(result: Result?) {
|
||||
try {
|
||||
val invoice = Invoice.fromQrCode(result!!.text)
|
||||
|
||||
Log.d(TAG, "QR Code read: " + invoice.toJsonString())
|
||||
|
||||
tietTo.setText(invoice.to)
|
||||
|
||||
for (i in 0 until mBalancesDetailsAdapter!!.count) {
|
||||
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == invoice.currency.toUpperCase()) {
|
||||
spAsset.setSelection(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
tietMemo.setText(invoice.memo)
|
||||
|
||||
|
||||
var amount = 0.0
|
||||
for (nextItem in invoice.lineItems) {
|
||||
amount += nextItem.quantity * nextItem.price
|
||||
}
|
||||
// TODO Improve pattern to account for different asset precisions
|
||||
val df = DecimalFormat("####.#####")
|
||||
df.roundingMode = RoundingMode.CEILING
|
||||
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
|
||||
tietAmount.setText(df.format(amount))
|
||||
|
||||
}catch (e: Exception) {
|
||||
Log.d(TAG, "Invoice error: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateAmount(amount: Double) {
|
||||
if (mBalancesDetailsAdapter?.isEmpty != false) return
|
||||
val balance = mBalancesDetailsAdapter?.getItem(spAsset.selectedItemPosition) ?: return
|
||||
val currentAmount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
|
||||
|
||||
if (currentAmount < amount) {
|
||||
// TODO extract string resource
|
||||
tilAmount.error = "Not enough funds"
|
||||
isAmountCorrect = false
|
||||
} else {
|
||||
tilAmount.isErrorEnabled = false
|
||||
isAmountCorrect = true
|
||||
}
|
||||
|
||||
enableDisableSendFAB()
|
||||
}
|
||||
|
||||
private fun enableDisableSendFAB() {
|
||||
if (isToAccountCorrect && isAmountCorrect)
|
||||
fabSendTransaction.show()
|
||||
else
|
||||
fabSendTransaction.hide()
|
||||
}
|
||||
|
||||
private fun startSendTransferOperation() {
|
||||
// Create TransferOperation
|
||||
if (mNetworkService!!.isConnected) {
|
||||
val balance = mBalancesDetailsAdapter!!.getItem(spAsset.selectedItemPosition)!!
|
||||
val amount = (tietAmount.text.toString().toDouble() * Math.pow(10.0, balance.precision.toDouble())).toLong()
|
||||
|
||||
val transferAmount = AssetAmount(UnsignedLong.valueOf(amount), Asset(balance.id))
|
||||
|
||||
val operationBuilder = TransferOperationBuilder()
|
||||
.setSource(mUserAccount)
|
||||
.setDestination(mSelectedUserAccount)
|
||||
.setTransferAmount(transferAmount)
|
||||
|
||||
val privateKey = ECKey.fromPrivate(DumpedPrivateKey.fromBase58(null, wifKey).key.privKeyBytes)
|
||||
|
||||
// Add memo if exists TODO enable memo
|
||||
// val memoMsg = tietMemo.text.toString()
|
||||
// if (memoMsg.isNotEmpty()) {
|
||||
// val nonce = SecureRandomGenerator.getSecureRandom().nextLong().toBigInteger()
|
||||
// val encryptedMemo = Memo.encryptMessage(privateKey, destinationPublicKey!!, nonce, memoMsg)
|
||||
// val from = Address(ECKey.fromPublicOnly(privateKey.pubKey))
|
||||
// val to = Address(destinationPublicKey!!.key)
|
||||
// val memo = Memo(from, to, nonce, encryptedMemo)
|
||||
// operationBuilder.setMemo(memo)
|
||||
// }
|
||||
|
||||
val operations = ArrayList<BaseOperation>()
|
||||
operations.add(operationBuilder.build())
|
||||
|
||||
transaction = Transaction(privateKey, null, operations)
|
||||
|
||||
val id = mNetworkService!!.sendMessage(GetDynamicGlobalProperties(),
|
||||
GetDynamicGlobalProperties.REQUIRED_API)
|
||||
responseMap[id] = RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS
|
||||
} else
|
||||
Log.d(TAG, "Network Service is not connected")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isCameraPreviewVisible)
|
||||
startCameraPreview()
|
||||
|
||||
val intent = Intent(context, NetworkService::class.java)
|
||||
if (context?.bindService(intent, this, Context.BIND_AUTO_CREATE) == true) {
|
||||
mShouldUnbindNetwork = true
|
||||
} else {
|
||||
Log.e(TAG, "Binding to the network service failed.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
// Unbinding from network service
|
||||
if (mShouldUnbindNetwork) {
|
||||
context?.unbindService(this)
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
|
||||
if (!isCameraPreviewVisible)
|
||||
stopCameraPreview()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) { }
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
val binder = service as NetworkService.LocalBinder
|
||||
mNetworkService = binder.service
|
||||
}
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import android.content.*
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.list.customListAdapter
|
||||
import cy.agorise.bitsybitshareswallet.BuildConfig
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.adapters.FullNodesAdapter
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
|
||||
import cy.agorise.graphenej.BrainKey
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
|
||||
import cy.agorise.graphenej.models.DynamicGlobalProperties
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import cy.agorise.graphenej.network.FullNode
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.fragment_settings.*
|
||||
import java.text.NumberFormat
|
||||
|
||||
class SettingsFragment : Fragment(), ServiceConnection {
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
private var mDisposables = CompositeDisposable()
|
||||
|
||||
/* Network service connection */
|
||||
private var mNetworkService: NetworkService? = null
|
||||
|
||||
/** Flag used to keep track of the NetworkService binding state */
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
// Dialog displaying the list of nodes and their latencies
|
||||
private var mNodesDialog: MaterialDialog? = null
|
||||
|
||||
/** Adapter that holds the FullNode list used in the Bitshares nodes modal */
|
||||
private var nodesAdapter: FullNodesAdapter? = null
|
||||
|
||||
private val mHandler = Handler()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
return inflater.inflate(R.layout.fragment_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initAutoCloseSwitch()
|
||||
|
||||
initNightModeSwitch()
|
||||
|
||||
btnViewBrainKey.setOnClickListener { getBrainkey(it) }
|
||||
|
||||
tvNetworkStatus.setOnClickListener { v ->
|
||||
if (mNetworkService != null) {
|
||||
// PublishSubject used to announce full node latencies updates
|
||||
val fullNodePublishSubject = mNetworkService!!.nodeLatencyObservable
|
||||
fullNodePublishSubject?.observeOn(AndroidSchedulers.mainThread())?.subscribe(nodeLatencyObserver)
|
||||
|
||||
val fullNodes = mNetworkService!!.nodes
|
||||
|
||||
nodesAdapter = FullNodesAdapter(v.context)
|
||||
nodesAdapter!!.add(fullNodes)
|
||||
|
||||
mNodesDialog = MaterialDialog(v.context)
|
||||
.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) {
|
||||
mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask)
|
||||
}
|
||||
|
||||
mNodesDialog?.show()
|
||||
|
||||
// Registering a recurrent task used to poll for dynamic global properties requests
|
||||
mHandler.post(mRequestDynamicGlobalPropertiesTask)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the RxBus, which receives events from the NetworkService
|
||||
mDisposables.add(
|
||||
RxBus.getBusInstance()
|
||||
.asFlowable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { handleIncomingMessage(it) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observer used to be notified about node latency measurement updates.
|
||||
*/
|
||||
private val nodeLatencyObserver = object : Observer<FullNode> {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
mDisposables.add(d)
|
||||
}
|
||||
|
||||
override fun onNext(fullNode: FullNode) {
|
||||
if (!fullNode.isRemoved)
|
||||
nodesAdapter?.add(fullNode)
|
||||
else
|
||||
nodesAdapter?.remove(fullNode)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Log.e(TAG, "nodeLatencyObserver.onError.Msg: " + e.message)
|
||||
}
|
||||
|
||||
override fun onComplete() {}
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(message: Any?) {
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
if (message.result is DynamicGlobalProperties) {
|
||||
val dynamicGlobalProperties = message.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to obtain frequent updates on the global dynamic properties object
|
||||
*/
|
||||
private val mRequestDynamicGlobalPropertiesTask = object : Runnable {
|
||||
override fun run() {
|
||||
if (mNetworkService != null) {
|
||||
if (mNetworkService?.isConnected == true) {
|
||||
mNetworkService?.sendMessage(GetDynamicGlobalProperties(), GetDynamicGlobalProperties.REQUIRED_API)
|
||||
} else {
|
||||
Log.d(TAG, "NetworkService exists but is not connected")
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "NetworkService reference is null")
|
||||
}
|
||||
mHandler.postDelayed(this, Constants.BLOCK_PERIOD)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the relevant preference from the SharedPreferences and configures the corresponding switch accordingly,
|
||||
* and adds a listener to the said switch to store the preference in case the user changes it.
|
||||
*/
|
||||
private fun initAutoCloseSwitch() {
|
||||
val autoCloseOn = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, true)
|
||||
|
||||
switchAutoClose.isChecked = autoCloseOn
|
||||
|
||||
switchAutoClose.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
|
||||
.putBoolean(Constants.KEY_AUTO_CLOSE_ACTIVATED, isChecked).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the relevant preference from the SharedPreferences and configures the corresponding switch accordingly,
|
||||
* and adds a listener to the said switch to store the preference in case the user changes it. Also makes a call to
|
||||
* recreate the activity and apply the selected theme.
|
||||
*/
|
||||
private fun initNightModeSwitch() {
|
||||
val nightModeOn = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
|
||||
|
||||
switchNightMode.isChecked = nightModeOn
|
||||
|
||||
switchNightMode.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(buttonView.context).edit()
|
||||
.putBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, isChecked).apply()
|
||||
|
||||
// Recreates the activity to apply the selected theme
|
||||
activity?.recreate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the brainKey from the authorities db table for the current user account and if it is not null it passes
|
||||
* the brainKey to a method to show it in a nice MaterialDialog
|
||||
*/
|
||||
private fun getBrainkey(view: View) {
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
|
||||
|
||||
val authorityRepository = AuthorityRepository(view.context)
|
||||
|
||||
mDisposables.add(authorityRepository.get(userId)
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { authority ->
|
||||
if (authority != null) {
|
||||
val plainBrainKey = CryptoUtils.decrypt(view.context, authority.encryptedBrainKey)
|
||||
val plainSequenceNumber = CryptoUtils.decrypt(view.context, authority.encryptedSequenceNumber)
|
||||
val sequenceNumber = Integer.parseInt(plainSequenceNumber)
|
||||
val brainKey = BrainKey(plainBrainKey, sequenceNumber)
|
||||
showBrainKeyDialog(view, brainKey)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the plain brainkey in a dialog so that the user can view and Copy it.
|
||||
*/
|
||||
private fun showBrainKeyDialog(view: View, brainKey: BrainKey) {
|
||||
MaterialDialog(view.context).show {
|
||||
title(text = "BrainKey")
|
||||
message(text = brainKey.brainKey)
|
||||
customView(R.layout.dialog_copy_brainkey)
|
||||
cancelable(false)
|
||||
positiveButton(android.R.string.copy) {
|
||||
Toast.makeText(it.context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
val clipboard = it.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("label", brainKey.brainKey)
|
||||
clipboard.primaryClip = clip
|
||||
it.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val intent = Intent(context, NetworkService::class.java)
|
||||
if (context?.bindService(intent, this, Context.BIND_AUTO_CREATE) == true) {
|
||||
mShouldUnbindNetwork = true
|
||||
} else {
|
||||
Log.e(TAG, "Binding to the network service failed.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
// Unbinding from network service
|
||||
if (mShouldUnbindNetwork) {
|
||||
context?.unbindService(this)
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
|
||||
resources.getDrawable(R.drawable.ic_disconnected, null), null)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
val binder = service as NetworkService.LocalBinder
|
||||
mNetworkService = binder.service
|
||||
|
||||
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
|
||||
resources.getDrawable(R.drawable.ic_connected, null), null)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,216 @@
|
|||
package cy.agorise.bitsybitshareswallet.fragments
|
||||
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import android.graphics.Point
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.jakewharton.rxbinding3.appcompat.queryTextChangeEvents
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.TransactionsViewModel
|
||||
import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
||||
import cy.agorise.bitsybitshareswallet.utils.BounceTouchListener
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.viewmodels.TransferDetailViewModel
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.android.synthetic.main.fragment_transactions.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class TransactionsFragment : Fragment() {
|
||||
class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSelectedListener {
|
||||
|
||||
companion object {
|
||||
fun newInstance() = TransactionsFragment()
|
||||
}
|
||||
private lateinit var mTransferDetailViewModel: TransferDetailViewModel
|
||||
|
||||
private lateinit var viewModel: TransactionsViewModel
|
||||
private lateinit var transfersDetailsAdapter: TransfersDetailsAdapter
|
||||
|
||||
private val transfersDetails = ArrayList<TransferDetail>()
|
||||
private val filteredTransfersDetails = ArrayList<TransferDetail>()
|
||||
|
||||
/** Variables used to filter the transaction items */
|
||||
private var filterQuery = ""
|
||||
private var filterTransactionsDirection = 0
|
||||
private var filterDateRangeAll = true
|
||||
private var filterStartDate = 0L
|
||||
private var filterEndDate = 0L
|
||||
private var filterCryptocurrencyAll = true
|
||||
private var filterCryptocurrency = "BTS"
|
||||
private var filterFiatAmountAll = true
|
||||
private var filterFromFiatAmount = 0L
|
||||
private var filterToFiatAmount = 500L
|
||||
|
||||
private var mDisposables = CompositeDisposable()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_transactions, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel = ViewModelProviders.of(this).get(TransactionsViewModel::class.java)
|
||||
// TODO: Use the ViewModel
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val userId = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
|
||||
|
||||
transfersDetailsAdapter = TransfersDetailsAdapter(context!!)
|
||||
rvTransactions.adapter = transfersDetailsAdapter
|
||||
rvTransactions.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
// Configure TransferDetailViewModel to fetch the transaction history
|
||||
mTransferDetailViewModel = ViewModelProviders.of(this).get(TransferDetailViewModel::class.java)
|
||||
|
||||
mTransferDetailViewModel.getAll(userId).observe(this, Observer<List<TransferDetail>> { transfersDetails ->
|
||||
this.transfersDetails.clear()
|
||||
this.transfersDetails.addAll(transfersDetails)
|
||||
applyFilterOptions(false)
|
||||
})
|
||||
|
||||
// Set custom touch listener to handle bounce/stretch effect
|
||||
val bounceTouchListener = BounceTouchListener(rvTransactions)
|
||||
rvTransactions.setOnTouchListener(bounceTouchListener)
|
||||
|
||||
// Initialize filter options
|
||||
val calendar = Calendar.getInstance()
|
||||
filterEndDate = calendar.timeInMillis / 1000
|
||||
calendar.add(Calendar.MONTH, -2)
|
||||
filterStartDate = calendar.timeInMillis / 1000
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_transactions, menu)
|
||||
|
||||
// Adds listener for the SearchView
|
||||
val searchItem = menu.findItem(R.id.menu_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
mDisposables.add(
|
||||
searchView.queryTextChangeEvents()
|
||||
.skipInitialValue()
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.map { it.queryText.toString().toLowerCase() }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
filterQuery = it
|
||||
applyFilterOptions()
|
||||
}
|
||||
)
|
||||
|
||||
// Adjust SearchView width to avoid pushing other menu items out of the screen
|
||||
searchView.maxWidth = getScreenWidth(activity) * 3 / 5
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
return when (item?.itemId) {
|
||||
R.id.menu_filter -> {
|
||||
val filterOptionsDialog = FilterOptionsDialog.newInstance(
|
||||
filterTransactionsDirection, filterDateRangeAll, filterStartDate * 1000,
|
||||
filterEndDate * 1000, filterCryptocurrencyAll, filterCryptocurrency,
|
||||
filterFiatAmountAll, filterFromFiatAmount, filterToFiatAmount
|
||||
)
|
||||
filterOptionsDialog.show(childFragmentManager, "filter-options-tag")
|
||||
true
|
||||
}
|
||||
R.id.menu_export -> {
|
||||
// TODO add export options
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen width in pixels for the given [FragmentActivity]
|
||||
*/
|
||||
private fun getScreenWidth(activity: FragmentActivity?): Int {
|
||||
if (activity == null)
|
||||
return 200
|
||||
|
||||
val size = Point()
|
||||
activity.windowManager.defaultDisplay.getSize(size)
|
||||
return size.x
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the TransferDetail list given the user selected filter options.
|
||||
* TODO move this to a background thread
|
||||
*/
|
||||
private fun applyFilterOptions(scrollToTop: Boolean = true) {
|
||||
// Clean the filtered list
|
||||
filteredTransfersDetails.clear()
|
||||
|
||||
for (transferDetail in transfersDetails) {
|
||||
// Filter by transfer direction
|
||||
if (transferDetail.direction) { // Transfer sent
|
||||
if (filterTransactionsDirection == 1)
|
||||
// Looking for received transfers only
|
||||
continue
|
||||
} else { // Transfer received
|
||||
if (filterTransactionsDirection == 2)
|
||||
// Looking for sent transactions only
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (!filterDateRangeAll && (transferDetail.date < filterStartDate || transferDetail.date > filterEndDate))
|
||||
continue
|
||||
|
||||
// Filter by cryptocurrency
|
||||
if (!filterCryptocurrencyAll && transferDetail.cryptoSymbol != filterCryptocurrency)
|
||||
continue
|
||||
|
||||
// // Filter by fiat amount
|
||||
// if (!filterFiatAmountAll && (transferDetail.fiatAmount < filterFromFiatAmount || transferDetail.fiatAmount > filterToFiatAmount))
|
||||
// continue
|
||||
|
||||
// Filter by search query
|
||||
val text = (transferDetail.from ?: "").toLowerCase() + (transferDetail.to ?: "").toLowerCase()
|
||||
if (text.contains(filterQuery, ignoreCase = true)) {
|
||||
filteredTransfersDetails.add(transferDetail)
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces the list of TransferDetail items with the new filtered list
|
||||
transfersDetailsAdapter.replaceAll(filteredTransfersDetails)
|
||||
|
||||
if (scrollToTop)
|
||||
rvTransactions.scrollToPosition(0)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
override fun onFilterOptionsSelected(
|
||||
filterTransactionsDirection: Int,
|
||||
filterDateRangeAll: Boolean,
|
||||
filterStartDate: Long,
|
||||
filterEndDate: Long,
|
||||
filterCryptocurrencyAll: Boolean,
|
||||
filterCryptocurrency: String,
|
||||
filterFiatAmountAll: Boolean,
|
||||
filterFromFiatAmount: Long,
|
||||
filterToFiatAmount: Long
|
||||
) {
|
||||
this.filterTransactionsDirection = filterTransactionsDirection
|
||||
this.filterDateRangeAll = filterDateRangeAll
|
||||
this.filterStartDate = filterStartDate / 1000
|
||||
this.filterEndDate = filterEndDate / 1000
|
||||
this.filterCryptocurrencyAll = filterCryptocurrencyAll
|
||||
this.filterCryptocurrency = filterCryptocurrency
|
||||
this.filterFiatAmountAll = filterFiatAmountAll
|
||||
this.filterFromFiatAmount = filterFromFiatAmount
|
||||
this.filterToFiatAmount = filterToFiatAmount
|
||||
applyFilterOptions(true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
class Ambassador(var id: String?, var account: String?, var country: String?, var city: String?) {
|
||||
var cityId: String? = null
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
class AmbassadorLocation(
|
||||
var id: String?,
|
||||
var name: String?,
|
||||
var country: String?
|
||||
) : Comparable<AmbassadorLocation> {
|
||||
|
||||
override fun toString(): String {
|
||||
return this.name!!
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is AmbassadorLocation && id == other.id
|
||||
}
|
||||
|
||||
override fun compareTo(@NonNull other: AmbassadorLocation): Int {
|
||||
return name!!.compareTo(other.name!!)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id?.hashCode() ?: 0
|
||||
result = 31 * result + (name?.hashCode() ?: 0)
|
||||
result = 31 * result + (country?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
import cy.agorise.graphenej.AssetAmount
|
||||
import cy.agorise.graphenej.models.OperationHistory
|
||||
import cy.agorise.graphenej.operations.TransferOperation
|
||||
|
||||
/**
|
||||
* This class is very similar to the OperationHistory, but while the later is used to deserialize
|
||||
* the transfer information exactly as it comes from the 'get_relative_account_history' API call,
|
||||
* this class is used to represent a single entry in the local database.
|
||||
*
|
||||
*
|
||||
* Every entry in the transfers table needs a bit more information than what is provided by the
|
||||
* HistoricalTransfer. We need to know the specific timestamp of a transaction for instance, instead
|
||||
* of just a block number.
|
||||
*
|
||||
*
|
||||
* There's also the data used for the equivalent fiat value.
|
||||
*
|
||||
*
|
||||
* Created by nelson on 12/18/16.
|
||||
*/
|
||||
class HistoricalOperationEntry {
|
||||
var historicalTransfer: OperationHistory? = null
|
||||
var timestamp: Long = 0
|
||||
var equivalentValue: AssetAmount? = null
|
||||
|
||||
override fun toString(): String {
|
||||
if (historicalTransfer != null) {
|
||||
// Since for now we know that all stored historical operations are 'transfers'
|
||||
val op = historicalTransfer!!.operation as TransferOperation
|
||||
var memo = "?"
|
||||
if (op.memo != null && op.memo.plaintextMessage != null) {
|
||||
memo = op.memo.plaintextMessage
|
||||
}
|
||||
return String.format(
|
||||
"<%d, %s -> %s, %d of %s, memo: %s>",
|
||||
timestamp,
|
||||
op.from.objectId,
|
||||
op.to.objectId,
|
||||
op.assetAmount.amount.toLong(),
|
||||
op.assetAmount.asset.objectId,
|
||||
memo
|
||||
)
|
||||
} else {
|
||||
return "<>"
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return historicalTransfer!!.objectId.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other == null) {
|
||||
return false
|
||||
}
|
||||
return if (javaClass != other.javaClass) {
|
||||
false
|
||||
} else {
|
||||
val otherEntry = other as HistoricalOperationEntry?
|
||||
otherEntry!!.historicalTransfer!!.objectId == this.historicalTransfer!!.objectId
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
import java.util.*
|
||||
|
||||
class Merchant {
|
||||
|
||||
var id: String? = null
|
||||
var address: String? = null
|
||||
var phone: String? = null
|
||||
var name: String? = null
|
||||
var lat: Float = 0F
|
||||
var lon: Float = 0F
|
||||
var city: String? = null
|
||||
var country: String? = null
|
||||
var createdAt: Date? = null
|
||||
var updatedAt: Date? = null
|
||||
var __v: Int = 0
|
||||
|
||||
constructor() {}
|
||||
constructor(id: String) {
|
||||
this.id = id
|
||||
}
|
||||
|
||||
constructor(_id: String, address: String, phone: String, name: String, lat: Float, lon: Float, city: String, country: String, createdAt: Date, updatedAt: Date, __v: Int) {
|
||||
this.id = _id
|
||||
this.address = address
|
||||
this.phone = phone
|
||||
this.name = name
|
||||
this.lat = lat
|
||||
this.lon = lon
|
||||
this.city = city
|
||||
this.country = country
|
||||
this.createdAt = createdAt
|
||||
this.updatedAt = updatedAt
|
||||
this.__v = __v
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package cy.agorise.bitsybitshareswallet.network
|
||||
|
||||
import cy.agorise.bitsybitshareswallet.models.Ambassador
|
||||
import cy.agorise.bitsybitshareswallet.models.AmbassadorLocation
|
||||
import cy.agorise.bitsybitshareswallet.models.Merchant
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface AmbassadorService {
|
||||
|
||||
//https://ambpay.palmpay.io/api/v1/merchants?%24sort%5Baccount%5D=1&%24limit=50&%24skip=0
|
||||
@get:GET("/api/v1/merchants?%24sort%5Baccount%5D=1&%24limit=50&%24skip=0")
|
||||
val allMerchants: Call<FeathersResponse<Merchant>>
|
||||
|
||||
@GET("/api/v1/ambassadors")
|
||||
fun getAmbassadors(@Query("cityId") cityId: String): Call<FeathersResponse<Ambassador>>
|
||||
|
||||
@GET("/api/v1/cities")
|
||||
fun getAllCitiesSync(@Query("\$skip") skip: Long): Call<FeathersResponse<AmbassadorLocation>>
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package cy.agorise.bitsybitshareswallet.network
|
||||
|
||||
class FeathersResponse<T>(private val error: Throwable?) {
|
||||
var total: Long = 0
|
||||
var limit: Long = 0
|
||||
var skip: Long = 0
|
||||
var data: List<T>? = null
|
||||
|
||||
val isSuccessful: Boolean
|
||||
get() = error == null && data != null
|
||||
|
||||
fun message(): String {
|
||||
return if (error != null) {
|
||||
error.message!!
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,330 @@
|
|||
package cy.agorise.bitsybitshareswallet.processors
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
|
||||
import cy.agorise.bitsybitshareswallet.models.HistoricalOperationEntry
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
|
||||
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
|
||||
import cy.agorise.graphenej.*
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory
|
||||
import cy.agorise.graphenej.errors.ChecksumException
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import cy.agorise.graphenej.models.OperationHistory
|
||||
import cy.agorise.graphenej.operations.TransferOperation
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.bitcoinj.core.DumpedPrivateKey
|
||||
import org.bitcoinj.core.ECKey
|
||||
import java.util.*
|
||||
import javax.crypto.AEADBadTagException
|
||||
|
||||
/**
|
||||
* This class is responsible for loading the local database with all past transfer operations of the
|
||||
* currently selected account.
|
||||
*
|
||||
* The procedure used to load the database in 3 steps:
|
||||
*
|
||||
* 1- Load all transfer operations
|
||||
* 2- Load all missing times
|
||||
* 3- Load all missing equivalent times
|
||||
*
|
||||
* Since the 'get_relative_account_history' will not provide either timestamps nor equivalent values
|
||||
* for every transfer, we must first load all historical transfer operations, and then proceed to
|
||||
* handle those missing columns.
|
||||
*/
|
||||
class TransfersLoader(private var mContext: Context?): ServiceConnection {
|
||||
|
||||
private val TAG = this.javaClass.simpleName
|
||||
|
||||
/** Constant that specifies if we are on debug mode */
|
||||
private val DEBUG = false
|
||||
|
||||
/* Constant used to fix the number of historical transfers to fetch from the network in one batch */
|
||||
private val HISTORICAL_TRANSFER_BATCH_SIZE = 100
|
||||
|
||||
private val RESPONSE_GET_RELATIVE_ACCOUNT_HISTORY = 0
|
||||
|
||||
private var mDisposables = CompositeDisposable()
|
||||
|
||||
/* Current user account */
|
||||
private var mCurrentAccount: UserAccount? = null
|
||||
|
||||
/** Variable holding the current user's private key in the WIF format */
|
||||
private var wifKey: String? = null
|
||||
|
||||
/** Repository to access and update Transfers */
|
||||
private var transferRepository: TransferRepository? = null
|
||||
|
||||
/** Repository to access and update Authorities */
|
||||
private var authorityRepository: AuthorityRepository? = null
|
||||
|
||||
/* Network service connection */
|
||||
private var mNetworkService: NetworkService? = null
|
||||
|
||||
/* Counter used to keep track of the transfer history batch count */
|
||||
private var historicalTransferCount = 0
|
||||
|
||||
// Used to keep track of the current state TODO this may not be needed
|
||||
private var mState = State.IDLE
|
||||
|
||||
/**
|
||||
* Flag used to keep track of the NetworkService binding state
|
||||
*/
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
private var lastId: Long = 0
|
||||
|
||||
// Map used to keep track of request and response id pairs
|
||||
private val responseMap = HashMap<Long, Int>()
|
||||
|
||||
/**
|
||||
* Enum class used to keep track of the current state of the loader
|
||||
*/
|
||||
private enum class State {
|
||||
IDLE,
|
||||
LOADING_MISSING_TIMES,
|
||||
LOADING_EQ_VALUES,
|
||||
CANCELLED,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
init {
|
||||
transferRepository = TransferRepository(mContext!!)
|
||||
authorityRepository = AuthorityRepository(mContext!!)
|
||||
|
||||
val pref = PreferenceManager.getDefaultSharedPreferences(mContext)
|
||||
val userId = pref.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
|
||||
if (userId != "") {
|
||||
mCurrentAccount = UserAccount(userId)
|
||||
mDisposables.add(authorityRepository!!.getWIF(userId!!, AuthorityType.MEMO.ordinal)
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { encryptedWIF ->
|
||||
try {
|
||||
wifKey = CryptoUtils.decrypt(mContext!!, encryptedWIF)
|
||||
} catch (e: AEADBadTagException) {
|
||||
Log.e(TAG, "AEADBadTagException. Class: " + e.javaClass + ", Msg: " + e.message)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
mDisposables.add(RxBus.getBusInstance()
|
||||
.asFlowable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(Consumer { message ->
|
||||
if (mState == State.FINISHED) return@Consumer
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
if (message.result is List<*>) {
|
||||
if (responseMap.containsKey(message.id)) {
|
||||
val responseType = responseMap[message.id]
|
||||
when (responseType) {
|
||||
RESPONSE_GET_RELATIVE_ACCOUNT_HISTORY -> handleOperationList(message.result as List<OperationHistory>)
|
||||
}
|
||||
responseMap.remove(message.id)
|
||||
}
|
||||
}
|
||||
} else if (message is ConnectionStatusUpdate) {
|
||||
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
|
||||
// If we got a disconnection notification, we should clear our response map, since
|
||||
// all its stored request ids will now be reset
|
||||
responseMap.clear()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// If there is no current user, we should not do anything
|
||||
mState = State.CANCELLED
|
||||
}
|
||||
|
||||
onStart()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// We've bound to LocalService, cast the IBinder and get LocalService instance
|
||||
val binder = service as NetworkService.LocalBinder
|
||||
mNetworkService = binder.service
|
||||
|
||||
// Start the transfers update
|
||||
startTransfersUpdateProcedure()
|
||||
}
|
||||
|
||||
private fun onStart() {
|
||||
if (mState != State.CANCELLED) {
|
||||
val intent = Intent(mContext, NetworkService::class.java)
|
||||
if (mContext!!.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
|
||||
mShouldUnbindNetwork = true
|
||||
} else {
|
||||
Log.e(TAG, "Binding to the network service failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the procedure that will try to update the 'transfers' table
|
||||
*/
|
||||
private fun startTransfersUpdateProcedure() {
|
||||
if (DEBUG) {
|
||||
// If we are in debug mode, we first erase all entries in the 'transfer' table
|
||||
transferRepository!!.deleteAll()
|
||||
}
|
||||
mDisposables.add(
|
||||
transferRepository!!.getCount()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { transferCount ->
|
||||
if (transferCount > 0) {
|
||||
// If we already have some transfers in the database, we might want to skip the request
|
||||
// straight to the last batch
|
||||
historicalTransferCount = Math.floor((transferCount /
|
||||
HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt()
|
||||
}
|
||||
// Retrieving account transactions
|
||||
loadNextOperationsBatch()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a freshly obtained list of OperationHistory instances. This is how the full node
|
||||
* answers our 'get_relative_account_history' API call.
|
||||
*
|
||||
* This response however, has to be processed before being stored in the local database.
|
||||
*
|
||||
* @param operationHistoryList List of OperationHistory instances
|
||||
*/
|
||||
private fun handleOperationList(operationHistoryList: List<OperationHistory>) {
|
||||
historicalTransferCount++
|
||||
|
||||
val insertedCount = transferRepository!!.insertAll(processOperationList(operationHistoryList))
|
||||
// TODO return number of inserted rows
|
||||
// Log.d(TAG, String.format("Inserted count: %d, list size: %d", insertedCount, operationHistoryList.size))
|
||||
if (/* insertedCount == 0 && */ operationHistoryList.isEmpty()) {
|
||||
onDestroy()
|
||||
} else {
|
||||
|
||||
// If we inserted more than one operation, we cannot yet be sure we've reached the
|
||||
// end of the operation list, so we issue another call to the 'get_relative_account_history'
|
||||
// API call
|
||||
loadNextOperationsBatch()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used to issue a new 'get_relative_account_history' API call. This is expected to retrieve
|
||||
* at most HISTORICAL_TRANSFER_BATCH_SIZE operations.
|
||||
*/
|
||||
private fun loadNextOperationsBatch() {
|
||||
val stop = historicalTransferCount * HISTORICAL_TRANSFER_BATCH_SIZE
|
||||
val start = stop + HISTORICAL_TRANSFER_BATCH_SIZE
|
||||
lastId = mNetworkService!!.sendMessage(
|
||||
GetRelativeAccountHistory(
|
||||
mCurrentAccount,
|
||||
stop,
|
||||
HISTORICAL_TRANSFER_BATCH_SIZE,
|
||||
start
|
||||
), GetRelativeAccountHistory.REQUIRED_API
|
||||
)
|
||||
responseMap[lastId] = RESPONSE_GET_RELATIVE_ACCOUNT_HISTORY
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that will transform a list of OperationHistory instances to a list of
|
||||
* HistoricalOperationEntry.
|
||||
*
|
||||
* The HistoricalOperationEntry class is basically a wrapper around the OperationHistory class
|
||||
* provided by the Graphenej library. It is used to better reflect what we store in the internal
|
||||
* database for every transfer and expands the OperationHistory class basically adding
|
||||
* two things:
|
||||
*
|
||||
* 1- A timestamp
|
||||
* 2- An AssetAmount instance to represent the equivalent value in a fiat value
|
||||
*
|
||||
* @param operations List of OperationHistory instances
|
||||
* @return List of HistoricalOperationEntry instances
|
||||
*/
|
||||
private fun processOperationList(operations: List<OperationHistory>): List<Transfer> {
|
||||
val transfers = ArrayList<Transfer>()
|
||||
|
||||
if (wifKey == null) {
|
||||
// In case of key storage corruption, we give up on processing this list of operations
|
||||
return transfers
|
||||
}
|
||||
val memoKey = DumpedPrivateKey.fromBase58(null, wifKey!!).key
|
||||
val publicKey = PublicKey(ECKey.fromPublicOnly(memoKey.pubKey))
|
||||
val myAddress = Address(publicKey.key)
|
||||
|
||||
|
||||
for (historicalOp in operations) {
|
||||
if (historicalOp.operation == null || historicalOp.operation !is TransferOperation) {
|
||||
// Some historical operations might not be transfer operations.
|
||||
// As of right now non-transfer operations get deserialized as null
|
||||
continue
|
||||
}
|
||||
|
||||
val entry = HistoricalOperationEntry()
|
||||
val op = historicalOp.operation as TransferOperation
|
||||
|
||||
val memo = op.memo
|
||||
if (memo.byteMessage != null) {
|
||||
val destinationAddress = memo.destination
|
||||
try {
|
||||
if (destinationAddress.toString() == myAddress.toString()) {
|
||||
val decryptedMessage = Memo.decryptMessage(memoKey, memo.source, memo.nonce, memo.byteMessage)
|
||||
memo.plaintextMessage = decryptedMessage
|
||||
}
|
||||
} catch (e: ChecksumException) {
|
||||
Log.e(TAG, "ChecksumException. Msg: " + e.message)
|
||||
} catch (e: NullPointerException) {
|
||||
// This is expected in case the decryption fails, so no need to log this event.
|
||||
Log.e(TAG, "NullPointerException. Msg: " + e.message)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception while decoding memo. Msg: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
val transfer = Transfer(
|
||||
historicalOp.objectId,
|
||||
historicalOp.blockNum,
|
||||
entry.timestamp,
|
||||
op.fee.amount.toLong(),
|
||||
op.fee.asset.objectId,
|
||||
op.from.objectId,
|
||||
op.to.objectId,
|
||||
op.assetAmount.amount.toLong(),
|
||||
op.assetAmount.asset.objectId,
|
||||
memo.plaintextMessage
|
||||
)
|
||||
|
||||
transfers.add(transfer)
|
||||
}
|
||||
return transfers
|
||||
}
|
||||
|
||||
private fun onDestroy() {
|
||||
Log.d(TAG, "Destroying TransfersLoader")
|
||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||
if (mShouldUnbindNetwork) {
|
||||
mContext!!.unbindService(this)
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
mContext = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.os.AsyncTask
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.daos.AssetDao
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Asset
|
||||
|
||||
class AssetRepository internal constructor(context: Context) {
|
||||
|
||||
private val mAssetDao: AssetDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mAssetDao = db!!.assetDao()
|
||||
}
|
||||
|
||||
fun getAll(): LiveData<List<Asset>> {
|
||||
return mAssetDao.getAll()
|
||||
}
|
||||
|
||||
fun insertAll(assets: List<Asset>) {
|
||||
insertAllAsyncTask(mAssetDao).execute(assets)
|
||||
}
|
||||
|
||||
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: AssetDao) :
|
||||
AsyncTask<List<Asset>, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg assets: List<Asset>): Void? {
|
||||
mAsyncTaskDao.insertAll(assets[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.os.AsyncTask
|
||||
import cy.agorise.bitsybitshareswallet.database.daos.AuthorityDao
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Authority
|
||||
import io.reactivex.Single
|
||||
|
||||
class AuthorityRepository internal constructor(context: Context) {
|
||||
|
||||
private val mAuthorityDao: AuthorityDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mAuthorityDao = db!!.authorityDao()
|
||||
}
|
||||
|
||||
fun insert(authority: Authority) {
|
||||
insertAsyncTask(mAuthorityDao).execute(authority)
|
||||
}
|
||||
|
||||
fun get(userId: String): Single<Authority> {
|
||||
return mAuthorityDao.get(userId)
|
||||
}
|
||||
|
||||
fun getWIF(userId: String, authorityType: Int): Single<String> {
|
||||
return mAuthorityDao.getWIF(userId, authorityType)
|
||||
}
|
||||
|
||||
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: AuthorityDao) :
|
||||
AsyncTask<Authority, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg authorities: Authority): Void? {
|
||||
mAsyncTaskDao.insert(authorities[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetailDao
|
||||
|
||||
class BalanceDetailRepository internal constructor(context: Context) {
|
||||
|
||||
private val mBalanceDetailDao: BalanceDetailDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mBalanceDetailDao = db!!.balanceDetailDao()
|
||||
}
|
||||
|
||||
fun getAll(): LiveData<List<BalanceDetail>> {
|
||||
return mBalanceDetailDao.getAll()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.os.AsyncTask
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.daos.BalanceDao
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Balance
|
||||
|
||||
class BalanceRepository internal constructor(context: Context) {
|
||||
|
||||
private val mBalanceDao: BalanceDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mBalanceDao = db!!.balanceDao()
|
||||
}
|
||||
|
||||
fun insertAll(balances: List<Balance>) {
|
||||
insertAllAsyncTask(mBalanceDao).execute(balances)
|
||||
}
|
||||
|
||||
fun getAll(): LiveData<List<Balance>> {
|
||||
return mBalanceDao.getAll()
|
||||
}
|
||||
|
||||
fun getMissingAssetIds(): LiveData<List<String>> {
|
||||
return mBalanceDao.getMissingAssetIds()
|
||||
}
|
||||
|
||||
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: BalanceDao) :
|
||||
AsyncTask<List<Balance>, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg transfers: List<Balance>): Void? {
|
||||
mAsyncTaskDao.insertAll(transfers[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetailDao
|
||||
|
||||
class TransferDetailRepository internal constructor(context: Context) {
|
||||
|
||||
private val mTransferDetailDao: TransferDetailDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mTransferDetailDao = db!!.transferDetailDao()
|
||||
}
|
||||
|
||||
fun getAll(userId: String): LiveData<List<TransferDetail>> {
|
||||
return mTransferDetailDao.getAll(userId)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.os.AsyncTask
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.daos.TransferDao
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Transfer
|
||||
import io.reactivex.Single
|
||||
|
||||
class TransferRepository internal constructor(context: Context) {
|
||||
|
||||
private val mTransferDao: TransferDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mTransferDao = db!!.transferDao()
|
||||
}
|
||||
|
||||
fun insertAll(transfers: List<Transfer>) {
|
||||
insertAllAsyncTask(mTransferDao).execute(transfers)
|
||||
}
|
||||
|
||||
fun setBlockTime(blockNumber: Long, timestamp: Long) {
|
||||
setBlockTimeAsyncTask(mTransferDao).execute(Pair(blockNumber, timestamp))
|
||||
}
|
||||
|
||||
fun getAll(): LiveData<List<Transfer>> {
|
||||
return mTransferDao.getAll()
|
||||
}
|
||||
|
||||
fun getCount(): Single<Int> {
|
||||
return mTransferDao.getCount()
|
||||
}
|
||||
|
||||
fun getTransferBlockNumberWithMissingTime(): LiveData<Long> {
|
||||
return mTransferDao.getTransferBlockNumberWithMissingTime()
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
deleteAllAsyncTask(mTransferDao).execute()
|
||||
}
|
||||
|
||||
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: TransferDao) :
|
||||
AsyncTask<List<Transfer>, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg transfers: List<Transfer>): Void? {
|
||||
mAsyncTaskDao.insertAll(transfers[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private class setBlockTimeAsyncTask internal constructor(private val mAsyncTaskDao: TransferDao) :
|
||||
AsyncTask<Pair<Long, Long>, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg pair: Pair<Long, Long>): Void? {
|
||||
mAsyncTaskDao.setBlockTime(pair[0].first, pair[0].second)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private class deleteAllAsyncTask internal constructor(private val mAsyncTaskDao: TransferDao) :
|
||||
AsyncTask<Void, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg params: Void?): Void? {
|
||||
mAsyncTaskDao.deleteAll()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.os.AsyncTask
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.database.daos.UserAccountDao
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
|
||||
|
||||
class UserAccountRepository internal constructor(context: Context) {
|
||||
|
||||
private val mUserAccountDao: UserAccountDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(context)
|
||||
mUserAccountDao = db!!.userAccountDao()
|
||||
}
|
||||
|
||||
fun insert(userAccount: UserAccount) {
|
||||
insertAsyncTask(mUserAccountDao).execute(userAccount)
|
||||
}
|
||||
|
||||
fun insertAll(userAccounts: List<UserAccount>) {
|
||||
insertAllAsyncTask(mUserAccountDao).execute(userAccounts)
|
||||
}
|
||||
|
||||
fun getUserAccount(id: String): LiveData<UserAccount> {
|
||||
return mUserAccountDao.getUserAccount(id)
|
||||
}
|
||||
|
||||
fun getMissingUserAccountIds(): LiveData<List<String>> {
|
||||
return mUserAccountDao.getMissingAccountIds()
|
||||
}
|
||||
|
||||
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
|
||||
AsyncTask<UserAccount, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg userAccounts: UserAccount): Void? {
|
||||
mAsyncTaskDao.insert(userAccounts[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
|
||||
AsyncTask<List<UserAccount>, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg userAccounts: List<UserAccount>): Void? {
|
||||
mAsyncTaskDao.insertAll(userAccounts[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
import android.app.Application
|
||||
import cy.agorise.graphenej.api.ApiAccess
|
||||
import cy.agorise.graphenej.api.android.NetworkServiceManager
|
||||
|
||||
class BitsyApplication : Application() {
|
||||
|
||||
private val BITSHARES_NODE_URLS = arrayOf(
|
||||
// PP private nodes
|
||||
"wss://nl.palmpay.io/ws",
|
||||
|
||||
// Other public nodes
|
||||
"wss://bitshares.nu/ws", // Stockholm, Sweden
|
||||
"wss://bitshares.openledger.info/ws", // Openledger node
|
||||
"wss://dallas.bitshares.apasia.tech/ws", // Dallas, USA
|
||||
"wss://atlanta.bitshares.apasia.tech/ws", // Atlanta, USA
|
||||
"wss://dex.rnglab.org", // Amsterdam, Netherlands
|
||||
"wss://citadel.li/node"
|
||||
)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Specifying some important information regarding the connection, such as the
|
||||
// credentials and the requested API accesses
|
||||
val requestedApis = ApiAccess.API_DATABASE or ApiAccess.API_HISTORY or ApiAccess.API_NETWORK_BROADCAST
|
||||
val networkManager = NetworkServiceManager.Builder()
|
||||
.setUserName("")
|
||||
.setPassword("")
|
||||
.setRequestedApis(requestedApis)
|
||||
.setCustomNodeUrls(setupNodes())
|
||||
.setAutoConnect(true)
|
||||
.setNodeLatencyVerification(true)
|
||||
.build(this)
|
||||
|
||||
/*
|
||||
* Registering this class as a listener to all activity's callback cycle events, in order to
|
||||
* better estimate when the user has left the app and it is safe to disconnect the websocket connection
|
||||
*/
|
||||
registerActivityLifecycleCallbacks(networkManager)
|
||||
}
|
||||
|
||||
private fun setupNodes(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
for (url in BITSHARES_NODE_URLS) {
|
||||
stringBuilder.append(url).append(",")
|
||||
}
|
||||
stringBuilder.replace(stringBuilder.length - 1, stringBuilder.length, "")
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.view.MotionEventCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
|
||||
class BounceTouchListener(private val mRecyclerView: RecyclerView) : View.OnTouchListener {
|
||||
|
||||
private var downCalled = false
|
||||
private var mDownY: Float = 0.toFloat()
|
||||
private var mSwipingDown: Boolean = false
|
||||
private var mSwipingUp: Boolean = false
|
||||
private val mInterpolator = DecelerateInterpolator(3f)
|
||||
private var mActivePointerId = -99
|
||||
private var mLastTouchY = -99f
|
||||
private var mMaxAbsTranslation = -99
|
||||
|
||||
init {
|
||||
mRecyclerView.pivotY = 0F
|
||||
}
|
||||
|
||||
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
|
||||
val action = MotionEventCompat.getActionMasked(motionEvent)
|
||||
|
||||
when (action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
onDownMotionEvent(motionEvent)
|
||||
view.onTouchEvent(motionEvent)
|
||||
downCalled = true
|
||||
if (this.mRecyclerView.translationY == 0f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (mActivePointerId == -99) {
|
||||
onDownMotionEvent(motionEvent)
|
||||
downCalled = true
|
||||
}
|
||||
val pointerIndex = MotionEventCompat.findPointerIndex(motionEvent, mActivePointerId)
|
||||
val y = MotionEventCompat.getY(motionEvent, pointerIndex)
|
||||
if (!hasHitTop() && !hasHitBottom() || !downCalled) {
|
||||
if (!downCalled) {
|
||||
downCalled = true
|
||||
}
|
||||
mDownY = y
|
||||
view.onTouchEvent(motionEvent)
|
||||
return false
|
||||
}
|
||||
val deltaY = y - mDownY
|
||||
if (Math.abs(deltaY) > 0 && hasHitTop() && deltaY > 0) {
|
||||
mSwipingDown = true
|
||||
sendCancelEventToView(view, motionEvent)
|
||||
}
|
||||
if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) {
|
||||
mSwipingUp = true
|
||||
sendCancelEventToView(view, motionEvent)
|
||||
}
|
||||
if (mSwipingDown || mSwipingUp) {
|
||||
if (deltaY <= 0 && mSwipingDown || deltaY >= 0 && mSwipingUp) {
|
||||
mDownY = 0f
|
||||
mSwipingDown = false
|
||||
mSwipingUp = false
|
||||
downCalled = false
|
||||
val downEvent = MotionEvent.obtain(motionEvent)
|
||||
downEvent.action = MotionEvent.ACTION_DOWN or
|
||||
(MotionEventCompat.getActionIndex(motionEvent) shl MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)
|
||||
view.onTouchEvent(downEvent)
|
||||
}
|
||||
var translation = (deltaY / Math.abs(deltaY) * Math.pow(Math.abs(deltaY).toDouble(), .8)).toInt()
|
||||
if (mMaxAbsTranslation > 0) {
|
||||
if (translation < 0) {
|
||||
translation = Math.max(-mMaxAbsTranslation, translation)
|
||||
} else {
|
||||
translation = Math.min(mMaxAbsTranslation, translation)
|
||||
}
|
||||
}
|
||||
mRecyclerView.translationY = translation.toFloat()
|
||||
translate(mRecyclerView.translationY)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
mActivePointerId = -99
|
||||
// cancel
|
||||
mRecyclerView.animate()
|
||||
.setInterpolator(mInterpolator)
|
||||
.translationY(0f)
|
||||
.setDuration(600L)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
(animation as ValueAnimator).addUpdateListener {
|
||||
translate(mRecyclerView.translationY)
|
||||
}
|
||||
super.onAnimationStart(animation)
|
||||
}
|
||||
})
|
||||
|
||||
mDownY = 0f
|
||||
mSwipingDown = false
|
||||
mSwipingUp = false
|
||||
downCalled = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
mActivePointerId = -99
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
val pointerIndex = MotionEventCompat.getActionIndex(motionEvent)
|
||||
val pointerId = MotionEventCompat.getPointerId(motionEvent, pointerIndex)
|
||||
|
||||
if (pointerId == mActivePointerId) {
|
||||
val newPointerIndex = if (pointerIndex == 0) 1 else 0
|
||||
mLastTouchY = MotionEventCompat.getY(motionEvent, newPointerIndex)
|
||||
mActivePointerId = MotionEventCompat.getPointerId(motionEvent, newPointerIndex)
|
||||
|
||||
if (this.mRecyclerView.translationY > 0) {
|
||||
mDownY = mLastTouchY - Math.pow(this.mRecyclerView.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
|
||||
this.mRecyclerView.animate().cancel()
|
||||
} else if (this.mRecyclerView.translationY < 0) {
|
||||
mDownY = mLastTouchY +
|
||||
Math.pow((-this.mRecyclerView.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
|
||||
this.mRecyclerView.animate().cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun translate(translation: Float) {
|
||||
val scale = 2 * translation / mRecyclerView.measuredHeight + 1
|
||||
mRecyclerView.scaleY = Math.pow(scale.toDouble(), .6).toFloat()
|
||||
}
|
||||
|
||||
private fun sendCancelEventToView(view: View, motionEvent: MotionEvent) {
|
||||
(view as ViewGroup).requestDisallowInterceptTouchEvent(true)
|
||||
val cancelEvent = MotionEvent.obtain(motionEvent)
|
||||
cancelEvent.action = MotionEvent.ACTION_CANCEL or
|
||||
(MotionEventCompat.getActionIndex(motionEvent) shl MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)
|
||||
view.onTouchEvent(cancelEvent)
|
||||
}
|
||||
|
||||
private fun onDownMotionEvent(motionEvent: MotionEvent) {
|
||||
val pointerIndex = MotionEventCompat.getActionIndex(motionEvent)
|
||||
mLastTouchY = MotionEventCompat.getY(motionEvent, pointerIndex)
|
||||
mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0)
|
||||
|
||||
if (this.mRecyclerView.translationY > 0) {
|
||||
mDownY = mLastTouchY - Math.pow(this.mRecyclerView.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
|
||||
this.mRecyclerView.animate().cancel()
|
||||
} else if (this.mRecyclerView.translationY < 0) {
|
||||
mDownY = mLastTouchY + Math.pow((-this.mRecyclerView.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
|
||||
this.mRecyclerView.animate().cancel()
|
||||
} else {
|
||||
mDownY = mLastTouchY
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasHitBottom(): Boolean {
|
||||
val recyclerView = this.mRecyclerView
|
||||
if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
|
||||
val adapter = recyclerView.adapter
|
||||
if (adapter!!.itemCount > 0) {
|
||||
val layoutManager = recyclerView.layoutManager
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
val linearLayoutManager = layoutManager as LinearLayoutManager?
|
||||
return linearLayoutManager!!.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1
|
||||
} else if (layoutManager is StaggeredGridLayoutManager) {
|
||||
val staggeredGridLayoutManager = layoutManager as StaggeredGridLayoutManager?
|
||||
val checks = staggeredGridLayoutManager!!.findLastCompletelyVisibleItemPositions(null)
|
||||
for (check in checks) {
|
||||
if (check == adapter.itemCount - 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasHitTop(): Boolean {
|
||||
val recyclerView = this.mRecyclerView
|
||||
if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
|
||||
val adapter = recyclerView.adapter
|
||||
if (adapter!!.itemCount > 0) {
|
||||
val layoutManager = recyclerView.layoutManager
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
val linearLayoutManager = layoutManager as LinearLayoutManager?
|
||||
return linearLayoutManager!!.findFirstCompletelyVisibleItemPosition() == 0
|
||||
} else if (layoutManager is StaggeredGridLayoutManager) {
|
||||
val staggeredGridLayoutManager = layoutManager as StaggeredGridLayoutManager?
|
||||
val checks = staggeredGridLayoutManager!!.findFirstCompletelyVisibleItemPositions(null)
|
||||
for (check in checks) {
|
||||
if (check == 0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,10 +1,51 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
|
||||
object Constants {
|
||||
|
||||
/** Key used to store the number of the last agreed License version */
|
||||
const val KEY_LAST_AGREED_LICENSE_VERSION = "key_last_agreed_license_version"
|
||||
|
||||
/** Version of the currently used license */
|
||||
const val CURRENT_LICENSE_VERSION = 1
|
||||
|
||||
/** Key used to store if the initial setup is already done or not */
|
||||
const val KEY_INITIAL_SETUP_DONE = "key_initial_setup_done"
|
||||
|
||||
/** Key used to store the id value of the currently active account in the shared preferences */
|
||||
const val KEY_CURRENT_ACCOUNT_ID = "key_current_account_id"
|
||||
|
||||
/** The minimum required length for a PIN number */
|
||||
const val MIN_PIN_LENGTH = 6
|
||||
|
||||
/** The user selected encrypted PIN */
|
||||
const val KEY_ENCRYPTED_PIN = "key_encrypted_pin"
|
||||
|
||||
/**
|
||||
* Key used to store the night mode setting into the shared preferences
|
||||
* LTM accounts come with an expiration date expressed as this string.
|
||||
* This is used to recognize such accounts from regular ones.
|
||||
*/
|
||||
val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated"
|
||||
const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59"
|
||||
|
||||
/**
|
||||
* Time period between two consecutive requests to the full node performed whenever we have
|
||||
* open payment requests as a matter of redundancy.
|
||||
*/
|
||||
const val MISSING_PAYMENT_CHECK_PERIOD: Long = 5000
|
||||
|
||||
/** Time period to wait to send a request to the NetworkService, and retry in case it is still not connected */
|
||||
const val NETWORK_SERVICE_RETRY_PERIOD: Long = 5000
|
||||
|
||||
/** Bitshares block period */
|
||||
const val BLOCK_PERIOD: Long = 3000
|
||||
|
||||
/** Key used to store the number of operations that the currently selected account had last time we checked */
|
||||
const val KEY_ACCOUNT_OPERATION_COUNT = "key_account_operation_count"
|
||||
|
||||
/** Key used to store the auto close app if no user activity setting into the shared preferences */
|
||||
const val KEY_AUTO_CLOSE_ACTIVATED = "key_auto_close_activated"
|
||||
|
||||
/** Key used to store the night mode setting into the shared preferences */
|
||||
const val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated"
|
||||
|
||||
const val MERCHANTS_WEBSERVICE_URL = "https://websvc.palmpay.io/"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
|
||||
import com.moldedbits.r2d2.R2d2
|
||||
|
||||
import javax.crypto.AEADBadTagException
|
||||
|
||||
/**
|
||||
* Class that provides encryption/decryption support by using the key management framework provided
|
||||
* by the KeyStore system.
|
||||
*
|
||||
* The implemented scheme was taken from [this](https://medium.com/@ericfu/securely-storing-secrets-in-an-android-application-501f030ae5a3)> blog post.
|
||||
*
|
||||
* @see [Android Keystore System](https://developer.android.com/training/articles/keystore.html)
|
||||
*/
|
||||
|
||||
object CryptoUtils {
|
||||
|
||||
/**
|
||||
* Encrypts and stores a key-value pair in the shared preferences
|
||||
* @param context The application context
|
||||
* @param key The key to be used to reference the data
|
||||
* @param value The actual value to be stored
|
||||
*/
|
||||
fun put(context: Context, key: String, value: String) {
|
||||
val r2d2 = R2d2(context)
|
||||
val encrypted = r2d2.encryptData(value)
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(key, encrypted)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and decrypts an encrypted value from the shared preferences
|
||||
* @param context The application context
|
||||
* @param key The key used to reference the data
|
||||
* @return The plaintext version of the encrypted data
|
||||
*/
|
||||
operator fun get(context: Context, key: String): String {
|
||||
val r2d2 = R2d2(context)
|
||||
val encrypted = PreferenceManager.getDefaultSharedPreferences(context).getString(key, null)
|
||||
return r2d2.decryptData(encrypted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts some data
|
||||
* @param context The application context
|
||||
* @param plaintext The plaintext version of the data
|
||||
* @return Encrypted data
|
||||
*/
|
||||
fun encrypt(context: Context, plaintext: String): String {
|
||||
val r2d2 = R2d2(context)
|
||||
return r2d2.encryptData(plaintext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts some data
|
||||
* @param context The application context
|
||||
* @param ciphertext The ciphertext version of the data
|
||||
* @return Decrypted data
|
||||
*/
|
||||
@Throws(AEADBadTagException::class)
|
||||
fun decrypt(context: Context, ciphertext: String): String {
|
||||
val r2d2 = R2d2(context)
|
||||
return r2d2.decryptData(ciphertext)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Contains methods that are helpful in different parts of the app
|
||||
*/
|
||||
class Helper {
|
||||
|
||||
companion object {
|
||||
private val TAG = "Helper"
|
||||
|
||||
/**
|
||||
* Creates and returns a Bitmap from the contents of a View, does not matter
|
||||
* if it is a simple view or a ViewGroup like a ConstraintLayout or a LinearLayout.
|
||||
*
|
||||
* @param view The view that is gonna be pictured.
|
||||
* @return The generated image from the given view.
|
||||
*/
|
||||
fun loadBitmapFromView(view: View): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
view.draw(canvas)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun saveTemporalBitmap(context: Context, bitmap: Bitmap): Uri {
|
||||
// save bitmap to cache directory
|
||||
try {
|
||||
val cachePath = File(context.cacheDir, "images")
|
||||
if (!cachePath.mkdirs())
|
||||
// don't forget to make the directory
|
||||
Log.d(TAG, "shareBitmapImage creating cache images folder")
|
||||
|
||||
val stream = FileOutputStream(cachePath.toString() + "/image.png") // overwrites this image every time
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
stream.close()
|
||||
} catch (e: IOException) {
|
||||
Log.d(TAG, "shareBitmapImage error: " + e.message)
|
||||
}
|
||||
|
||||
// Send intent to share image+text
|
||||
val imagePath = File(context.cacheDir, "images")
|
||||
val newFile = File(imagePath, "image.png")
|
||||
|
||||
// Create and return image uri
|
||||
return FileProvider.getUriForFile(context, "cy.agorise.bitsybitshareswallet.FileProvider", newFile)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
|
||||
/**
|
||||
* Created by xd on 1/24/18.
|
||||
* ImageView which adjusts its size to always create a square
|
||||
*/
|
||||
|
||||
class SquaredImageView : AppCompatImageView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, @Nullable attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, @Nullable attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val size = Math.min(measuredWidth, measuredHeight)
|
||||
setMeasuredDimension(size, size)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Asset
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
|
||||
|
||||
class AssetViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var mRepository = AssetRepository(application)
|
||||
|
||||
internal fun getAll(): LiveData<List<Asset>> {
|
||||
return mRepository.getAll()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||
import cy.agorise.bitsybitshareswallet.repositories.BalanceDetailRepository
|
||||
|
||||
|
||||
class BalanceDetailViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var mRepository = BalanceDetailRepository(application)
|
||||
|
||||
internal fun getAll(): LiveData<List<BalanceDetail>> {
|
||||
return mRepository.getAll()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.Balance
|
||||
import cy.agorise.bitsybitshareswallet.repositories.BalanceRepository
|
||||
|
||||
class BalanceViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var mRepository = BalanceRepository(application)
|
||||
|
||||
internal fun getMissingAssetIds(): LiveData<List<String>> {
|
||||
return mRepository.getMissingAssetIds()
|
||||
}
|
||||
|
||||
fun insertAll(balances: List<Balance>) {
|
||||
mRepository.insertAll(balances)
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class BalancesViewModel : ViewModel() {
|
||||
// TODO: Implement the ViewModel
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
class MerchantsViewModel : ViewModel() {
|
||||
// TODO: Implement the ViewModel
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
class TransactionsViewModel : ViewModel() {
|
||||
// TODO: Implement the ViewModel
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
||||
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
|
||||
|
||||
class TransferDetailViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var mRepository = TransferDetailRepository(application)
|
||||
|
||||
internal fun getAll(userId: String): LiveData<List<TransferDetail>> {
|
||||
return mRepository.getAll(userId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
|
||||
|
||||
class TransferViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var mRepository = TransferRepository(application)
|
||||
|
||||
internal fun setBlockTime(blockNumber: Long, timestamp: Long) {
|
||||
mRepository.setBlockTime(blockNumber, timestamp)
|
||||
}
|
||||
|
||||
internal fun getTransferBlockNumberWithMissingTime(): LiveData<Long> {
|
||||
return mRepository.getTransferBlockNumberWithMissingTime()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package cy.agorise.bitsybitshareswallet.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import cy.agorise.bitsybitshareswallet.database.entities.UserAccount
|
||||
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
|
||||
|
||||
class UserAccountViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var mRepository = UserAccountRepository(application)
|
||||
|
||||
internal fun getUserAccount(id: String): LiveData<UserAccount> {
|
||||
return mRepository.getUserAccount(id)
|
||||
}
|
||||
|
||||
internal fun getMissingUserAccountIds(): LiveData<List<String>> {
|
||||
return mRepository.getMissingUserAccountIds()
|
||||
}
|
||||
|
||||
// fun insert(userAccount: UserAccount) {
|
||||
// mRepository.insert(userAccount)
|
||||
// }
|
||||
|
||||
fun insertAll(userAccounts: List<UserAccount>) {
|
||||
mRepository.insertAll(userAccounts)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package cy.agorise.bitsybitshareswallet.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
|
||||
|
||||
/**
|
||||
* Custom AutoCompleteTextView to be used inside a TextInputLayout, so that they can share their hint
|
||||
* From https://stackoverflow.com/a/41864063/5428997
|
||||
*/
|
||||
class MyTextInputAutoCompleteTextView : AppCompatAutoCompleteTextView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
||||
val ic = super.onCreateInputConnection(outAttrs)
|
||||
if (ic != null && outAttrs.hintText == null) {
|
||||
// If we don't have a hint and our parent is a TextInputLayout, use it's hint for the
|
||||
// EditorInfo. This allows us to display a hint in 'extract mode'.
|
||||
val parent = parent
|
||||
if (parent is TextInputLayout) {
|
||||
outAttrs.hintText = parent.hint
|
||||
}
|
||||
}
|
||||
// An EditText that lets you use actions ("Done", "Go", etc.) on multi-line edits.
|
||||
val imeActions = outAttrs.imeOptions and EditorInfo.IME_MASK_ACTION
|
||||
if (imeActions and EditorInfo.IME_ACTION_DONE != 0) {
|
||||
// clear the existing action
|
||||
outAttrs.imeOptions = outAttrs.imeOptions xor imeActions
|
||||
// set the DONE action
|
||||
outAttrs.imeOptions = outAttrs.imeOptions or EditorInfo.IME_ACTION_DONE
|
||||
}
|
||||
if (outAttrs.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION != 0) {
|
||||
outAttrs.imeOptions = outAttrs.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION.inv()
|
||||
}
|
||||
return ic
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package cy.agorise.bitsybitshareswallet.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
// An EditText that lets you use actions ("Done", "Go", etc.) on multi-line edits.
|
||||
public class MyTextInputEditText extends TextInputEditText
|
||||
{
|
||||
public MyTextInputEditText(Context context)
|
||||
{
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MyTextInputEditText(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MyTextInputEditText(Context context, AttributeSet attrs, int defStyle)
|
||||
{
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
InputConnection connection = super.onCreateInputConnection(outAttrs);
|
||||
int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION;
|
||||
if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) {
|
||||
// clear the existing action
|
||||
outAttrs.imeOptions ^= imeActions;
|
||||
// set the DONE action
|
||||
outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
|
||||
}
|
||||
if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
|
||||
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
}
|
15
app/src/main/res/anim/item_animation_from_bottom.xml
Normal file
15
app/src/main/res/anim/item_animation_from_bottom.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="700">
|
||||
|
||||
<translate
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:fromYDelta="50%p"
|
||||
android:toYDelta="0"/>
|
||||
|
||||
<alpha
|
||||
android:fromAlpha="0"
|
||||
android:toAlpha="1"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
|
||||
|
||||
</set>
|
6
app/src/main/res/anim/layout_animation_from_bottom.xml
Normal file
6
app/src/main/res/anim/layout_animation_from_bottom.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layoutAnimation
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:animation="@anim/item_animation_from_bottom"
|
||||
android:delay="15%"
|
||||
android:animationOrder="normal"/>
|
7
app/src/main/res/anim/slide_in_left.xml
Normal file
7
app/src/main/res/anim/slide_in_left.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<translate android:fromXDelta="-100%" android:toXDelta="0%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="700"/>
|
||||
</set>
|
7
app/src/main/res/anim/slide_in_right.xml
Normal file
7
app/src/main/res/anim/slide_in_right.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<translate android:fromXDelta="100%" android:toXDelta="0%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="700"/>
|
||||
</set>
|
7
app/src/main/res/anim/slide_out_left.xml
Normal file
7
app/src/main/res/anim/slide_out_left.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<translate android:fromXDelta="0%" android:toXDelta="-100%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="700"/>
|
||||
</set>
|
7
app/src/main/res/anim/slide_out_right.xml
Normal file
7
app/src/main/res/anim/slide_out_right.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<translate android:fromXDelta="0%" android:toXDelta="100%"
|
||||
android:fromYDelta="0%" android:toYDelta="0%"
|
||||
android:duration="700"/>
|
||||
</set>
|
93
app/src/main/res/animator/button_state_list_anim.xml
Normal file
93
app/src/main/res/animator/button_state_list_anim.xml
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Pressed state -->
|
||||
<item
|
||||
android:state_enabled="true"
|
||||
android:state_pressed="true">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:propertyName="translationZ"
|
||||
android:valueTo="2dp"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="elevation"
|
||||
android:valueTo="6dp"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</item>
|
||||
|
||||
<!-- Hover state. This is triggered via mouse. -->
|
||||
<item
|
||||
android:state_enabled="true"
|
||||
android:state_hovered="true">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:propertyName="translationZ"
|
||||
android:valueTo="2dp"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="elevation"
|
||||
android:valueTo="6dp"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</item>
|
||||
|
||||
<!-- Focused state. This is triggered via keyboard. -->
|
||||
<item
|
||||
android:state_enabled="true"
|
||||
android:state_focused="true">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:propertyName="translationZ"
|
||||
android:valueTo="2dp"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="elevation"
|
||||
android:valueTo="6dp"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</item>
|
||||
|
||||
<!-- Base state (enabled, not pressed) -->
|
||||
<item android:state_enabled="true">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:propertyName="translationZ"
|
||||
android:startDelay="100"
|
||||
android:valueTo="0dp"
|
||||
android:valueType="floatType"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="elevation"
|
||||
android:valueTo="6dp"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</item>
|
||||
|
||||
<!-- Disabled state -->
|
||||
<item>
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="translationZ"
|
||||
android:valueTo="0dp"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="0"
|
||||
android:propertyName="elevation"
|
||||
android:valueTo="0dp"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</item>
|
||||
|
||||
</selector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="255dp" android:viewportHeight="255" android:viewportWidth="255" android:width="255dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FEFEFE" android:pathData="M128 0c70 0 127 57 127 127 0 71-57 128-127 128C57 255 0 198 0 127 0 57 57 0 128 0z"/>
|
||||
<path android:fillColor="#2A8336" android:pathData="M178 73c-15-16-36-22-57-21l12 10c31 3 56 29 56 61 0 34-27 61-61 61s-62-27-62-61c0-13 4-25 11-35V73c-28 28-28 73 0 101l51 51 50-51c28-28 28-73 0-101z"/>
|
||||
<path android:fillColor="#009FE3" android:pathData="M99 155c7 6 15 10 25 11v-27c-2 0-4-1-6-2l-19 18zm31-16v27c9-1 18-5 24-11l-19-18c-1 1-3 2-5 2zm-18-12c0 2 1 4 2 6l-19 19c-6-7-9-16-10-25h27zm57 0c-1 9-5 17-10 24l-19-19c1-1 1-3 2-5h27zm-57-6H85V86l29 29c-1 2-2 4-2 6zm56 0h-26c-1-2-1-4-3-6l19-19c6 7 10 16 10 25zm-14-29c-6-5-15-9-24-10v27c2 0 4 1 5 2l19-19zM85 77c7 8 30 30 34 34 1-1 3-2 5-2V65L85 30v47z"/>
|
||||
</vector>
|
|
@ -1,19 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="255dp"
|
||||
android:height="255dp"
|
||||
android:viewportWidth="255"
|
||||
android:viewportHeight="255" >
|
||||
|
||||
<path
|
||||
android:fillColor="#BBFEFEFE"
|
||||
android:pathData="M128 0c70 0 127 57 127 127 0 71-57 128-127 128C57 255 0 198 0 127 0 57 57 0 128 0z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#2A8336"
|
||||
android:pathData="M178 73c-15-16-36-22-57-21l12 10c31 3 56 29 56 61 0 34-27 61-61 61s-62-27-62-61c0-13 4-25 11-35V73c-28 28-28 73 0 101l51 51 50-51c28-28 28-73 0-101z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#009FE3"
|
||||
android:pathData="M99 155c7 6 15 10 25 11v-27c-2 0-4-1-6-2l-19 18zm31-16v27c9-1 18-5 24-11l-19-18c-1 1-3 2-5 2zm-18-12c0 2 1 4 2 6l-19 19c-6-7-9-16-10-25h27zm57 0c-1 9-5 17-10 24l-19-19c1-1 1-3 2-5h27zm-57-6H85V86l29 29c-1 2-2 4-2 6zm56 0h-26c-1-2-1-4-3-6l19-19c6 7 10 16 10 25zm-14-29c-6-5-15-9-24-10v27c2 0 4 1 5 2l19-19zM85 77c7 8 30 30 34 34 1-1 3-2 5-2V65L85 30v47z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/camera_view_background.xml
Normal file
5
app/src/main/res/drawable/camera_view_background.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<corners android:radius="10dp" />
|
||||
<solid android:color="#111" />
|
||||
</shape>
|
5
app/src/main/res/drawable/ic_arrow_forward.xml
Normal file
5
app/src/main/res/drawable/ic_arrow_forward.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_arrow_receive.xml
Normal file
10
app/src/main/res/drawable/ic_arrow_receive.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorReceive"
|
||||
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_arrow_send.xml
Normal file
10
app/src/main/res/drawable/ic_arrow_send.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorSend"
|
||||
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_balances.xml
Normal file
9
app/src/main/res/drawable/ic_balances.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21,18v1c0,1.1 -0.9,2 -2,2L5,21c-1.11,0 -2,-0.9 -2,-2L3,5c0,-1.1 0.89,-2 2,-2h14c1.1,0 2,0.9 2,2v1h-9c-1.11,0 -2,0.9 -2,2v8c0,1.1 0.89,2 2,2h9zM12,16h10L22,8L12,8v8zM16,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_bitsy_logo.xml
Normal file
5
app/src/main/res/drawable/ic_bitsy_logo.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:viewportHeight="499563" android:viewportWidth="500000" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#009FE3" android:pathData="M250000 0c138022 0 250000 111545 250000 249564 0 138022-111981 249999-250000 249999C111978 499563 0 387583 0 249564 0 111543 111981 0 250000 0z"/>
|
||||
<path android:fillColor="#FEFEFE" android:pathData="M348958 142794c-30382-30381-71614-43836-111545-40365l22135 19532c61632 4775 110243 56423 110243 119791 0 65973-53384 119357-119791 119357-65972 0-120227-53384-120227-119357 0-25606 7812-49478 21701-69010v-29948h-434c-55123 55122-55123 143665-436 198351l99394 98958 98958-99393c54687-54687 55121-143229 0-197916h2z"/>
|
||||
<path android:fillColor="#FEFEFE" android:pathData="M194446 304254c13022 11717 29514 19097 48177 20398v-52517c-3906-867-7812-2604-10851-4775l-37326 36891v3zm59462-32119v52517c18663-1301 35590-8681 48177-20398l-36891-36892c-3472 2169-7380 3906-11286 4775v-2zm-34723-23872c435 4340 2170 7811 4341 11285l-36892 37326c-11717-13454-19097-29947-19964-48611h52518-3zm111111 0c-1302 18230-8247 34723-19532 47742l-37326-37327c2168-3038 3471-6510 4340-10415h52518zm-111111-11283h-52517v-68141l57290 56858c-2603 3471-4340 7379-4775 11285l2-2zm110676 0h-52083c-867-3906-2604-7812-4775-11286l36892-36891c11717 13021 19097 29946 19964 48177h2zm-27776-55991c-12587-11286-29514-19098-48177-19964v52517c3906 867 7812 2170 11286 4340l36891-36891v-2zm-135417-29514c13888 14325 58161 58593 65536 65973 3472-1736 6511-3471 10417-3906v-86371l-76388-67709s435 89845 435 92013z"/>
|
||||
</vector>
|
12
app/src/main/res/drawable/ic_bitsy_logo_2.xml
Normal file
12
app/src/main/res/drawable/ic_bitsy_logo_2.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="29dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="294778"
|
||||
android:viewportHeight="400026">
|
||||
<path
|
||||
android:pathData="M251503,87576c-31929,-31929 -75261,-46069 -117225,-42422l23262,20527c64770,5019 115859,59297 115859,125894 0,69331 -56104,125435 -125893,125435l0,0 0,0c-69333,0 -126350,-56104 -126350,-125435 0,-26911 8209,-52000 22806,-72526l0,-31473 -457,0c-57930,57929 -57930,150981 -457,208453l104454,103998 103999,-104455c57472,-57472 57929,-150524 0,-207996l0,0 2,0z"
|
||||
android:fillColor="#FEFEFE"/>
|
||||
<path
|
||||
android:pathData="M89122,257258c13686,12315 31018,20069 50631,21438l0,-55193c-4105,-911 -8210,-2736 -11403,-5019l-39228,38771 0,3zM151613,223503l0,55193c19612,-1369 37402,-9123 50631,-21438l-38771,-38770c-3647,2279 -7756,4104 -11860,5019l0,-4zM115121,198415c458,4562 2280,8210 4562,11860l-38771,39228c-12314,-14140 -20069,-31472 -20980,-51088l55192,0 -3,0zM231891,198415c-1368,19159 -8667,36491 -20527,50174l-39227,-39228c2280,-3193 3648,-6840 4562,-10946l55192,0zM115121,186558l-55191,0c0,-20069 0,-41053 0,-71611l60208,59753c-2737,3649 -4562,7756 -5019,11860l2,-2zM231434,186558l-54735,0c-912,-4104 -2737,-8209 -5019,-11860l38771,-38771c12314,13685 20070,31472 20980,50631l3,0zM202244,127715c-13229,-11860 -31019,-20069 -50631,-20981l0,55193c4104,911 8209,2280 11860,4562l38771,-38771 0,-3zM59930,96700c14595,15053 61122,61576 68874,69332 3648,-1825 6842,-3648 10946,-4105 0,-15508 0,-90770 0,-90770l-80278,-71157c0,0 458,94420 458,96700z"
|
||||
android:fillColor="#FEFEFE"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_close.xml
Normal file
5
app/src/main/res/drawable/ic_close.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -5,6 +5,6 @@
|
|||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/colorPrimary"
|
||||
android:fillColor="@color/ppGreen"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
||||
|
|
5
app/src/main/res/drawable/ic_file_download.xml
Normal file
5
app/src/main/res/drawable/ic_file_download.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_filter.xml
Normal file
5
app/src/main/res/drawable/ic_filter.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
|
||||
</vector>
|
12
app/src/main/res/drawable/ic_merchants.xml
Normal file
12
app/src/main/res/drawable/ic_merchants.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M12,9m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_pie_chart.xml
Normal file
5
app/src/main/res/drawable/ic_pie_chart.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M11,2v20c-5.07,-0.5 -9,-4.79 -9,-10s3.93,-9.5 9,-10zM13.03,2v8.99L22,10.99c-0.47,-4.74 -4.24,-8.52 -8.97,-8.99zM13.03,13.01L13.03,22c4.74,-0.47 8.5,-4.25 8.97,-8.99h-8.97z"/>
|
||||
</vector>
|
23
app/src/main/res/drawable/ic_pin_merchants.xml
Normal file
23
app/src/main/res/drawable/ic_pin_merchants.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<vector android:height="51dp"
|
||||
android:viewportHeight="225.302"
|
||||
android:viewportWidth="154.618"
|
||||
android:width="31dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#0E9257" android:pathData="M151.705 76.396C151.705 35.31 118.397 2 77.31 2 36.219 2 2.913 35.31 2.913 76.396c0 20.073 7.961 38.277 20.889 51.66l-0.09 0.969s43.451 42.428 51.781 93.092h4.121c8.332-50.664 51.537-93.092 51.537-93.092l-0.352-0.457c12.936-13.388 20.906-32.087 20.906-52.172z" android:strokeColor="#FFFFFF" android:strokeWidth="5"/>
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M14.478 77.906c0-34.705 28.131-62.835 62.832-62.835 34.706 0 62.836 28.13 62.836 62.835 0 34.7-28.13 62.837-62.836 62.837-34.702 0-62.832-28.137-62.832-62.837z" android:strokeColor="#A5D5A7" android:strokeWidth="2.5162"/>
|
||||
<path android:fillColor="#0E9257" android:fillType="evenOdd" android:pathData="M37.714 47.167c-0.072-2.064 0.691-3.015 2.888-3.608 5.333 0 10.67 0.003 16.006 0.002 9.134-0.003 18.271-0.014 27.403-0.014 7.197 0 14.396 0.009 21.592 0.012 2.804 0.002 5.603-0.004 8.404-0.008 1.083-0.146 1.854 0.426 2.502 1.176 0.646 0.742 0.76 1.646 0.593 2.599-0.518 1.547-1.835 2.489-3.451 2.45-0.554-0.011-1.112-0.013-1.669-0.013-23.728-0.002-47.457 0.002-71.185-0.002-1.291-0.142-2.334-0.67-2.938-1.886-0.108-0.221-0.243-0.443-0.145-0.708z"/>
|
||||
<path android:fillColor="#0E9257" android:pathData="M118.797 82.431c-0.017 0.003-0.034 0.008-0.054 0.011-0.264 0.375-0.681 0.282-1.048 0.347l-1.943 0.369c-0.32 0.112-0.662-0.031-0.981 0.094-0.476-0.004-0.95-0.009-1.428-0.011-0.95 0.15-1.86-0.058-2.759-0.294v18.662H45.259V82.548c-1.265 0.451-2.589 0.714-3.988 0.751-0.155 0.005-0.305 0.034-0.451 0.077-1.104 0.032-2.187-0.046-3.236-0.265v21.727c3.944 4.054 8.478 7.523 13.471 10.257h54.286c4.983-2.732 9.518-6.2 13.457-10.251V82.431zm4.879-11.674c-0.106-0.602-0.237-1.201-0.354-1.8-0.612-2.371-1.12-4.767-1.619-7.161l-1.238-5.481c0.037-0.621-0.127-1.214-0.254-1.81-0.077-0.277-0.105-0.564-0.176-0.843-0.312-1.247-0.862-1.687-2.153-1.688-1.547-0.002-3.095-0.015-4.639 0.007-2.347-0.037-4.688-0.03-7.033-0.004-1.455-0.018-2.907-0.026-4.359 0.001-1.195 0.023-1.821 0.683-1.847 1.88-0.007 0.154-0.011 0.307 0.002 0.462 0.379 2.316 0.647 4.649 0.972 6.974 0.014 0.074 0.05 0.146 0.056 0.223 0.253 2.459 0.637 4.899 0.946 7.351 0.042 0.303 0.108 0.602 0.13 0.912 0.162 2.496 1.099 4.607 3.018 6.228 0.033 0.028 0.062 0.06 0.097 0.089 0.326 0.234 0.631 0.502 0.948 0.748 1.269 0.893 2.675 1.493 4.16 1.893 1.011 0.272 2.032 0.559 3.103 0.387l1.431 0.011c0.317-0.125 0.661 0.018 0.981-0.094l1.942-0.367c0.365-0.065 0.784 0.029 1.05-0.346l0.05-0.013c0.647-0.138 1.255-0.345 1.688-0.892 0.556-0.399 1.074-0.844 1.566-1.318 1.252-1.568 1.896-3.315 1.532-5.349zM52.445 51.958c-1.316 0.073-2.641 0.009-3.964 0.019-2.177-0.039-4.354-0.009-6.531-0.014-0.236-0.002-0.454 0.06-0.658 0.171-1.591 0.005-3.177 0.006-4.77 0.02-0.827 0.006-1.365 0.447-1.566 1.246-0.081 0.315-0.166 0.631-0.248 0.946-0.117 0.219-0.228 0.439-0.291 0.685-0.142 0.581-0.155 1.179-0.261 1.762-0.349 1.579-0.702 3.158-1.054 4.736-0.037 0.188-0.065 0.378-0.11 0.564-0.533 2.328-1.066 4.656-1.601 6.984-0.114 0.817-0.401 1.604-0.446 2.435-0.09 1.766 0.554 3.27 1.643 4.609 0.522 0.514 1.087 0.977 1.686 1.396 0.045 0.19 0.198 0.274 0.351 0.356 0.982 0.54 2.002 0.902 3.057 1.123 1.049 0.219 2.128 0.296 3.231 0.263 0.151-0.041 0.297-0.071 0.454-0.076 2.618-0.068 4.989-0.909 7.164-2.347 0.29-0.202 0.566-0.417 0.838-0.643 0.073-0.061 0.143-0.125 0.21-0.186 1.499-1.289 2.529-2.861 2.845-4.878 0.154-0.989 0.219-1.988 0.409-2.971 0.306-2.086 0.571-4.179 0.85-6.271 0.366-2.477 0.642-4.967 1.017-7.444 0.342-1.673-0.671-2.572-2.255-2.485zm46.477 9.917c-0.006-0.148-0.015-0.297-0.021-0.447 0.009-0.956-0.047-1.91-0.172-2.857-0.187-1.422-0.195-2.865-0.536-4.268 0.136-1.003-0.186-1.714-0.895-1.958-0.339-0.153-0.688-0.199-1.058-0.197-1.49 0.002-2.98-0.01-4.472-0.019-0.228-0.184-0.503-0.168-0.768-0.168-1.89-0.003-3.78-0.001-5.669-0.002-0.291 0-0.572 0.023-0.832 0.176-1.467 0.005-2.934 0.01-4.399 0.017-0.969 0.005-1.406 0.329-1.722 1.269-0.041 0.752-0.106 1.501-0.149 2.256 3.21-0.751 7.827-0.979 14.478-0.127l2.44 4.187-4.77-2.002 1.825 2.152-4.978 2.152s-3.053-3.073-4.676-3.285c0 0 2.033 4.521 1.154 4.14 0 0-1.986-1.227-5.39-1.749 0.013 0.327 0.021 0.654 0.03 0.984 1.441 0.392 2.902 1.049 4.074 2.146 0 0 6.576 5.317 9.561 14.262l-0.406 0.438c0.104-0.013 0.209-0.021 0.313-0.038 2.218-0.386 4.217-1.237 5.883-2.797 0.061-0.041 0.123-0.085 0.184-0.13 0.363-0.269 0.688-0.572 0.791-1.056 0.029-0.092 0.035-0.2 0.153-0.231 0.458-0.888 0.842-1.799 0.934-2.813 0.08-0.874-0.106-1.729-0.128-2.593-0.258-2.481-0.52-4.962-0.779-7.442z"/>
|
||||
<path android:fillColor="#0E9257" android:pathData="M88.538 73.529l-1.164 1.506-2.915-4.533-0.714 0.79s-4.243-1.543-4.465-5.193l-0.941 1.673c0.004 0.35-0.002 0.699 0.004 1.048 0.031 0.65 0.156 1.295 0.132 1.95-0.054 1.713 0.56 3.189 1.646 4.48 0.226 0.265 0.406 0.583 0.714 0.757 0.075 0.042 0.157 0.08 0.251 0.102 1.066 0.893 2.252 1.587 3.527 2.13 1.947 0.673 3.93 0.955 5.951 0.831-0.218-0.426-0.63-1.06-1.412-1.969-0.001 0.001-1.663-1.567-0.614-3.572zm-17.553-11.03s-0.049-0.415 0.068-1.045l0.005-0.002c0.006-0.025 0.006-0.053 0.014-0.078 0.028-0.073 0.002-0.052 0.078-0.07 0.102-0.022 0.204 0.003 0.301-0.046-0.034 0.008-0.068 0.007-0.105 0.009-0.088 0.015-0.178 0.029-0.266 0.045 0.3-1.406 1.417-3.73 5.407-5.131 0.011-0.562 0.018-1.125 0.009-1.687-0.005-0.077 0.008-0.155 0.008-0.231 0.022-1.646-0.599-2.289-2.232-2.294-1.113-0.002-2.225 0-3.334-0.001-0.291-0.001-0.577 0.012-0.835 0.164l-7.139-0.004c-1.556 0.009-3.107 0.011-4.663 0.024-0.964 0.008-1.538 0.527-1.661 1.484-0.036 0.284-0.079 0.569-0.116 0.854-0.132 0.104-0.162 0.252-0.18 0.407-0.135 1.062-0.255 2.126-0.34 3.194-0.059 0.753-0.165 1.513-0.088 2.277-0.021 0.594-0.071 1.184-0.207 1.763l-0.708 6.7c-0.014 0.249-0.014 0.5-0.057 0.745-0.333 1.865-0.124 3.637 0.882 5.275 0.17 0.493 0.405 0.935 0.916 1.153 0.002 0 0.002 0.002 0.005 0.004 0.83 0.799 1.759 1.457 2.804 1.952 0.203 0.096 0.414 0.175 0.624 0.256 0.929-2.816 3.475-8.805 9.553-14.586 0 0 1.345 1.415-0.254 2.75 0 0-5.172 6.826-6.514 12.597l0.146 0.028c2.385 0.261 4.715 0.019 6.983-0.798 1.242-0.536 2.395-1.217 3.441-2.083 0.08-0.037 0.158-0.075 0.235-0.117 0.123-0.068 0.241-0.147 0.337-0.258 1.016-1.145 1.766-2.415 2.024-3.955-0.724-1.412-1.596-2.757-2.408-4.107 0 0-0.448 0.115-1.048-0.318-0.78-0.425-1.106-1.189-1.416-2.02l-1.133-2.192c2.329-0.202 3.687 0.845 4.917 1.969 0.165 0.151 0.326 0.321 0.48 0.502 0.268 0.286 0.523 0.606 0.761 0.962 0.01-0.383 0.024-0.768 0.037-1.153-1.129-1.099-2.838-2.281-5.331-2.938zm-3.712 2.199l-4.204-0.359s2.349-3.566 6.402-1.527l-2.198 1.886zm2.638-2.36c-2.869-0.969-2.47-2.525-1.874-3.466 0.021-0.035 0.037-0.072 0.061-0.104 0.017-0.021 0.035-0.037 0.047-0.059 0.168-0.235 0.335-0.421 0.441-0.53 0.011-0.009 0.019-0.02 0.03-0.029 0.052-0.053 0.087-0.083 0.087-0.083 0.072-0.066 0.15-0.125 0.228-0.186 0.381-0.315 0.801-0.563 1.254-0.756l0.174-0.075c0.06-0.021 0.119-0.047 0.176-0.069 1.554-0.594 3.046-0.558 3.046-0.558-1.56 0.978-2.454 2.153-2.969 3.2-0.325 0.801-0.543 1.706-0.701 2.715z"/>
|
||||
<path android:fillColor="#0E9257" android:pathData="M76.377 60.942c-1.377-0.076-2.92-0.022-4.609 0.255-0.121 0.02-0.231 0.039-0.344 0.056 0.005 0.002 0.022 0.002 0.026 0.005l0.004-0.001c-0.051 0.021-0.061 0.057-0.078 0.098-0.015 0.029-0.019 0.044-0.023 0.055l0.031-0.004 1.106 0.234s1.727-0.195 3.889 0.092c-0.002-0.032 0-0.063 0-0.094-0.004-0.232 0-0.464-0.002-0.696z"/>
|
||||
<path android:fillColor="#F7F8F9" android:pathData="M77.698 76.82c0.63-1.794 0.581-4.177 0.058-6.393 0.302 1.703 0.332 3.802-0.058 6.393zm-1.554-5.131c-0.005 0.038-0.013 0.072-0.02 0.106 0.798 1.559 1.417 3.201 1.573 5.025 0 0-0.055-2.078-1.549-5.174 0.001 0.016 0.001 0.029-0.004 0.043zm-4.891-6.338c0.31 0.83 0.636 1.594 1.416 2.019-0.43-0.309-0.937-0.897-1.416-2.019zm4.265 0.278c-0.154-0.181-0.316-0.351-0.48-0.502-1.231-1.125-2.588-2.171-4.917-1.969-0.001 0 3.09-0.006 5.397 2.471z"/>
|
||||
<path android:fillColor="#F7F8F9" android:pathData="M76.149 71.646c-0.573-1.189-1.35-2.525-2.432-3.958 0.812 1.35 1.684 2.695 2.408 4.107 0.006-0.034 0.014-0.068 0.02-0.106 0.004-0.014 0.004-0.027 0.004-0.043zm-7.563-13.467l0.03-0.029c-0.011 0.01-0.018 0.02-0.03 0.029zm-0.488 0.589c-0.024 0.032-0.041 0.069-0.061 0.104 0.036-0.057 0.073-0.112 0.108-0.163-0.012 0.021-0.03 0.037-0.047 0.059zm2.262-1.718l0.176-0.069c-0.057 0.022-0.116 0.048-0.176 0.069zm-0.449 5.288c0.158-1.009 0.376-1.914 0.704-2.715-0.737 1.49-0.704 2.715-0.704 2.715zm-0.979-4.457c0.395-0.314 0.824-0.562 1.254-0.756-0.454 0.192-0.873 0.44-1.254 0.756zM59.7 79.872l2.96 2.071c-0.062-0.923 0.059-1.927 0.297-2.964-0.948-0.188-1.884-0.411-2.785-0.761-0.344 1.041-0.472 1.654-0.472 1.654zm10.837-22.891c0.928-0.327 1.968-0.467 3.046-0.558 0 0-1.493-0.036-3.046 0.558z"/>
|
||||
<path android:fillColor="#F7F8F9" android:pathData="M73.583 56.423c-1.503 0.733-2.402 1.807-2.969 3.2 0.515-1.047 1.409-2.222 2.969-3.2zm-5.437 2.286c0.138-0.191 0.287-0.37 0.441-0.53-0.106 0.109-0.274 0.295-0.441 0.53zm2.04-1.584c0.061-0.024 0.114-0.052 0.174-0.075l-0.174 0.075zm-1.482 0.942s-0.035 0.03-0.087 0.083c0.1-0.099 0.208-0.181 0.315-0.269-0.078 0.061-0.157 0.12-0.228 0.186z"/>
|
||||
<path android:fillAlpha="0.3" android:fillColor="#F7F8F9" android:pathData="M77.756 70.428c-0.288-1.609-0.822-2.86-1.477-3.837-0.002 0.031-0.002 0.063-0.002 0.094 0.646 1.064 1.155 2.37 1.479 3.743z" android:strokeAlpha="0.3"/>
|
||||
<path android:fillColor="#0E9257" android:pathData="M73.717 67.688c-0.428-0.049-0.765-0.163-1.048-0.318 0.6 0.433 1.048 0.318 1.048 0.318z"/>
|
||||
<path android:fillAlpha="0.3" android:fillColor="#F7F8F9" android:pathData="M73.717 67.688c-0.428-0.049-0.765-0.163-1.048-0.318 0.6 0.433 1.048 0.318 1.048 0.318z" android:strokeAlpha="0.3"/>
|
||||
<path android:fillColor="#0E9257" android:pathData="M71.253 65.35c-0.279-0.751-0.545-1.553-1.133-2.192l1.133 2.192z"/>
|
||||
<path android:fillAlpha="0.3" android:fillColor="#F7F8F9" android:pathData="M71.253 65.35c-0.279-0.751-0.545-1.553-1.133-2.192l1.133 2.192z" android:strokeAlpha="0.3"/>
|
||||
<path android:fillColor="#0E9257" android:pathData="M76.276 66.685c0-0.031 0-0.062 0.002-0.094-0.238-0.355-0.493-0.676-0.761-0.962 0.271 0.312 0.524 0.669 0.759 1.056z"/>
|
||||
<path android:fillAlpha="0.3" android:fillColor="#F7F8F9" android:pathData="M76.276 66.685c0-0.031 0-0.062 0.002-0.094-0.238-0.355-0.493-0.676-0.761-0.962 0.271 0.312 0.524 0.669 0.759 1.056z" android:strokeAlpha="0.3"/>
|
||||
<path android:fillColor="#F7F8F9" android:pathData="M76.267 68.808c-0.055 0.945 0.021 1.893-0.118 2.839 1.494 3.096 1.549 5.174 1.549 5.174 0.391-2.592 0.36-4.69 0.058-6.393-0.324-1.373-0.833-2.679-1.479-3.743-0.02 0.707-0.032 1.415-0.01 2.123zm0.11-7.866c0.689 0.038 1.335 0.106 1.938 0.2-0.06-1.619-0.151-3.238-0.119-4.858 0.002-0.203 0.023-0.406 0.034-0.609-0.636 0.149-1.215 0.32-1.745 0.506-0.033 1.586-0.113 3.173-0.108 4.761zm1.968 1.184c-0.671-0.185-1.336-0.31-1.966-0.394 0.032 1.237-0.017 2.473-0.064 3.706 1.377 1.342 1.896 2.564 1.896 2.564l0.127-0.229c-0.003-1.042-0.011-2.085 0-3.127 0.034-0.841 0.028-1.681 0.007-2.52zm12.449 17.688l0.779-0.841c-0.337 0.044-0.676 0.076-1.01 0.098 0.274 0.54 0.231 0.743 0.231 0.743z"/>
|
||||
</vector>
|
7
app/src/main/res/drawable/ic_receive.xml
Normal file
7
app/src/main/res/drawable/ic_receive.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fromDegrees="90"
|
||||
android:toDegrees="90"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%"
|
||||
android:drawable="@drawable/ic_send">
|
||||
</rotate>
|
5
app/src/main/res/drawable/ic_search.xml
Normal file
5
app/src/main/res/drawable/ic_search.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_send.xml
Normal file
5
app/src/main/res/drawable/ic_send.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFF" android:pathData="M12,8V4l8,8 -8,8v-4H4V8z"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue