Merge branch 'develop'

This commit is contained in:
Severiano Jaramillo 2019-01-16 18:46:49 -06:00
commit 710a04ac5d
64 changed files with 2378 additions and 913 deletions

4
.gitignore vendored
View file

@ -64,3 +64,7 @@ app/fabric.properties
# Allocation tracker
/captures/*
# Google services info
app/google-services.json

View file

@ -13,13 +13,14 @@ android {
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "0.1"
versionName "0.8.0-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
// TODO Fix minify issues and enable again
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
@ -34,6 +35,11 @@ android {
exclude 'lib/x86_64/freebsd/libscrypt.so'
exclude 'lib/x86_64/linux/libscrypt.so'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
@ -47,7 +53,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// AndroidX
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
// Google
implementation 'com.google.zxing:core:3.3.1'
implementation 'com.google.code.gson:gson:2.8.5'
@ -67,18 +73,21 @@ dependencies {
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
// Retrofit & OkHttp
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.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'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.8'
// 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'
implementation 'com.afollestad.material-dialogs:core:2.0.0-rc7'
// Android Debug Database
debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'

View file

@ -44,8 +44,6 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".activities.LicenseActivity"/>
<activity android:name=".activities.ImportBrainkeyActivity"/>
<activity
android:name=".activities.MainActivity"
android:screenOrientation="portrait"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,7 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.crashlytics.android.Crashlytics
import cy.agorise.bitsybitshareswallet.database.entities.Balance
import cy.agorise.bitsybitshareswallet.processors.TransfersLoader
import cy.agorise.bitsybitshareswallet.repositories.AssetRepository
@ -40,16 +41,21 @@ import kotlin.collections.ArrayList
import kotlin.collections.HashMap
/**
* Class in charge of managing the connection to graphenej's NetworkService
* The app uses the single Activity methodology, but this activity was created so that MainActivity can extend from it.
* This class manages everything related to keeping the information in the database updated using graphenej's
* NetworkService, leaving to MainActivity only the Navigation work and some other UI features.
*/
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
companion object {
private const val TAG = "ConnectedActivity"
private const val RESPONSE_GET_FULL_ACCOUNTS = 1
private const val RESPONSE_GET_ACCOUNTS = 2
private const val RESPONSE_GET_ACCOUNT_BALANCES = 3
private const val RESPONSE_GET_ASSETS = 4
private const val RESPONSE_GET_BLOCK_HEADER = 5
}
private lateinit var mUserAccountViewModel: UserAccountViewModel
private lateinit var mBalanceViewModel: BalanceViewModel
@ -89,10 +95,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userId = PreferenceManager.getDefaultSharedPreferences(this)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
if (userId != "")
mCurrentAccount = UserAccount(userId)
getUserAccount()
mAssetRepository = AssetRepository(this)
@ -139,10 +142,19 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
.subscribe { handleIncomingMessage(it) }
}
/**
* Obtains the userId from the shared preferences and creates a [UserAccount] instance.
* Created as a public function, so that it can be called from its Fragments.
*/
fun getUserAccount() {
val userId = PreferenceManager.getDefaultSharedPreferences(this)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
if (userId != "")
mCurrentAccount = UserAccount(userId)
}
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)) {
@ -178,8 +190,11 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
).show()
}
} else if (message is ConnectionStatusUpdate) {
handleConnectionStatusUpdate(message)
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
if (message.updateCode == ConnectionStatusUpdate.CONNECTED) {
// Make sure the Crashlytics report contains the currently selected node
val selectedNode = mNetworkService?.selectedNode
Crashlytics.log(selectedNode?.url)
} else 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()
@ -198,7 +213,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
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()
mNetworkService?.removeCurrentNodeAndReconnect()
} else if (storedOpCount == -1L) {
// Initial case when the app starts
storedOpCount = latestOpCount
@ -291,7 +306,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
}
private fun updateBalances() {
if (mNetworkService!!.isConnected) {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccountBalances(mCurrentAccount, ArrayList()),
GetAccountBalances.REQUIRED_API)
@ -304,7 +319,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
*/
private val mRequestMissingUserAccountsTask = object : Runnable {
override fun run() {
if (mNetworkService!!.isConnected) {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAccounts(missingUserAccounts), GetAccounts.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ACCOUNTS
@ -319,7 +334,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
*/
private val mRequestMissingAssetsTask = object : Runnable {
override fun run() {
if (mNetworkService!!.isConnected) {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetAssets(missingAssets), GetAssets.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ASSETS
@ -334,7 +349,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
*/
private val mCheckMissingPaymentsTask = object : Runnable {
override fun run() {
if (mNetworkService != null && mNetworkService!!.isConnected) {
if (mNetworkService?.isConnected == true) {
if (mCurrentAccount != null) {
val userAccounts = ArrayList<String>()
userAccounts.add(mCurrentAccount!!.objectId)
@ -357,7 +372,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
private val mRequestBlockMissingTimeTask = object : Runnable {
override fun run() {
if (mNetworkService != null && mNetworkService!!.isConnected) {
if (mNetworkService?.isConnected == true) {
val id = mNetworkService!!.sendMessage(GetBlockHeader(blockNumberWithMissingTime),
GetBlockHeader.REQUIRED_API)
@ -387,6 +402,7 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
mHandler.removeCallbacks(mCheckMissingPaymentsTask)
mHandler.removeCallbacks(mRequestMissingUserAccountsTask)
mHandler.removeCallbacks(mRequestMissingAssetsTask)
mHandler.removeCallbacks(mRequestBlockMissingTimeTask)
}
override fun onResume() {
@ -405,16 +421,4 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
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)
}

View file

@ -1,55 +0,0 @@
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()
}
}

View file

@ -12,12 +12,13 @@ import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupActionBarWithNavController
import cy.agorise.bitsybitshareswallet.R
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.*
/**
* Uses the AAC Navigation Component with a NavHostFragment which is the place where all Fragments are shown,
* following the philosophy of using a single Activity.
*/
class MainActivity : ConnectedActivity() {
private val TAG = this.javaClass.simpleName
private lateinit var appBarConfiguration : AppBarConfiguration
@ -100,15 +101,12 @@ class MainActivity : ConnectedActivity() {
return findNavController(R.id.navHostFragment).navigateUp(appBarConfiguration)
}
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
override fun onBackPressed() {
// Trick used to avoid crashes when the user is in the License or ImportBrainkey and presses the back button
val currentDestination=NavHostFragment.findNavController(navHostFragment).currentDestination
when(currentDestination?.id) {
R.id.license_dest, R.id.import_brainkey_dest -> finish()
else -> super.onBackPressed()
}
/**
* Private method called whenever there's an update to the connection status
* @param connectionStatusUpdate Connection status update.
*/
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
}
}

View file

@ -9,7 +9,7 @@ class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, LicenseActivity::class.java)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}

View file

@ -9,7 +9,9 @@ import android.widget.TextView
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
/**
* Adapter used to populate a Spinner with a list of [BalanceDetail] items.
*/
class BalancesDetailsAdapter(context: Context, resource: Int, data: List<BalanceDetail>) :
ArrayAdapter<BalanceDetail>(context, resource, data) {

View file

@ -128,7 +128,7 @@ class TransfersDetailsAdapter(private val context: Context) :
val cryptoAmount = "${df.format(amount)} ${transferDetail.cryptoSymbol}"
viewHolder.tvCryptoAmount.text = cryptoAmount
viewHolder.tvFiatEquivalent.text = "$4119.75"
viewHolder.tvFiatEquivalent.text = "-"
viewHolder.ivDirectionArrow.setImageDrawable(context.getDrawable(
if(transferDetail.direction) R.drawable.ic_arrow_receive else R.drawable.ic_arrow_send

View file

@ -15,6 +15,6 @@ interface AssetDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(assets: List<Asset>)
@Query("SELECT * FROM assets")
fun getAll(): LiveData<List<Asset>>
@Query("SELECT id, symbol, precision, description, bit_asset_id FROM assets INNER JOIN balances WHERE assets.id = balances.asset_id AND balances.asset_amount > 0")
fun getAllNonZero(): LiveData<List<Asset>>
}

View file

@ -7,6 +7,6 @@ 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")
"FROM balances INNER JOIN assets on balances.asset_id = assets.id WHERE balances.asset_amount > 0")
fun getAll(): LiveData<List<BalanceDetail>>
}

View file

@ -0,0 +1,102 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.preference.PreferenceManager
import androidx.navigation.fragment.findNavController
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.AuthorityType
import cy.agorise.graphenej.BrainKey
import cy.agorise.graphenej.PublicKey
import cy.agorise.graphenej.models.AccountProperties
import org.bitcoinj.core.ECKey
import cy.agorise.bitsybitshareswallet.activities.ConnectedActivity
abstract class BaseAccountFragment : ConnectedFragment() {
/** Private variable that will hold an instance of the [BrainKey] class */
protected var mBrainKey: BrainKey? = null
/**
* 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
*/
protected fun onAccountSelected(accountProperties: AccountProperties, pin: String) {
val encryptedPIN = CryptoUtils.encrypt(context!!, pin)
// Stores the user selected PIN encrypted
PreferenceManager.getDefaultSharedPreferences(context!!)
.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(context!!.applicationContext)
userAccountRepository.insert(userAccount)
// Stores the id of the currently active user account
PreferenceManager.getDefaultSharedPreferences(context!!).edit()
.putString(Constants.KEY_CURRENT_ACCOUNT_ID, accountProperties.id).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!!)
}
}
// Force [ConnectedActivity] to refresh the userId from the SharedPreferences, so that the app can immediately
// to fetch the account's transactions.
(activity as ConnectedActivity).getUserAccount()
// Send the user back to HomeFragment
findNavController().navigate(R.id.home_action)
}
/**
* 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(context!!, brainKeyWords)
val encryptedSequenceNumber = CryptoUtils.encrypt(context!!, sequenceNumber.toString())
val encryptedWIF = CryptoUtils.encrypt(context!!, wif)
val authority = Authority(0, userId, authorityType, encryptedWIF, encryptedBrainKey, encryptedSequenceNumber)
val authorityRepository = AuthorityRepository(context!!)
authorityRepository.insert(authority)
}
}

View file

@ -0,0 +1,106 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.android.NetworkService
import cy.agorise.graphenej.api.android.RxBus
import cy.agorise.graphenej.models.JsonRpcResponse
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
/**
* Base fragment that defines the methods and variables commonly used in all fragments that directly connect and
* talk to the BitShares nodes through graphenej's NetworkService
*/
abstract class ConnectedFragment : Fragment(), ServiceConnection {
companion object {
private const val TAG = "ConnectedFragment"
}
/** Network service connection */
protected var mNetworkService: NetworkService? = null
/** Flag used to keep track of the NetworkService binding state */
private var mShouldUnbindNetwork: Boolean = false
/** Keeps track of all RxJava disposables, to make sure they are all disposed when the fragment is destroyed */
protected var mDisposables = CompositeDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 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<*>) {
// Generic processing taken care by subclasses
handleJsonRpcResponse(message)
} else if (message is ConnectionStatusUpdate) {
// Generic processing taken care by subclasses
handleConnectionStatusUpdate(message)
}
}
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
}
/**
* Method to be implemented by all subclasses in order to be notified of JSON-RPC responses.
* @param response
*/
abstract fun handleJsonRpcResponse(response: JsonRpcResponse<*>)
/**
* Method to be implemented by all subclasses in order to be notified of connection status updates
* @param connectionStatusUpdate
*/
abstract fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate)
}

View file

@ -0,0 +1,305 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import com.jakewharton.rxbinding3.widget.textChanges
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.network.FaucetService
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.containsDigits
import cy.agorise.bitsybitshareswallet.utils.containsVowels
import cy.agorise.bitsybitshareswallet.utils.toast
import cy.agorise.graphenej.Address
import cy.agorise.graphenej.BrainKey
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.GetAccountByName
import cy.agorise.graphenej.models.AccountProperties
import cy.agorise.graphenej.models.JsonRpcResponse
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_create_account.*
import org.bitcoinj.core.ECKey
import retrofit2.Callback
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.concurrent.TimeUnit
import com.afollestad.materialdialogs.MaterialDialog
import cy.agorise.bitsybitshareswallet.models.FaucetRequest
import cy.agorise.bitsybitshareswallet.models.FaucetResponse
import cy.agorise.bitsybitshareswallet.network.ServiceGenerator
import retrofit2.Call
import retrofit2.Response
import java.util.*
class CreateAccountFragment : BaseAccountFragment() {
companion object {
private const val TAG = "CreateAccountFragment"
private const val BRAINKEY_FILE = "brainkeydict.txt"
private const val MIN_ACCOUNT_NAME_LENGTH = 8
// Used when trying to validate that the account name is available
private const val RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION = 1
// Used when trying to obtain the info of the newly created account
private const val RESPONSE_GET_ACCOUNT_BY_NAME_CREATED = 2
}
private lateinit var mAddress: String
/** Variables used to store the validation status of the form fields */
private var isPINValid = false
private var isPINConfirmationValid = false
private var isAccountValidAndAvailable = false
// Map used to keep track of request and response id pairs
private val responseMap = HashMap<Long, Int>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_create_account, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Use RxJava Debounce to check the validity and availability of the user's proposed account name
mDisposables.add(
tietAccountName.textChanges()
.skipInitialValue()
.debounce(800, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { validateAccountName(it.toString()) }
)
// 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() }
)
btnCancel.setOnClickListener { findNavController().navigateUp() }
btnCreate.isEnabled = false
btnCreate.setOnClickListener { createAccount() }
// Generating BrainKey
generateKeys()
}
private fun validateAccountName(accountName: String) {
isAccountValidAndAvailable = false
if ( !isAccountNameValid(accountName) ) {
tilAccountName.helperText = ""
tilAccountName.error = getString(R.string.error__invalid_account_name)
} else {
tilAccountName.isErrorEnabled = false
tilAccountName.helperText = getString(R.string.text__verifying_account_availability)
val id = mNetworkService?.sendMessage(GetAccountByName(accountName), GetAccountByName.REQUIRED_API)
if (id != null)
responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION
}
enableDisableCreateButton()
}
/**
* Method used to determine if the account name entered by the user is valid
* @param accountName The proposed account name
* @return True if the name is valid, false otherwise
*/
private fun isAccountNameValid(accountName: String): Boolean {
return accountName.length >= MIN_ACCOUNT_NAME_LENGTH &&
(accountName.containsDigits() || !accountName.containsVowels()) &&
!accountName.contains("_")
}
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
}
enableDisableCreateButton()
}
private fun enableDisableCreateButton() {
btnCreate.isEnabled = (isPINValid && isPINConfirmationValid && isAccountValidAndAvailable)
}
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
if (responseMap.containsKey(response.id)) {
val responseType = responseMap[response.id]
when (responseType) {
RESPONSE_GET_ACCOUNT_BY_NAME_VALIDATION -> handleAccountNameValidation(response.result)
RESPONSE_GET_ACCOUNT_BY_NAME_CREATED -> handleAccountNameCreated(response.result)
}
responseMap.remove(response.id)
}
}
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
/**
* Handles the response from the NetworkService's GetAccountByName call to decide if the user's suggested
* account is available or not.
*/
private fun handleAccountNameValidation(result: Any?) {
if (result is AccountProperties) {
tilAccountName.helperText = ""
tilAccountName.error = getString(R.string.error__account_not_available)
isAccountValidAndAvailable = false
} else {
tilAccountName.isErrorEnabled = false
tilAccountName.helperText = getString(R.string.text__account_is_available)
isAccountValidAndAvailable = true
}
enableDisableCreateButton()
}
/**
* Handles the response from the NetworkService's GetAccountByName call and stores the information of the newly
* created account if the result is successful, shows a toast error otherwise
*/
private fun handleAccountNameCreated(result: Any?) {
if (result is AccountProperties) {
onAccountSelected(result, tietPin.text.toString())
} else {
context?.toast(getString(R.string.error__created_account_not_found))
}
}
/**
* Sends the account-creation request to the faucet server.
* Only account name and public address is sent here.
*/
private fun createAccount() {
val accountName = tietAccountName.text.toString()
val faucetRequest = FaucetRequest(accountName, mAddress, Constants.FAUCET_REFERRER)
val sg = ServiceGenerator(Constants.FAUCET_URL)
val faucetService = sg.getService(FaucetService::class.java)
val call = faucetService.registerPrivateAccount(faucetRequest)
// Execute the call asynchronously. Get a positive or negative callback.
call.enqueue(object : Callback<FaucetResponse> {
override fun onResponse(call: Call<FaucetResponse>, response: Response<FaucetResponse>) {
// The network call was a success and we got a response, obtain the info of the newly created account
// with a delay to let the nodes update their information
val handler = Handler()
handler.postDelayed({
getCreatedAccountInfo(response.body())
}, 4000)
}
override fun onFailure(call: Call<FaucetResponse>, t: Throwable) {
// the network call was a failure
MaterialDialog(context!!)
.title(R.string.title_error)
.message(cy.agorise.bitsybitshareswallet.R.string.error__faucet)
.negativeButton(android.R.string.ok)
.show()
}
})
}
private fun getCreatedAccountInfo(faucetResponse: FaucetResponse?) {
if (faucetResponse?.account != null) {
val id = mNetworkService?.sendMessage(GetAccountByName(faucetResponse.account?.name),
GetAccountByName.REQUIRED_API)
if (id != null)
responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME_CREATED
} else {
Log.d(TAG, "Private account creation failed ")
val content = if (faucetResponse?.error?.base?.size ?: 0 > 0) {
getString(R.string.error__faucet_template, faucetResponse?.error?.base?.get(0))
} else {
getString(R.string.error__faucet_template, "None")
}
MaterialDialog(context!!)
.title(R.string.title_error)
.message(text = content)
.show()
}
}
/**
* Method that generates a fresh key that will be controlling the newly created account.
*/
private fun generateKeys() {
var reader: BufferedReader? = null
val dictionary: String
try {
reader = BufferedReader(InputStreamReader(context!!.assets.open(BRAINKEY_FILE), "UTF-8"))
dictionary = reader.readLine()
val brainKeySuggestion = BrainKey.suggest(dictionary)
mBrainKey = BrainKey(brainKeySuggestion, 0)
val address = Address(ECKey.fromPublicOnly(mBrainKey?.privateKey?.pubKey))
Log.d(TAG, "brain key: $brainKeySuggestion")
Log.d(TAG, "address would be: " + address.toString())
mAddress = address.toString()
tvBrainKey.text = mBrainKey?.brainKey
} catch (e: IOException) {
Log.e(TAG, "IOException while trying to generate key. Msg: " + e.message)
context?.toast(getString(R.string.error__read_dict_file))
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
Log.e(TAG, "IOException while trying to close BufferedReader. Msg: " + e.message)
}
}
}
}
}

View file

@ -6,11 +6,18 @@ import android.content.res.Resources
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.view.View
import androidx.fragment.app.DialogFragment
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.Observer
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
import cy.agorise.bitsybitshareswallet.views.DatePickerFragment
import java.text.SimpleDateFormat
import java.util.*
import kotlin.ClassCastException
@ -22,24 +29,65 @@ import kotlin.ClassCastException
*/
class FilterOptionsDialog : DialogFragment() {
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_ASSET_ALL = "key_filter_asset_all"
const val KEY_FILTER_ASSET = "key_filter_asset"
const val KEY_FILTER_FIAT_AMOUNT_ALL = "key_filter_fiat_amount_all"
const val KEY_FILTER_FROM_FIAT_AMOUNT = "key_filter_from_fiat_amount"
const val KEY_FILTER_TO_FIAT_AMOUNT = "key_filter_to_fiat_amount"
const val KEY_FILTER_AGORISE_FEES = "key_filter_agorise_fees"
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, filterAssetAll: Boolean,
filterAsset: String, filterFiatAmountAll: Boolean,
filterFromFiatAmount: Long, filterToFiatAmount: Long, filterAgoriseFees: Boolean): 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_ASSET_ALL, filterAssetAll)
args.putString(KEY_FILTER_ASSET, filterAsset)
args.putBoolean(KEY_FILTER_FIAT_AMOUNT_ALL, filterFiatAmountAll)
args.putLong(KEY_FILTER_FROM_FIAT_AMOUNT, filterFromFiatAmount)
args.putLong(KEY_FILTER_TO_FIAT_AMOUNT, filterToFiatAmount)
args.putBoolean(KEY_FILTER_AGORISE_FEES, filterAgoriseFees)
frag.arguments = args
return frag
}
}
// 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
private lateinit var rbTransactionAll: RadioButton
private lateinit var rbTransactionSent: RadioButton
private lateinit var rbTransactionReceived: RadioButton
private lateinit var cbDateRange: CheckBox
private lateinit var llDateRange: LinearLayout
private lateinit var tvStartDate: TextView
private lateinit var tvEndDate: TextView
private lateinit var cbAsset: CheckBox
private lateinit var sAsset: Spinner
private lateinit var cbFiatAmount: CheckBox
private lateinit var llFiatAmount: LinearLayout
// lateinit var etFromFiatAmount: CurrencyEditText
// lateinit var etToFiatAmount: CurrencyEditText
private lateinit var switchAgoriseFees: Switch
private var mCallback: OnFilterOptionsSelectedListener? = null
private var mDatePickerHandler: DatePickerHandler? = null
private lateinit var mDatePickerHandler: DatePickerHandler
private var dateFormat: SimpleDateFormat = SimpleDateFormat("d/MMM/yyyy",
Resources.getSystem().configuration.locale)
@ -52,43 +100,9 @@ class FilterOptionsDialog : DialogFragment() {
// */
// private val mUserCurrency = RuntimeData.EXTERNAL_CURRENCY
companion object {
private lateinit var mBalanceDetailViewModel: BalanceDetailViewModel
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
}
}
private var mBalancesDetailsAdapter: BalancesDetailsAdapter? = null
/**
* DatePicker message handler.
@ -139,11 +153,12 @@ class FilterOptionsDialog : DialogFragment() {
filterDateRangeAll: Boolean,
filterStartDate: Long,
filterEndDate: Long,
filterCryptocurrencyAll: Boolean,
filterCryptocurrency: String,
filterAssetAll: Boolean,
filterAsset: String,
filterFiatAmountAll: Boolean,
filterFromFiatAmount: Long,
filterToFiatAmount: Long)
filterToFiatAmount: Long,
filterAgoriseFees: Boolean)
}
@ -154,9 +169,9 @@ class FilterOptionsDialog : DialogFragment() {
mDatePickerHandler = DatePickerHandler()
val builder = AlertDialog.Builder(context!!)
.setTitle("Filter options")
.setPositiveButton("Filter") { _, _ -> validateFields() }
.setNegativeButton("Cancel") { _, _ -> dismiss() }
.setTitle(getString(R.string.title_filter_options))
.setPositiveButton(getString(R.string.button__filter)) { _, _ -> validateFields() }
.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> dismiss() }
// Inflate layout
val inflater = activity!!.layoutInflater
@ -175,32 +190,47 @@ class FilterOptionsDialog : DialogFragment() {
// 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 }
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)
tvStartDate = view.findViewById(R.id.tvStartDate)
tvEndDate = view.findViewById(R.id.tvEndDate)
// sCryptocurrency = view.findViewById(R.id.sCryptocurrency)
// initializeCryptocurrencySpinner()
startDate = arguments!!.getLong(KEY_FILTER_START_DATE, 0)
tvStartDate.setOnClickListener(mDateClickListener)
endDate = arguments!!.getLong(KEY_FILTER_END_DATE, 0)
tvEndDate.setOnClickListener(mDateClickListener)
updateDateTextViews()
// Initialize Asset
cbAsset = view.findViewById(R.id.cbAsset)
sAsset = view.findViewById(R.id.sAsset)
cbAsset.setOnCheckedChangeListener { _, isChecked ->
sAsset.visibility = if(isChecked) View.GONE else View.VISIBLE
}
cbAsset.isChecked = arguments!!.getBoolean(KEY_FILTER_ASSET_ALL, true)
// Configure BalanceDetailViewModel to obtain the user's Balances
mBalanceDetailViewModel = ViewModelProviders.of(this).get(BalanceDetailViewModel::class.java)
mBalanceDetailViewModel.getAll().observe(this, Observer<List<BalanceDetail>> { balancesDetails ->
mBalancesDetailsAdapter = BalancesDetailsAdapter(context!!, android.R.layout.simple_spinner_item, balancesDetails!!)
sAsset.adapter = mBalancesDetailsAdapter
val assetSelected = arguments!!.getString(KEY_FILTER_ASSET)
// Try to select the selectedAssetSymbol
for (i in 0 until mBalancesDetailsAdapter!!.count) {
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == assetSelected) {
sAsset.setSelection(i)
break
}
}
})
// Initialize Fiat amount
cbFiatAmount = view.findViewById(R.id.cbFiatAmount)
@ -221,6 +251,10 @@ class FilterOptionsDialog : DialogFragment() {
// val toFiatAmount = arguments!!.getLong(KEY_FILTER_TO_FIAT_AMOUNT, 0)
// etToFiatAmount.setText("$toFiatAmount", TextView.BufferType.EDITABLE)
// Initialize transaction network fees
switchAgoriseFees = view.findViewById(R.id.switchAgoriseFees)
switchAgoriseFees.isChecked = arguments!!.getBoolean(KEY_FILTER_AGORISE_FEES, true)
builder.setView(view)
return builder.create()
@ -238,48 +272,29 @@ class FilterOptionsDialog : DialogFragment() {
}
}
// 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()
// 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")
// }
// 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 {
@ -291,9 +306,9 @@ class FilterOptionsDialog : DialogFragment() {
val filterDateRangeAll = cbDateRange.isChecked
val filterCryptocurrencyAll = cbCryptocurrency.isChecked
val filterAssetAll = cbAsset.isChecked
val filterCryptocurrency = "" //(sCryptocurrency.selectedItem as CryptoCurrency).symbol
val filterAsset = (sAsset.selectedItem as BalanceDetail).symbol
val filterFiatAmountAll = cbFiatAmount.isChecked
@ -309,8 +324,10 @@ class FilterOptionsDialog : DialogFragment() {
// Math.pow(10.0, mUserCurrency.defaultFractionDigits.toDouble()).toLong()
// }
val filterAgoriseFees = switchAgoriseFees.isChecked
mCallback!!.onFilterOptionsSelected(filterTransactionsDirection, filterDateRangeAll,
startDate, endDate, filterCryptocurrencyAll, filterCryptocurrency, filterFiatAmountAll,
filterFromFiatAmount, filterToFiatAmount)
startDate, endDate, filterAssetAll, filterAsset, filterFiatAmountAll,
filterFromFiatAmount, filterToFiatAmount, filterAgoriseFees)
}
}

View file

@ -3,9 +3,9 @@ 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.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
@ -23,14 +23,21 @@ class HomeFragment : Fragment() {
private lateinit var mUserAccountViewModel: UserAccountViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
// Sets the toolbar background color to primaryColor and forces shows the Bitsy icon to the left
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.navigationIcon = resources.getDrawable(R.drawable.ic_bitsy_logo_2, null)
toolbar?.setBackgroundResource(if (!nightMode) R.color.colorPrimary else R.color.colorToolbarDark)
// Sets the status bar background color to a primaryColorDark
val window = activity?.window
window?.statusBarColor = ContextCompat.getColor(context!!,
if (!nightMode) R.color.colorPrimaryDark else R.color.colorStatusBarDark)
return inflater.inflate(R.layout.fragment_home, container, false)
}
@ -38,14 +45,23 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Get version number of the last agreed license version
val agreedLicenseVersion = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, 0)
val userId = PreferenceManager.getDefaultSharedPreferences(context)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") ?: ""
if (agreedLicenseVersion != Constants.CURRENT_LICENSE_VERSION || userId == "") {
findNavController().navigate(R.id.license_action)
return
}
// 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
mUserAccountViewModel.getUserAccount(userId).observe(this, Observer<UserAccount>{ user ->
tvAccountName.text = user?.name ?: ""
})
// Navigate to the Receive Transaction Fragment
@ -61,7 +77,7 @@ class HomeFragment : Fragment() {
// Navigate to the Send Transaction Fragment using Navigation's SafeArgs to activate the camera
fabSendTransactionCamera.setOnClickListener {
val action = HomeFragmentDirections.sendActionCamera()
action.setOpenCamera(true)
action.openCamera = true
findNavController().navigate(action)
}

View file

@ -1,46 +1,50 @@
package cy.agorise.bitsybitshareswallet.activities
package cy.agorise.bitsybitshareswallet.fragments
import android.content.Intent
import android.content.ComponentName
import android.os.Bundle
import android.preference.PreferenceManager
import android.os.Handler
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.navigation.Navigation
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.list.customListAdapter
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import com.jakewharton.rxbinding3.widget.textChanges
import cy.agorise.bitsybitshareswallet.BuildConfig
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.adapters.FullNodesAdapter
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
import cy.agorise.bitsybitshareswallet.utils.toast
import cy.agorise.graphenej.*
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.calls.GetAccounts
import cy.agorise.graphenej.api.calls.GetDynamicGlobalProperties
import cy.agorise.graphenej.api.calls.GetKeyReferences
import cy.agorise.graphenej.models.AccountProperties
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 kotlinx.android.synthetic.main.activity_import_brainkey.*
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_import_brainkey.*
import org.bitcoinj.core.ECKey
import java.text.NumberFormat
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 ImportBrainkeyFragment : BaseAccountFragment() {
class ImportBrainkeyActivity : ConnectedActivity() {
private val TAG = "ImportBrainkeyActivity"
companion object {
private const 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
*/
/** User account associated with the key derived from the brainkey that the user just typed in */
private var mUserAccount: UserAccount? = null
/**
@ -51,18 +55,32 @@ class ImportBrainkeyActivity : ConnectedActivity() {
private var mKeyReferencesAttempts = 0
private var keyReferencesRequestId: Long = 0
private var getAccountsRequestId: Long = 0
private var mDisposables = CompositeDisposable()
private var keyReferencesRequestId: Long? = null
private var getAccountsRequestId: Long? = null
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)
// 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 mNodesAdapter: FullNodesAdapter? = null
/** Handler that will be used to make recurrent calls to get the latest BitShares block number*/
private val mHandler = Handler()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Remove up navigation icon from the toolbar
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.navigationIcon = null
return inflater.inflate(R.layout.fragment_import_brainkey, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Use RxJava Debounce to update the PIN error only after the user stops writing for > 500 ms
mDisposables.add(
@ -94,6 +112,35 @@ class ImportBrainkeyActivity : ConnectedActivity() {
btnImport.isEnabled = false
btnImport.setOnClickListener { verifyBrainKey(false) }
btnCreate.setOnClickListener (
Navigation.createNavigateOnClickListener(R.id.create_account_action)
)
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
mNodesAdapter = FullNodesAdapter(v.context)
mNodesAdapter?.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(mNodesAdapter as FullNodesAdapter)
.negativeButton(android.R.string.ok)
.onDismiss { mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask) }
mNodesDialog?.show()
// Registering a recurrent task used to poll for dynamic global properties requests
mHandler.post(mRequestDynamicGlobalPropertiesTask)
}
}
}
private fun validatePIN() {
@ -161,7 +208,7 @@ class ImportBrainkeyActivity : ConnectedActivity() {
*/
private fun verifyBrainKey(switchCase: Boolean) {
//showDialog("", getString(R.string.importing_your_wallet))
val brainKey = tietBrainKey.text.toString()
val brainKey = tietBrainKey.text.toString().trim()
// Should we switch the brainkey case?
if (switchCase) {
if (Character.isUpperCase(brainKey.toCharArray()[brainKey.length - 1])) {
@ -189,70 +236,19 @@ class ImportBrainkeyActivity : ConnectedActivity() {
mBrainKey = BrainKey(brainKey, 0)
val address = Address(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
Log.d(TAG, String.format("Brainkey would generate address: %s", address.toString()))
keyReferencesRequestId = mNetworkService!!.sendMessage(GetKeyReferences(address), GetKeyReferences.REQUIRED_API)
keyReferencesRequestId = mNetworkService?.sendMessage(GetKeyReferences(address), GetKeyReferences.REQUIRED_API)
}
override fun 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
)
}
}
}
handleBrainKeyAccountReferences(response.result)
} 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()
handleAccountProperties(response.result)
} else if (response.result is DynamicGlobalProperties) {
val dynamicGlobalProperties = response.result as DynamicGlobalProperties
if (mNodesDialog != null && mNodesDialog?.isShowing == true) {
val blockNumber = NumberFormat.getInstance().format(dynamicGlobalProperties.head_block_number)
mNodesDialog?.message(text = getString(R.string.title__bitshares_nodes_dialog, blockNumber))
}
}
}
@ -262,96 +258,132 @@ class ImportBrainkeyActivity : ConnectedActivity() {
}
/**
* 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
* Handles the response from the NetworkService when the app asks for the accounts that are controlled by a
* specified BrainKey
*/
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!!)
}
private fun handleBrainKeyAccountReferences(result: Any?) {
if (result !is List<*> || result[0] !is List<*>) {
context?.toast(getString(R.string.error__invalid_brainkey))
return
}
// 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()
val resp = result as List<List<UserAccount>>
val accountList: List<UserAccount> = resp[0].distinct()
// Send the user to the MainActivity
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
if (accountList.isEmpty()) {
if (mKeyReferencesAttempts == 0) {
mKeyReferencesAttempts++
verifyBrainKey(true)
} else {
context?.toast(getString(R.string.error__invalid_brainkey))
}
} 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
)
}
}
/**
* Adds the given BrainKey encrypted as AuthorityType of userId.
* Handles the response from the NetworkService when the app asks for the AccountProperties of a list of
* Accounts controlled by the given BrainKey
*/
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)
private fun handleAccountProperties(result: Any?) {
if (result is List<*> && result[0] is AccountProperties) {
val accountPropertiesList = result as List<AccountProperties>
if (accountPropertiesList.size > 1) {
val candidates = ArrayList<String>()
for (accountProperties in accountPropertiesList) {
candidates.add(accountProperties.name)
}
MaterialDialog(context!!)
.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], tietPin.text.toString())
}
}
.positiveButton(android.R.string.ok)
.negativeButton(android.R.string.cancel) {
mKeyReferencesAttempts = 0
}
.cancelable(false)
.show()
} else if (accountPropertiesList.size == 1) {
onAccountSelected(accountPropertiesList[0], tietPin.text.toString())
} else {
context?.toast(getString(R.string.error__try_again))
}
}
}
override fun onDestroy() {
super.onDestroy()
/**
* Observer used to be notified about node latency measurement updates.
*/
private val nodeLatencyObserver = object : Observer<FullNode> {
override fun onSubscribe(d: Disposable) {
mDisposables.add(d)
}
if (!mDisposables.isDisposed) mDisposables.dispose()
override fun onNext(fullNode: FullNode) {
if (!fullNode.isRemoved)
mNodesAdapter?.add(fullNode)
else
mNodesAdapter?.remove(fullNode)
}
override fun onError(e: Throwable) {
Log.e(TAG, "nodeLatencyObserver.onError.Msg: " + e.message)
}
override fun onComplete() {}
}
/**
* 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)
}
}
override fun onServiceDisconnected(name: ComponentName?) {
super.onServiceDisconnected(name)
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
resources.getDrawable(R.drawable.ic_disconnected, null), null)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
super.onServiceConnected(name, service)
tvNetworkStatus.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null,
resources.getDrawable(R.drawable.ic_connected, null), null)
}
}

View file

@ -0,0 +1,54 @@
package cy.agorise.bitsybitshareswallet.fragments
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.utils.Constants
import kotlinx.android.synthetic.main.fragment_license.*
class LicenseFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Remove up navigation icon from the toolbar
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.navigationIcon = null
return inflater.inflate(R.layout.fragment_license, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Get version number of the last agreed license version
val agreedLicenseVersion = PreferenceManager.getDefaultSharedPreferences(context)
.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.loadUrl("file:///android_asset/eula.html")
btnDisagree.setOnClickListener { activity?.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.
*/
private fun agree() {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putInt(Constants.KEY_LAST_AGREED_LICENSE_VERSION, Constants.CURRENT_LICENSE_VERSION).apply()
findNavController().navigate(R.id.import_brainkey_action)
}
}

View file

@ -1,22 +1,18 @@
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.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.google.common.primitives.UnsignedLong
@ -35,12 +31,9 @@ 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
@ -50,16 +43,19 @@ import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
class ReceiveTransactionFragment : Fragment(), ServiceConnection {
private val TAG = this.javaClass.simpleName
class ReceiveTransactionFragment : ConnectedFragment() {
private val RESPONSE_LIST_ASSETS = 1
private val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
companion object {
private const val TAG = "ReceiveTransactionFrag"
private const val RESPONSE_LIST_ASSETS = 1
private const 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 const val AUTO_SUGGEST_ASSET_LIMIT = 5
private val OTHER_ASSET = "other_asset"
private const val OTHER_ASSET = "other_asset"
}
private lateinit var mUserAccountViewModel: UserAccountViewModel
private lateinit var mAssetViewModel: AssetViewModel
@ -67,8 +63,6 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
/** Current user account */
private var mUserAccount: UserAccount? = null
private var mDisposables = CompositeDisposable()
private var mAsset: Asset? = null
private var mAssetsAdapter: AssetsAdapter? = null
@ -85,15 +79,21 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
// 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)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
// Sets the toolbar background color to green
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.setBackgroundResource(if (!nightMode) R.color.colorReceive else R.color.colorToolbarDark)
// Sets the status bar background color to a dark green
val window = activity?.window
window?.statusBarColor = ContextCompat.getColor(context!!,
if (!nightMode) R.color.colorReceiveDark else R.color.colorStatusBarDark)
return inflater.inflate(R.layout.fragment_receive_transaction, container, false)
}
@ -114,14 +114,14 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
// Configure Assets spinner to show Assets already saved into the db
mAssetViewModel = ViewModelProviders.of(this).get(AssetViewModel::class.java)
mAssetViewModel.getAll().observe(this,
mAssetViewModel.getAllNonZero().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, "", ""
OTHER_ASSET, getString(R.string.text__other), 0, "", ""
)
mAssets.add(asset)
@ -198,35 +198,33 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
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]
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
if (responseMap.containsKey(response.id)) {
val responseType = responseMap[response.id]
when (responseType) {
RESPONSE_LIST_ASSETS -> handleListAssets(message.result as List<Asset>)
RESPONSE_LIST_ASSETS -> handleListAssets(response.result)
}
responseMap.remove(message.id)
responseMap.remove(response.id)
}
} else if (message is ConnectionStatusUpdate) {
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
}
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
if (connectionStatusUpdate.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
// If we got a disconnection notification, we should clear our response map, since
// all its stored request ids will now be reset
responseMap.clear()
}
}
}
private fun handleListAssets(assetList: List<Asset>) {
/**
* Handles the list of assets returned from the node that correspond to what the user has typed in the Asset
* AutoCompleteTextView and adds them to its adapter to show as suggestions.
*/
private fun handleListAssets(result: Any?) {
if (result is List<*> && result.isNotEmpty() && result[0] is Asset) {
val assetList = result as List<Asset>
Log.d(TAG, "handleListAssets")
val assets = ArrayList<cy.agorise.bitsybitshareswallet.database.entities.Asset>()
for (_asset in assetList) {
@ -243,6 +241,7 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
mAutoSuggestAssetAdapter.setData(assets)
mAutoSuggestAssetAdapter.notifyDataSetChanged()
}
}
private fun updateQR() {
if (mAsset == null) {
@ -325,14 +324,18 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
* @param account Account to pay total
*/
private fun updateAmountAddressUI(total: AssetAmount, account: String) {
val txtAmount: String = if (total.amount.toLong() == 0L) {
getString(R.string.template__please_send, getString(R.string.text__any_amount), " ")
} else {
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)
getString(R.string.template__please_send, strAmount, total.asset.symbol)
}
val txtAmount = getString(R.string.template__please_pay, strAmount, total.asset.symbol)
val txtAccount = getString(R.string.template__to, account)
tvPleasePay.text = txtAmount
@ -370,8 +373,7 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
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()
Toast.makeText(context!!, getString(R.string.msg__storage__permission__necessary), Toast.LENGTH_SHORT).show()
}
return
}
@ -406,39 +408,4 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection {
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
}
}

View file

@ -1,24 +1,20 @@
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.view.*
import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.fragment.findNavController
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.google.common.primitives.UnsignedLong
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
@ -27,23 +23,21 @@ 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.utils.*
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.crypto.SecureRandomGenerator
import cy.agorise.graphenej.models.AccountProperties
import cy.agorise.graphenej.models.DynamicGlobalProperties
import cy.agorise.graphenej.models.JsonRpcResponse
import cy.agorise.graphenej.operations.TransferOperation
import cy.agorise.graphenej.operations.TransferOperationBuilder
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.fragment_send_transaction.*
import me.dm7.barcodescanner.zxing.ZXingScannerView
@ -57,17 +51,22 @@ 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
class SendTransactionFragment : ConnectedFragment(), ZXingScannerView.ResultHandler {
companion object {
private const val TAG = "SendTransactionFragment"
// Camera Permission
private val REQUEST_CAMERA_PERMISSION = 1
private const 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
// Constants used to organize NetworkService requests
private const val RESPONSE_GET_ACCOUNT_BY_NAME = 1
private const val RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS = 2
private const val RESPONSE_GET_REQUIRED_FEES = 3
private const val RESPONSE_BROADCAST_TRANSACTION = 4
}
/** Variables used in field's validation */
private var isCameraPreviewVisible = false
private var isToAccountCorrect = false
private var isAmountCorrect = false
@ -78,6 +77,7 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
private var mBalancesDetailsAdapter: BalancesDetailsAdapter? = null
/** Keeps track of the asset's symbol selected in the Asset spinner */
private var selectedAssetSymbol = ""
/** Current user account */
@ -86,17 +86,10 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
/** 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>()
/** Transaction being built */
private var transaction: Transaction? = null
/** Variable holding the current user's private key in the WIF format */
@ -105,10 +98,24 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
/** 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 */
/** This is one of the recipient account's public key, it will be used for memo encoding */
private var destinationPublicKey: PublicKey? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
val nightMode = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false)
// Sets the toolbar background color to red
val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar)
toolbar?.setBackgroundResource(if (!nightMode) R.color.colorSend else R.color.colorToolbarDark)
// Sets the status bar background color to a dark red
val window = activity?.window
window?.statusBarColor = ContextCompat.getColor(context!!,
if (!nightMode) R.color.colorSendDark else R.color.colorStatusBarDark)
return inflater.inflate(R.layout.fragment_send_transaction, container, false)
}
@ -116,13 +123,13 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
super.onViewCreated(view, savedInstanceState)
val userId = PreferenceManager.getDefaultSharedPreferences(context)
.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
.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)
val safeArgs = SendTransactionFragmentArgs.fromBundle(arguments!!)
if (safeArgs.openCamera)
verifyCameraPermission()
@ -145,27 +152,16 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
}
})
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)
}
}
spAsset.onItemSelectedListener = assetItemSelectedListener
fabSendTransaction.setOnClickListener { startSendTransferOperation() }
fabSendTransaction.hide()
fabSendTransaction.disable(R.color.lightGray)
authorityRepository = AuthorityRepository(context!!)
// Obtain the WifKey from the db, which is used in the Send Transfer procedure
mDisposables.add(
authorityRepository!!.getWIF(userId!!, AuthorityType.ACTIVE.ordinal)
authorityRepository!!.getWIF(userId, AuthorityType.ACTIVE.ordinal)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { encryptedWIF ->
@ -181,56 +177,67 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
// Use RxJava Debounce to avoid making calls to the NetworkService on every text change event
mDisposables.add(
tietTo.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.map { it.toString().trim() }
.filter { it.length > 1 }
.subscribe {
val id = mNetworkService!!.sendMessage(GetAccountByName(it!!), GetAccountByName.REQUIRED_API)
responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME
val id = mNetworkService?.sendMessage(GetAccountByName(it!!), GetAccountByName.REQUIRED_API)
if (id != null) responseMap[id] = RESPONSE_GET_ACCOUNT_BY_NAME
}
)
// Use RxJava Debounce to update the Amount error only after the user stops writing for > 500 ms
mDisposables.add(
tietAmount.textChanges()
.skipInitialValue()
.debounce(500, TimeUnit.MILLISECONDS)
.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) }
.subscribe { validateAmount() }
)
}
private fun handleIncomingMessage(message: Any?) {
if (message is JsonRpcResponse<*>) {
if (responseMap.containsKey(message.id)) {
val responseType = responseMap[message.id]
/** Handles the selection of items in the Asset spinner, to keep track of the selectedAssetSymbol and show the
* current user's balance of the selected asset. */
private val assetItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onNothingSelected(parent: AdapterView<*>?) { }
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val balance = mBalancesDetailsAdapter!!.getItem(position)!!
selectedAssetSymbol = balance.symbol
val amount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
tvAvailableAssetAmount.text =
String.format("%." + Math.min(balance.precision, 8) + "f %s", amount, balance.symbol)
validateAmount()
}
}
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
if (responseMap.containsKey(response.id)) {
val responseType = responseMap[response.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)
RESPONSE_GET_ACCOUNT_BY_NAME -> handleAccountProperties(response.result)
RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS -> handleDynamicGlobalProperties(response.result)
RESPONSE_GET_REQUIRED_FEES -> handleRequiredFees(response.result)
RESPONSE_BROADCAST_TRANSACTION -> handleBroadcastTransaction(response)
}
responseMap.remove(message.id)
responseMap.remove(response.id)
}
} else if (message is ConnectionStatusUpdate) {
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
}
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
if (connectionStatusUpdate.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
// If we got a disconnection notification, we should clear our response map, since
// all its stored request ids will now be reset
responseMap.clear()
}
}
}
private fun handleAccountName(result: Any?) {
/** Handles the result of the [GetAccountByName] api call to find out if the account written in the To text
* field corresponds to an actual BitShares account or not and acts accordingly */
private fun handleAccountProperties(result: Any?) {
if (result is AccountProperties) {
mSelectedUserAccount = UserAccount(result.id, result.name)
destinationPublicKey = result.active.keyAuths.keys.iterator().next()
@ -239,13 +246,16 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
} else {
mSelectedUserAccount = null
destinationPublicKey = null
tilTo.error = "Invalid account"
tilTo.error = getString(R.string.error__invalid_account)
isToAccountCorrect = false
}
enableDisableSendFAB()
}
/** Handles the result of the [GetDynamicGlobalProperties] api call to add the needed metadata to the [Transaction]
* the app is building and ultimately send, if everything is correct adds the needed info to the [Transaction] and
* calls the next step which is [GetRequiredFees] else it shows an error */
private fun handleDynamicGlobalProperties(result: Any?) {
if (result is DynamicGlobalProperties) {
val expirationTime = (result.time.time / 1000) + Transaction.DEFAULT_EXPIRATION_TIME
@ -256,43 +266,41 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
val asset = Asset(mBalancesDetailsAdapter!!.getItem(spAsset.selectedItemPosition)!!.id)
val id = mNetworkService!!.sendMessage(GetRequiredFees(transaction!!, asset), GetRequiredFees.REQUIRED_API)
responseMap[id] = RESPONSE_GET_REQUIRED_FEES
val id = mNetworkService?.sendMessage(GetRequiredFees(transaction!!, asset), GetRequiredFees.REQUIRED_API)
if (id != null) responseMap[id] = RESPONSE_GET_REQUIRED_FEES
} else {
// TODO unableToSendTransactionError()
context?.toast(getString(R.string.msg__transaction_not_sent))
}
}
/** Handles the result of the [GetRequiredFees] api call to add the fees to the [Transaction] the app is building
* and ultimately send, and if everything is correct broadcasts the [Transaction] else it shows an error */
private fun handleRequiredFees(result: Any?) {
if (result is List<*> && result[0] is AssetAmount) {
Log.d(TAG, "GetRequiredFees: " + transaction.toString())
transaction!!.setFees(result as List<AssetAmount>) // TODO find how to remove this warning
val id = mNetworkService!!.sendMessage(BroadcastTransaction(transaction), BroadcastTransaction.REQUIRED_API)
responseMap[id] = RESPONSE_BROADCAST_TRANSACTION
val id = mNetworkService?.sendMessage(BroadcastTransaction(transaction), BroadcastTransaction.REQUIRED_API)
if (id != null) responseMap[id] = RESPONSE_BROADCAST_TRANSACTION
} else {
// TODO unableToSendTransactionError()
context?.toast(getString(R.string.msg__transaction_not_sent))
}
}
/** Handles the result of the [BroadcastTransaction] api call to find out if the Transaction was sent successfully
* or not and acts accordingly */
private fun handleBroadcastTransaction(message: JsonRpcResponse<*>) {
if (message.result == null && message.error == null) {
// TODO extract string resources
Toast.makeText(context!!, "Transaction sent!", Toast.LENGTH_SHORT).show()
context?.toast(getString(R.string.text__transaction_sent))
// 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()
// Return to the main screen
findNavController().navigateUp()
} else if (message.error != null) {
context?.toast(message.error.message, Toast.LENGTH_LONG)
}
}
/** Verifies if the user has already granted the Camera permission, if not the asks for it */
private fun verifyCameraPermission() {
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
@ -304,6 +312,7 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
}
}
/** Handles the result from the camera permission request */
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@ -311,8 +320,7 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
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()
context?.toast(getString(R.string.msg__camera_permission_necessary))
}
return
}
@ -340,6 +348,8 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
cameraPreview.stopCamera()
}
/** Handles the result of the QR code read from the camera and tries to populate the Account, Amount and Memo fields
* and the Asset spinner with the obtained information */
override fun handleResult(result: Result?) {
try {
val invoice = Invoice.fromQrCode(result!!.text)
@ -348,21 +358,25 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
tietTo.setText(invoice.to)
for (i in 0 until mBalancesDetailsAdapter!!.count) {
if (mBalancesDetailsAdapter!!.getItem(i)!!.symbol == invoice.currency.toUpperCase()) {
// Try to select the invoice's Asset in the Assets spinner
for (i in 0 until (mBalancesDetailsAdapter?.count ?: 0)) {
if (mBalancesDetailsAdapter?.getItem(i)?.symbol == invoice.currency.toUpperCase() ||
(invoice.currency.startsWith("bit", true) &&
invoice.currency.replaceFirst("bit", "").toUpperCase() ==
mBalancesDetailsAdapter?.getItem(i)?.symbol)) {
spAsset.setSelection(i)
break
}
}
tietMemo.setText(invoice.memo)
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("####.#####")
val df = DecimalFormat("####.########")
df.roundingMode = RoundingMode.CEILING
df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
tietAmount.setText(df.format(amount))
@ -372,33 +386,52 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
}
}
private fun validateAmount(amount: Double) {
private fun validateAmount() {
val txtAmount = tietAmount.text.toString()
if (mBalancesDetailsAdapter?.isEmpty != false) return
val balance = mBalancesDetailsAdapter?.getItem(spAsset.selectedItemPosition) ?: return
val currentAmount = balance.amount.toDouble() / Math.pow(10.0, balance.precision.toDouble())
if (currentAmount < amount) {
// TODO extract string resource
tilAmount.error = "Not enough funds"
val amount: Double = try {
txtAmount.toDouble()
} catch (e: Exception) {
0.0
}
when {
currentAmount < amount -> {
tilAmount.error = getString(R.string.error__not_enough_funds)
isAmountCorrect = false
} else {
}
amount == 0.0 -> {
tilAmount.isErrorEnabled = false
isAmountCorrect = false
}
else -> {
tilAmount.isErrorEnabled = false
isAmountCorrect = true
}
}
enableDisableSendFAB()
}
private fun enableDisableSendFAB() {
if (isToAccountCorrect && isAmountCorrect)
fabSendTransaction.show()
else
fabSendTransaction.hide()
if (isToAccountCorrect && isAmountCorrect) {
fabSendTransaction.enable(R.color.colorSend)
vSend.setBackgroundResource(R.drawable.send_fab_background)
} else {
fabSendTransaction.disable(R.color.lightGray)
vSend.setBackgroundResource(R.drawable.send_fab_background_disabled)
}
}
/** Starts the Send Transfer operation procedure, creating a [TransferOperation] and sending a call to the
* NetworkService to obtain the [DynamicGlobalProperties] object needed to successfully send a Transfer */
private fun startSendTransferOperation() {
// Create TransferOperation
if (mNetworkService!!.isConnected) {
if (mNetworkService?.isConnected == true) {
val balance = mBalancesDetailsAdapter!!.getItem(spAsset.selectedItemPosition)!!
val amount = (tietAmount.text.toString().toDouble() * Math.pow(10.0, balance.precision.toDouble())).toLong()
@ -411,66 +444,82 @@ class SendTransactionFragment : Fragment(), ZXingScannerView.ResultHandler, Serv
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)
// }
// Add memo if it is not empty
val memoMsg = tietMemo.text.toString()
if (memoMsg.isNotEmpty()) {
val nonce = Math.abs(SecureRandomGenerator.getSecureRandom().nextLong()).toBigInteger()
val encryptedMemo = Memo.encryptMessage(privateKey, destinationPublicKey!!, nonce, memoMsg)
val from = Address(ECKey.fromPublicOnly(privateKey.pubKey))
val to = Address(destinationPublicKey!!.key)
val memo = Memo(from, to, nonce, encryptedMemo)
operationBuilder.setMemo(memo)
}
// Object that will contain all operations to be sent at once
val operations = ArrayList<BaseOperation>()
operations.add(operationBuilder.build())
// Transfer from the current user to the selected one
val transferOperation = operationBuilder.build()
operations.add(transferOperation)
// Transfer operation to be sent as a fee to Agorise
val feeOperation = getAgoriseFeeOperation(transferOperation)
if (feeOperation != null && (feeOperation.assetAmount?.amount?.toLong() ?: 0L) > 0L)
operations.add(feeOperation)
transaction = Transaction(privateKey, null, operations)
val id = mNetworkService!!.sendMessage(GetDynamicGlobalProperties(),
// Start the send transaction procedure which includes a series of calls
val id = mNetworkService?.sendMessage(GetDynamicGlobalProperties(),
GetDynamicGlobalProperties.REQUIRED_API)
responseMap[id] = RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS
if (id != null ) responseMap[id] = RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS
} else
Log.d(TAG, "Network Service is not connected")
}
/**
* Obtains the correct [TransferOperation] object to send the fee to Agorise. A fee is only sent if the Asset is
* BTS or a SmartCoin.
*/
private fun getAgoriseFeeOperation(transferOperation: TransferOperation): TransferOperation? {
// Verify that the current Asset is either BTS or a SmartCoin
if (Constants.assetsWhichSendFeeToAgorise.contains(transferOperation.assetAmount?.asset?.objectId ?: "")) {
val fee = transferOperation.assetAmount?.multiplyBy(Constants.FEE_PERCENTAGE) ?: return null
return TransferOperationBuilder()
.setSource(mUserAccount)
.setDestination(Constants.AGORISE_ACCOUNT)
.setTransferAmount(fee)
.build()
} else
return null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_send_transaction, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if (item?.itemId == R.id.menu_info) {
MaterialDialog(context!!).show {
customView(R.layout.dialog_send_transaction_info, scrollable = true)
positiveButton(android.R.string.ok) { dismiss() }
}
return true
}
return super.onOptionsItemSelected(item)
}
override fun onResume() {
super.onResume()
if (isCameraPreviewVisible)
startCameraPreview()
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
}
}

View file

@ -1,7 +1,6 @@
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
@ -10,9 +9,9 @@ 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.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.customListAdapter
import cy.agorise.bitsybitshareswallet.BuildConfig
@ -37,7 +36,10 @@ import kotlinx.android.synthetic.main.fragment_settings.*
import java.text.NumberFormat
class SettingsFragment : Fragment(), ServiceConnection {
private val TAG = this.javaClass.simpleName
companion object {
private const val TAG = "SettingsFragment"
}
private var mDisposables = CompositeDisposable()
@ -79,15 +81,14 @@ class SettingsFragment : Fragment(), ServiceConnection {
val fullNodes = mNetworkService!!.nodes
nodesAdapter = FullNodesAdapter(v.context)
nodesAdapter!!.add(fullNodes)
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)
}
.negativeButton(android.R.string.ok)
.onDismiss { mHandler.removeCallbacks(mRequestDynamicGlobalPropertiesTask) }
mNodesDialog?.show()
@ -228,13 +229,7 @@ class SettingsFragment : Fragment(), ServiceConnection {
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()
}
positiveButton(R.string.button__copied) { it.dismiss() }
}
}

View file

@ -39,11 +39,12 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
private var filterDateRangeAll = true
private var filterStartDate = 0L
private var filterEndDate = 0L
private var filterCryptocurrencyAll = true
private var filterCryptocurrency = "BTS"
private var filterAssetAll = true
private var filterAsset = "BTS"
private var filterFiatAmountAll = true
private var filterFromFiatAmount = 0L
private var filterToFiatAmount = 500L
private var filterAgoriseFees = true
private var mDisposables = CompositeDisposable()
@ -110,8 +111,8 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
R.id.menu_filter -> {
val filterOptionsDialog = FilterOptionsDialog.newInstance(
filterTransactionsDirection, filterDateRangeAll, filterStartDate * 1000,
filterEndDate * 1000, filterCryptocurrencyAll, filterCryptocurrency,
filterFiatAmountAll, filterFromFiatAmount, filterToFiatAmount
filterEndDate * 1000, filterAssetAll, filterAsset,
filterFiatAmountAll, filterFromFiatAmount, filterToFiatAmount, filterAgoriseFees
)
filterOptionsDialog.show(childFragmentManager, "filter-options-tag")
true
@ -160,14 +161,18 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
if (!filterDateRangeAll && (transferDetail.date < filterStartDate || transferDetail.date > filterEndDate))
continue
// Filter by cryptocurrency
if (!filterCryptocurrencyAll && transferDetail.cryptoSymbol != filterCryptocurrency)
// Filter by asset
if (!filterAssetAll && transferDetail.cryptoSymbol != filterAsset)
continue
// // Filter by fiat amount
// if (!filterFiatAmountAll && (transferDetail.fiatAmount < filterFromFiatAmount || transferDetail.fiatAmount > filterToFiatAmount))
// continue
// Filter transactions sent to agorise
if (filterAgoriseFees && transferDetail.to.equals("agorise"))
continue
// Filter by search query
val text = (transferDetail.from ?: "").toLowerCase() + (transferDetail.to ?: "").toLowerCase()
if (text.contains(filterQuery, ignoreCase = true)) {
@ -183,28 +188,30 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
}
/**
*
* Gets called when the user selects some filter options in the [FilterOptionsDialog] and wants to apply them.
*/
override fun onFilterOptionsSelected(
filterTransactionsDirection: Int,
filterDateRangeAll: Boolean,
filterStartDate: Long,
filterEndDate: Long,
filterCryptocurrencyAll: Boolean,
filterCryptocurrency: String,
filterAssetAll: Boolean,
filterAsset: String,
filterFiatAmountAll: Boolean,
filterFromFiatAmount: Long,
filterToFiatAmount: Long
filterToFiatAmount: Long,
filterAgoriseFees: Boolean
) {
this.filterTransactionsDirection = filterTransactionsDirection
this.filterDateRangeAll = filterDateRangeAll
this.filterStartDate = filterStartDate / 1000
this.filterEndDate = filterEndDate / 1000
this.filterCryptocurrencyAll = filterCryptocurrencyAll
this.filterCryptocurrency = filterCryptocurrency
this.filterAssetAll = filterAssetAll
this.filterAsset = filterAsset
this.filterFiatAmountAll = filterFiatAmountAll
this.filterFromFiatAmount = filterFromFiatAmount
this.filterToFiatAmount = filterToFiatAmount
this.filterAgoriseFees = filterAgoriseFees
applyFilterOptions(true)
}

View file

@ -0,0 +1,72 @@
package cy.agorise.bitsybitshareswallet.models;
/**
* Class used to deserialize a the "account" object contained in the faucet response to the
* {@link cy.agorise.bitsybitshareswallet.network.FaucetService#registerPrivateAccount(FaucetRequest)} API call.
*/
public class FaucetAccount {
public String name;
public String owner_key;
public String active_key;
public String memo_key;
public String referrer;
public String refcode;
public FaucetAccount(String accountName, String address, String referrer){
this.name = accountName;
this.owner_key = address;
this.active_key = address;
this.memo_key = address;
this.refcode = referrer;
this.referrer = referrer;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getOwnerKey() {
return owner_key;
}
public void setOwnerKey(String owner_key) {
this.owner_key = owner_key;
}
public String getActiveKey() {
return active_key;
}
public void setActiveKey(String active_key) {
this.active_key = active_key;
}
public String getMemoKey() {
return memo_key;
}
public void setMemoKey(String memo_key) {
this.memo_key = memo_key;
}
public String getRefcode() {
return refcode;
}
public void setRefcode(String refcode) {
this.refcode = refcode;
}
public String getReferrer() {
return referrer;
}
public void setReferrer(String referrer) {
this.referrer = referrer;
}
}

View file

@ -0,0 +1,13 @@
package cy.agorise.bitsybitshareswallet.models;
/**
* Class used to encapsulate a faucet account creation request
*/
public class FaucetRequest {
private FaucetAccount account;
public FaucetRequest(String accountName, String address, String referrer){
account = new FaucetAccount(accountName, address, referrer);
}
}

View file

@ -0,0 +1,10 @@
package cy.agorise.bitsybitshareswallet.models;
public class FaucetResponse {
public FaucetAccount account;
public Error error;
public class Error {
public String[] base;
}
}

View file

@ -0,0 +1,23 @@
package cy.agorise.bitsybitshareswallet.network
import cy.agorise.bitsybitshareswallet.models.FaucetRequest
import cy.agorise.bitsybitshareswallet.models.FaucetResponse
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
/**
* Interface to the faucet service. The faucet is used in order to register new BitShares accounts.
*/
interface FaucetService {
@GET("/")
fun checkStatus(): Call<ResponseBody>
@Headers("Content-Type: application/json")
@POST("/api/v1/accounts")
fun registerPrivateAccount(@Body faucetRequest: FaucetRequest): Call<FaucetResponse>
}

View file

@ -0,0 +1,82 @@
package cy.agorise.bitsybitshareswallet.network;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
public class ServiceGenerator{
public static String API_BASE_URL;
private static HttpLoggingInterceptor logging;
private static OkHttpClient.Builder httpClient;
private static Retrofit.Builder builder;
private static HashMap<Class<?>, Object> Services;
public ServiceGenerator(String apiBaseUrl) {
API_BASE_URL= apiBaseUrl;
logging = new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY);
httpClient = new OkHttpClient.Builder().addInterceptor(logging);
builder = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(getGson()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
Services = new HashMap<Class<?>, Object>();
}
/**
* Customizes the Gson instance with specific de-serialization logic
*/
private Gson getGson(){
GsonBuilder builder = new GsonBuilder();
return builder.create();
}
public void setCallbackExecutor(Executor executor){
builder.callbackExecutor(executor);
}
public static <T> void setService(Class<T> klass, T thing) {
Services.put(klass, thing);
}
public <T> T getService(Class<T> serviceClass) {
T service = serviceClass.cast(Services.get(serviceClass));
if (service == null) {
service = createService(serviceClass);
setService(serviceClass, service);
}
return service;
}
public static <S> S createService(Class<S> serviceClass) {
httpClient.interceptors().add(new Interceptor() {
@Override
public okhttp3.Response intercept(Interceptor.Chain chain) throws IOException {
okhttp3.Request original = chain.request();
// Request customization: add request headers
okhttp3.Request.Builder requestBuilder = original.newBuilder().method(original.method(), original.body());
okhttp3.Request request = requestBuilder.build();
return chain.proceed(request);
}
});
httpClient.readTimeout(5, TimeUnit.MINUTES);
httpClient.connectTimeout(5, TimeUnit.MINUTES);
OkHttpClient client = httpClient.build();
Retrofit retrofit = builder.client(client).build();
return retrofit.create(serviceClass);
}
}

View file

@ -16,8 +16,8 @@ class AssetRepository internal constructor(context: Context) {
mAssetDao = db!!.assetDao()
}
fun getAll(): LiveData<List<Asset>> {
return mAssetDao.getAll()
fun getAllNonZero(): LiveData<List<Asset>> {
return mAssetDao.getAllNonZero()
}
fun insertAll(assets: List<Asset>) {

View file

@ -1,5 +1,8 @@
package cy.agorise.bitsybitshareswallet.utils
import cy.agorise.graphenej.Asset
import cy.agorise.graphenej.UserAccount
object Constants {
/** Key used to store the number of the last agreed License version */
@ -8,18 +11,54 @@ object Constants {
/** 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
/** Name of the account passed to the faucet as the referrer */
const val FAUCET_REFERRER = "agorise"
/** Faucet URL used to create new accounts */
const val FAUCET_URL = "https://faucet.palmpay.io"
/** The user selected encrypted PIN */
const val KEY_ENCRYPTED_PIN = "key_encrypted_pin"
/** The fee to send in every transfer (0.01%) */
const val FEE_PERCENTAGE = 0.0001
/** The account used to send the fees */
val AGORISE_ACCOUNT = UserAccount("1.2.390320", "agorise")
/** List of assets symbols that send fee to Agorise when sending a transaction (BTS and smartcoins only) */
val assetsWhichSendFeeToAgorise = setOf(
"1.3.0", // BTS
"1.3.113", // CNY
"1.3.121", // USD
"1.3.1325", // RUBLE
"1.3.120", // EUR
"1.3.103" // BTC
// "1.3.109", // HKD
// "1.3.119", // JPY
// "1.3.102", // KRW
// "1.3.106", // GOLD
// "1.3.105", // SILVER
// "1.3.118", // GBP
// "1.3.115", // CAD
// "1.3.1017", // ARS
// "1.3.114", // MXN
// "1.3.111", // SEK
// "1.3.117", // AUD
// "1.3.116", // CHF
// "1.3.112", // NZD
// "1.3.110", // RUB
// "1.3.2650", // XCD
// "1.3.107", // TRY
// "1.3.108" // SGD
)
/**
* LTM accounts come with an expiration date expressed as this string.
* This is used to recognize such accounts from regular ones.

View file

@ -0,0 +1,56 @@
package cy.agorise.bitsybitshareswallet.utils
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.util.regex.Pattern
/**
* Creates an enabled state, by enabling the button and using the given [colorResource] to color it.
*/
fun FloatingActionButton.enable(colorResource: Int) {
this.isEnabled = true
this.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this.context, colorResource))
}
/**
* Creates a disabled state, by disabling the button and using the given [colorResource] to color it.
*/
fun FloatingActionButton.disable(colorResource: Int) {
this.isEnabled = false
this.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this.context, colorResource))
}
/**
* Easily create a toast message with less boilerplate code
*/
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, message, duration).show()
}
/**
* Verifies that the current string contains at least one digit
*/
fun String.containsDigits(): Boolean {
return Pattern.matches("\\d", this)
}
/**
* Verifies that the current string contains at least one vowel
*/
fun String.containsVowels(): Boolean {
return Pattern.matches("[aeiou]", this)
}
/**
* Allows to hide the Keyboard from any view
*/
fun View.hideKeyboard(){
val inputMethodManager = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(this.windowToken, 0)
}

View file

@ -9,7 +9,7 @@ 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()
internal fun getAllNonZero(): LiveData<List<Asset>> {
return mRepository.getAllNonZero()
}
}

View file

@ -0,0 +1,79 @@
package cy.agorise.bitsybitshareswallet.views
import android.app.DatePickerDialog
import android.app.Dialog
import android.os.Bundle
import android.os.Message
import android.widget.DatePicker
import androidx.fragment.app.DialogFragment
import cy.agorise.bitsybitshareswallet.fragments.FilterOptionsDialog
import java.util.*
class DatePickerFragment : DialogFragment(), DatePickerDialog.OnDateSetListener {
companion object {
const val TAG = "DatePickerFragment"
const val KEY_WHICH = "key_which"
const val KEY_CURRENT = "key_current"
const val KEY_MAX = "key_max"
fun newInstance(
which: Int, currentTime: Long, maxTime: Long,
handler: FilterOptionsDialog.DatePickerHandler
): DatePickerFragment {
val f = DatePickerFragment()
val bundle = Bundle()
bundle.putInt(KEY_WHICH, which)
bundle.putLong(KEY_CURRENT, currentTime)
bundle.putLong(KEY_MAX, maxTime)
f.arguments = bundle
f.setHandler(handler)
return f
}
}
private var which: Int = 0
private var mHandler: FilterOptionsDialog.DatePickerHandler? = null
fun setHandler(handler: FilterOptionsDialog.DatePickerHandler) {
mHandler = handler
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
which = arguments!!.getInt(KEY_WHICH)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val currentTime = arguments!!.getLong(KEY_CURRENT)
val maxTime = arguments!!.getLong(KEY_MAX)
// Use the current date as the default date in the picker
val calendar = Calendar.getInstance()
calendar.timeInMillis = currentTime
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val day = calendar.get(Calendar.DAY_OF_MONTH)
// Create a new instance of DatePickerDialog and return it
val datePicker = DatePickerDialog(activity!!, this, year, month, day)
// Set maximum date allowed to today
datePicker.datePicker.maxDate = maxTime
return datePicker
}
override fun onDateSet(view: DatePicker, year: Int, month: Int, day: Int) {
val msg = Message.obtain()
msg.arg1 = which
val calendar = GregorianCalendar()
calendar.set(year, month, day)
val bundle = Bundle()
bundle.putLong(FilterOptionsDialog.KEY_TIMESTAMP, calendar.time.time)
msg.data = bundle
mHandler!!.sendMessage(msg)
}
}

View file

@ -1,43 +0,0 @@
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;
}
}

View file

@ -0,0 +1,37 @@
package cy.agorise.bitsybitshareswallet.views
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import com.google.android.material.textfield.TextInputEditText
import cy.agorise.bitsybitshareswallet.utils.hideKeyboard
/**
* A TextInputEditText that hides the keyboard when the focus is removed from it and also lets you
* use actions ("Done", "Go", etc.) on multi-line edits.
*/
class MyTextInputEditText(context: Context?, attrs: AttributeSet?) : TextInputEditText(context, attrs){
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
val connection = super.onCreateInputConnection(outAttrs)
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 connection
}
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(focused, direction, previouslyFocusedRect)
if (!focused) this.hideKeyboard()
}
}

View file

@ -3,5 +3,5 @@
<translate android:fromXDelta="-100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
android:duration="300"/>
</set>

View file

@ -3,5 +3,5 @@
<translate android:fromXDelta="100%" android:toXDelta="0%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
android:duration="300"/>
</set>

View file

@ -3,5 +3,5 @@
<translate android:fromXDelta="0%" android:toXDelta="-100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
android:duration="300"/>
</set>

View file

@ -3,5 +3,5 @@
<translate android:fromXDelta="0%" android:toXDelta="100%"
android:fromYDelta="0%" android:toYDelta="0%"
android:duration="700"/>
android:duration="300"/>
</set>

View file

@ -69,7 +69,7 @@
<objectAnimator
android:duration="0"
android:propertyName="elevation"
android:valueTo="6dp"
android:valueTo="3dp"
android:valueType="floatType" />
</set>
</item>

View 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,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent"/>
<corners android:radius="4dp"/>
<stroke android:color="@color/colorAccent" android:width="1dp"/>
</shape>

View file

@ -3,5 +3,5 @@
<corners
android:bottomLeftRadius="60dp"
android:topLeftRadius="60dp" />
<solid android:color="#5fcaff" />
<solid android:color="#88DC473A" />
</shape>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<corners
android:bottomLeftRadius="60dp"
android:topLeftRadius="60dp" />
<solid android:color="@color/superLightGray" />
</shape>

View file

@ -14,7 +14,7 @@
android:id="@+id/tvTransactionDirection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Transactions"
android:text="@string/title_transactions"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="parent"/>
@ -31,21 +31,21 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="All"/>
android:text="@string/text__all"/>
<RadioButton
android:id="@+id/rbTransactionSent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Sent"/>
android:text="@string/text__sent"/>
<RadioButton
android:id="@+id/rbTransactionReceived"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Received"/>
android:text="@string/text__received"/>
</RadioGroup>
@ -55,7 +55,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Date Range"
android:text="@string/text__date_range"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="@id/cbDateRange"
app:layout_constraintStart_toStartOf="parent"/>
@ -65,7 +65,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_different_topic"
android:text="All"
android:text="@string/text__all"
app:layout_constraintTop_toBottomOf="@id/rgTransactionDirection"
app:layout_constraintEnd_toEndOf="parent"/>
@ -100,40 +100,52 @@
</LinearLayout>
<!-- Cryptocurrency -->
<!-- Asset -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Cryptocurrency"
android:text="@string/text__asset"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="@id/cbCryptocurrency"
app:layout_constraintTop_toTopOf="@id/cbAsset"
app:layout_constraintStart_toStartOf="parent"/>
<CheckBox
android:id="@+id/cbCryptocurrency"
android:id="@+id/cbAsset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_different_topic"
android:text="All"
android:text="@string/text__all"
app:layout_constraintTop_toBottomOf="@+id/llDateRange"
app:layout_constraintEnd_toEndOf="parent"/>
<Spinner
android:id="@+id/sCryptocurrency"
android:id="@+id/sAsset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@android:layout/simple_list_item_1"
app:layout_constraintTop_toBottomOf="@id/cbCryptocurrency"/>
app:layout_constraintTop_toBottomOf="@id/cbAsset"/>
<!-- Ignore Agorise Fees -->
<Switch
android:id="@+id/switchAgoriseFees"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:switchPadding="12dp"
android:text="@string/text__ignore_network_fees"
android:textSize="16sp"
android:textColor="?android:textColorSecondary"
app:layout_constraintTop_toBottomOf="@id/sAsset"/>
<!-- Fiat Amount -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Fiat Amount"
android:text="@string/text__fiat_amount"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="@id/cbFiatAmount"
app:layout_constraintStart_toStartOf="parent"/>
@ -142,24 +154,25 @@
android:id="@+id/cbFiatAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:text="All"
android:layout_marginTop="@dimen/spacing_different_topic"
android:text="@string/text__all"
android:enabled="false"
app:layout_constraintTop_toBottomOf="@id/sCryptocurrency"
app:layout_constraintTop_toBottomOf="@id/switchAgoriseFees"
app:layout_constraintEnd_toEndOf="parent"/>
<!--<LinearLayout-->
<!--android:id="@+id/llFiatAmount"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_margin="4dp"-->
<!--android:orientation="horizontal"-->
<!--app:layout_constraintTop_toBottomOf="@id/cbFiatAmount">-->
<LinearLayout
android:id="@+id/llFiatAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/cbFiatAmount">
<!--<TextView-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:text="@string/between"/>-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Between"/>
<!--<faranjit.currency.edittext.CurrencyEditText-->
<!--android:id="@+id/etFromFiatAmount"-->
@ -170,10 +183,10 @@
<!--android:textAlignment="center"-->
<!--app:showSymbol="true"/>-->
<!--<TextView-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:text="@string/and"/>-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="and"/>
<!--<faranjit.currency.edittext.CurrencyEditText-->
<!--android:id="@+id/etToFiatAmount"-->
@ -184,6 +197,6 @@
<!--android:textAlignment="center"-->
<!--app:showSymbol="true"/>-->
<!--</LinearLayout>-->
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/text__to"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/msg__to_explanation"
android:textAppearance="@style/TextAppearance.Bitsy.Body2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:text="@string/text__asset_balance"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/msg__asset_balance_explanation"
android:textAppearance="@style/TextAppearance.Bitsy.Body2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:text="@string/text__memo"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/msg__memo_explanation"
android:textAppearance="@style/TextAppearance.Bitsy.Body2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:text="@string/text__network_fee"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/msg__network_fee_explanation"
android:textAppearance="@style/TextAppearance.Bitsy.Body2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:text="@string/text__qr_code"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/msg__qr_code_explanation"
android:textAppearance="@style/TextAppearance.Bitsy.Body2"/>
</LinearLayout>

View file

@ -2,30 +2,49 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context=".activities.ImportBrainkeyActivity">
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAccountName"
style="@style/Widget.Bitsy.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/text__bitshares_account_name">
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietAccountName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:digits="abcdefghijklmnopqrstuvwxyz0123456789-"
android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPin"
style="@style/Widget.Bitsy.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/spacing_same_topic"
android:hint="@string/text_field__6_digit_pin"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietPin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
@ -34,68 +53,55 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:hint="@string/text_field__confirm_pin"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietPinConfirmation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBrainKey"
style="@style/Widget.Bitsy.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:hint="@string/text__brain_key">
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietBrainKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:gravity="top"
android:lines="4"
android:scrollHorizontally="false"
android:imeOptions="actionDone"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnImport"
style="@style/Widget.Bitsy.Button"
<TextView
android:id="@+id/tvBrainKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/outline_rounded_corners"
android:gravity="center"
android:padding="8dp"
android:layout_marginTop="@dimen/spacing_different_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:text="@string/button__import"/>
tools:text="SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
<View
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/spacing_different_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:background="@color/black"/>
android:layout_height="wrap_content"
android:text="@string/msg__brainkey_info"
android:textAppearance="@style/TextAppearance.Bitsy.Body2"
android:gravity="center"
android:padding="8dp"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_different_topic">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCreate"
style="@style/Widget.Bitsy.Button"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_different_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:text="@string/button__create"
android:enabled="false"/>
android:layout_alignParentEnd="true"
android:layout_marginBottom="4dp"
android:text="@string/button__create"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancel"
style="@style/Widget.Bitsy.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_toStartOf="@id/btnCreate"
android:text="@android:string/cancel"/>
</RelativeLayout>
</LinearLayout>

View file

@ -37,11 +37,14 @@
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabReceiveTransaction"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="auto"
app:fabCustomSize="120dp"
app:maxImageSize="70dp"
app:elevation="@dimen/fab_elevation"
app:borderWidth="0dp"
android:backgroundTint="@color/colorReceive"
android:src="@drawable/ic_receive"
app:layout_constraintTop_toTopOf="parent"
@ -54,7 +57,9 @@
android:id="@+id/tvReceiveTransaction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:text="@string/title_receive"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@+id/fabReceiveTransaction"
@ -64,10 +69,13 @@
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabSendTransactionCamera"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="mini"
android:src="@drawable/ic_camera"
app:elevation="@dimen/fab_elevation"
app:borderWidth="0dp"
android:backgroundTint="@color/colorSend"
app:layout_constraintTop_toTopOf="@id/fabSendTransaction"
app:layout_constraintBottom_toBottomOf="@id/fabSendTransaction"
@ -76,11 +84,14 @@
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabSendTransaction"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fabSize="auto"
app:fabCustomSize="120dp"
app:maxImageSize="70dp"
app:elevation="@dimen/fab_elevation"
app:borderWidth="0dp"
android:backgroundTint="@color/colorSend"
android:src="@drawable/ic_send"
app:layout_constraintTop_toTopOf="parent"
@ -93,7 +104,9 @@
android:id="@+id/tvSendTransaction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:text="@string/title_send"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@+id/fabSendTransaction"
@ -104,7 +117,7 @@
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/gray"
android:background="@color/colorSend"
app:layout_constraintTop_toTopOf="@id/fabSendTransactionCamera"
app:layout_constraintBottom_toBottomOf="@id/fabSendTransactionCamera"
app:layout_constraintStart_toEndOf="@id/fabSendTransactionCamera"

View file

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:focusable="true"
android:focusableInTouchMode="true"
tools:context=".fragments.ImportBrainkeyFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPin"
style="@style/Widget.Bitsy.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:hint="@string/text_field__6_digit_pin"
app:passwordToggleEnabled="true">
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietPin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPinConfirmation"
style="@style/Widget.Bitsy.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:hint="@string/text_field__confirm_pin"
app:passwordToggleEnabled="true">
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietPinConfirmation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
android:singleLine="true"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBrainKey"
style="@style/Widget.Bitsy.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:hint="@string/text__brain_key">
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietBrainKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:gravity="top"
android:lines="4"
android:scrollHorizontally="false"
android:imeOptions="actionDone"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnImport"
style="@style/Widget.Bitsy.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_different_topic"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:text="@string/button__import_existing_account"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_horizontal_margin">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_centerVertical="true"
android:background="@color/black"/>
<TextView
android:id="@+id/tvOR"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:background="?android:colorBackground"
android:layout_centerHorizontal="true"
android:text="@string/text__or"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"/>
</RelativeLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCreate"
style="@style/Widget.Bitsy.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:text="@string/button__create_new_account"/>
</LinearLayout>
</ScrollView>
<TextView
android:id="@+id/tvNetworkStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:text="@string/text__view_network_status"
android:gravity="end|center_vertical"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"
android:drawablePadding="8dp"
android:drawableEnd="@drawable/ic_disconnected"/>
</LinearLayout>

View file

@ -5,6 +5,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true"
tools:context=".fragments.ReceiveTransactionFragment">
<androidx.constraintlayout.widget.Guideline
@ -27,7 +30,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/centeredVerticalGuideline">
<com.google.android.material.textfield.TextInputEditText
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -4,14 +4,17 @@
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".fragments.SendTransactionFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".fragments.SendTransactionFragment">
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/centeredVerticalGuideline"
@ -37,7 +40,7 @@
android:hint="@string/text__to"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -61,7 +64,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/centeredVerticalGuideline">
<com.google.android.material.textfield.TextInputEditText
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -113,7 +116,7 @@
android:hint="@string/text__memo"
app:layout_constraintTop_toBottomOf="@id/tilAmount">
<com.google.android.material.textfield.TextInputEditText
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
android:id="@+id/tietMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -152,9 +155,11 @@
android:layout_height="wrap_content"
app:fabCustomSize="32dp"
app:maxImageSize="20dp"
app:srcCompat="@drawable/ic_camera"
app:borderWidth="0dp"
android:backgroundTint="@color/colorSend"
app:layout_constraintStart_toEndOf="@id/cameraVerticalGuideline"
app:layout_constraintTop_toBottomOf="@+id/tvScan"
app:srcCompat="@drawable/ic_camera" />
app:layout_constraintTop_toBottomOf="@+id/tvScan"/>
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/cameraPreview"
@ -171,7 +176,7 @@
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginEnd="0dp"
android:background="@drawable/send_fab_background"
android:background="@drawable/send_fab_background_disabled"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/viewCamera"
app:layout_constraintBottom_toBottomOf="@id/viewCamera"/>
@ -185,6 +190,7 @@
app:fabCustomSize="90dp"
app:maxImageSize="70dp"
app:srcCompat="@drawable/ic_arrow_forward"
app:borderWidth="0dp"
app:layout_constraintEnd_toEndOf="@id/vSend"
app:layout_constraintTop_toTopOf="@+id/vSend" />

View file

@ -55,7 +55,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:layout_marginEnd="2dp"
android:text="View Network Status"
android:text="@string/text__view_network_status"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Bitsy.Body1"
android:drawableEnd="@drawable/ic_disconnected"/>
@ -83,7 +83,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_same_topic"
android:text="@string/btn__view_and_copy"/>
android:text="@string/button__view_and_copy"/>
<!-- Bugs or Ideas -->

View file

@ -105,7 +105,7 @@
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="@color/lightGray"
android:background="@color/superLightGray"
app:layout_constraintTop_toBottomOf="@id/tvFrom"
app:layout_constraintStart_toEndOf="@id/firstVerticalGuideline"
app:layout_constraintEnd_toStartOf="@id/fourthVerticalGuideline" />
@ -133,7 +133,7 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="@color/lightGray"/>
android:background="@color/superLightGray"/>
</LinearLayout>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_info"
android:icon="@drawable/ic_info"
android:title="@string/title_info"
app:showAsAction="always"/>
</menu>

View file

@ -35,6 +35,11 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"/>
<action
android:id="@+id/license_action"
app:destination="@id/license_dest"/>
</fragment>
<fragment
@ -73,4 +78,48 @@
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/license_dest"
android:name="cy.agorise.bitsybitshareswallet.fragments.LicenseFragment"
android:label="@string/app_name"
tools:layout="@layout/fragment_license">
<action
android:id="@+id/import_brainkey_action"
app:destination="@id/import_brainkey_dest"/>
</fragment>
<fragment
android:id="@+id/import_brainkey_dest"
android:name="cy.agorise.bitsybitshareswallet.fragments.ImportBrainkeyFragment"
android:label="@string/app_name"
tools:layout="@layout/fragment_import_brainkey">
<action
android:id="@+id/create_account_action"
app:destination="@id/create_account_dest"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"/>
<action
android:id="@+id/home_action"
app:popUpTo="@id/home_dest"/>
</fragment>
<fragment
android:id="@+id/create_account_dest"
android:name="cy.agorise.bitsybitshareswallet.fragments.CreateAccountFragment"
android:label="@string/app_name"
tools:layout="@layout/fragment_create_account">
<action
android:id="@+id/home_action"
app:popUpTo="@id/home_dest"/>
</fragment>
</navigation>

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">BiTSy</string>
<!-- License Activity -->
<string name="button__agree">Aceptar</string>
<string name="button__disagree">Cancelar</string>
<!-- Import Brainkey -->
<string name="text_field__6_digit_pin">PIN de 6+ dígitos</string>
<string name="error__pin_too_short">El PIN es muy corto</string>
<string name="text_field__confirm_pin">Confirmar PIN</string>
<string name="error__pin_mismatch">El PIN no concuerda</string>
<string name="text__brain_key">BrainKey</string>
<string name="error__enter_correct_brainkey">Por favor ingresa un brainkey correcto, debe de tener entre 12 y 16 palabras.</string>
<string name="button__import_existing_account">Importar cuenta existente</string>
<string name="text__or">O</string>
<string name="button__create_new_account">Crear cuenta nueva</string>
<string name="error__invalid_brainkey">No se encontraron cuentas controladas por el brainkey dado, por favor revisa tu brainkey por errores de escritura</string>
<string name="error__try_again">Por favor intenta nuevamente después de 5 minutos</string>
<string name="dialog__account_candidates_title">Por favor selecciona una cuenta</string>
<string name="dialog__account_candidates_content">Las claves derivadas de este brainkey son usadas para controlar más de una cuenta, por favor selecciona la cuenta que deseas importar</string>
<!-- Create Account -->
<string name="text__bitshares_account_name">Cuenta de BitShares</string>
<string name="error__read_dict_file">Error al leer el archivo de diccionario</string>
<string name="error__invalid_account_name">La cuenta debe de tener más de 8 caracteres, contener un número o no contener vocales. El guion bajo no es permitido. </string>
<string name="text__verifying_account_availability">Verificando disponibilidad de cuenta…</string>
<string name="error__account_not_available">Cuenta no disponible</string>
<string name="text__account_is_available">Cuenta disponible</string>
<string name="title_error">Error</string>
<string name="error__faucet">El servidor regresó un error. Puede ser causado por una limitación a propósito para rechazar peticiones frecuentes provenientes de la misma dirección IP en un periodo corto de tiempo. Por favor espera 5 minutos e intenta de nuevo, o cambia a una red diferente, por ejemplo de WiFi a celular.</string>
<string name="error__faucet_template">El faucet regresó un error. Msj: %1$s</string>
<string name="error__created_account_not_found">La aplicación no pudo obtener la información sobre la cuenta recién creada</string>
<string name="button__create">Crear</string>
<!-- Home -->
<string name="title_transactions">Transacciones</string>
<string name="title_merchants">Comerciantes</string>
<string name="title_receive">Recibir</string>
<string name="title_balances">Balances</string>
<string name="title_send">Enviar</string>
<string name="title_net_worth">Valor neto</string>
<string name="text__coming_soon">Próximamente</string>
<!-- Transactions -->
<string name="title_search">Buscar</string>
<string name="title_filter">Filtrar</string>
<string name="title_export">Exportar</string>
<!-- Transactions filter options -->
<string name="title_filter_options">Opciones de filtrado</string>
<string name="text__all">Todas</string>
<string name="text__sent">Enviadas</string>
<string name="text__received">Recibidas</string>
<string name="text__date_range">Rango de fechas</string>
<string name="text__fiat_amount">Monto fiat</string>
<string name="text__ignore_network_fees">Ignorar cuotas de red</string>
<string name="button__filter">Filtrar</string>
<!-- Send Transaction -->
<string name="title_info">Info</string>
<string name="text__to">A</string>
<string name="text__amount">Cantidad</string>
<string name="text__memo">Memo</string>
<string name="text__scan_qr">Escanear QR</string>
<string name="error__invalid_account">Cuenta inválida</string>
<string name="error__not_enough_funds">Sin fondos suficientes</string>
<string name="msg__camera_permission_necessary">El permiso de cámara es necesario para leer códigos QR.</string>
<string name="text__transaction_sent">¡Transacción enviada!</string>
<string name="msg__transaction_not_sent">No se pudo enviar la transacción</string>
<!-- Send Transaction info dialog -->
<string name="msg__to_explanation">Escribe la cuenta BitShares de la persona a la que le deseas enviar fondos.\nPor ejemplo: agorise-faucet</string>
<string name="text__asset_balance">Balance del activo</string>
<string name="msg__asset_balance_explanation">Puedes tocar en el balance mostrado para enviar todo lo disponible de ese activo. Al hacerlo el campo Cantidad se llenará automáticamente por ti.</string>
<string name="msg__memo_explanation">Agregar un Memo no es necesario, pero tomar notas sobre porqué enviaste fondos es útil como referencia. Los Memos solamente son visibles para la persona que envía y recibe los fondos.</string>
<string name="text__network_fee">Cuota de red</string>
<string name="msg__network_fee_explanation">La cuota de red está incluida en la cantidad que deseas enviar. Por ejemplo, si deseas enviar 50 BTS, BiTSy en realidad enviará ~50.21 BTS. El agregado 0.21 en este ejemplo es la cuota de transacción de Bitshares más 0.01% para el equipo de dessarrollo de BiTSy(típicamente ~1 centavo).</string>
<string name="text__qr_code">Código QR</string>
<string name="msg__qr_code_explanation">No es necesario que escanees un código QR para enviar fondos, pero ayuda para evitar cometer errores. Una vez que envías fondos desde tu cuenta, se han ido para siempre, así que siempre asegúrate de que la cuenta en el campo “A” es correcta.</string>
<!-- Receive Transaction -->
<string name="text__asset">Activo</string>
<string name="text__other">Otro…</string>
<string name="template__please_send">Por favor enviar: %1$s %2$s</string>
<string name="text__any_amount">Cualquier Cantidad</string>
<string name="template__to">To: %1$s</string>
<string name="msg__invoice_subject">Invoice BiTSy de %1$s</string>
<string name="title_share">Compartir</string>
<string name="text__share_with">Compartir con</string>
<string name="msg__storage__permission__necessary">El permiso de almacenamiento es necesario para compartir imágenes.</string>
<!-- Settings -->
<string name="title_settings">Ajustes</string>
<string name="title__general">General</string>
<string name="msg__close_timer">Cerrar BiTSy automáticamente después de 3 minutos de inactividad</string>
<string name="msg__night_mode">Modo nocturno</string>
<string name="text__view_network_status">Ver Estatus de Red</string>
<string name="title__backup">Respaldo</string>
<string name="msg__brainkey_description">BrainKey. Palabras para respaldar cuenta que pueden ser capturadas o copiadas, pero no editadas.</string>
<string name="msg__brainkey_info">¡Escribe esto! Asegúrate de tener 2 copias de este BrainKey en 2 lugares seguros en caso de incendio o pérdida. ¡La seguridad primero! ¡Cualquiera con acceso a tu BrainKey puede acceder a los fondos en tu cuenta!</string>
<string name="button__copied">Copiado</string>
<string name="button__view_and_copy">Ver y Copiar</string>
<string name="title__bugs_or_ideas">Errores o Ideas?</string>
<string name="msg__bugs_or_ideas">Telegram: https://t.me/Agorise\nKeybase: https://keybase.io/team/Agorise</string>
<string name="title__bitshares_nodes_dialog">Bloque: %1$s</string>
</resources>

View file

@ -6,13 +6,18 @@
<!-- Dark theme -->
<color name="colorBackgroundFloating">#424242</color>
<color name="colorToolbarDark">#424242</color>
<color name="colorStatusBarDark">#212121</color>
<color name="black">#000</color>
<color name="gray">#888</color>
<color name="ppGreen">#139657</color>
<color name="lightGray">#e0e0e0</color>
<color name="lightGray">#aaa</color>
<color name="superLightGray">#d0d0d0</color>
<color name="darkGray">#616161</color>
<color name="colorReceive">#669900</color>
<color name="colorSend">#DC473A</color>
<color name="colorReceive">#388E3C</color>
<color name="colorReceiveDark">#1B5E20</color>
<color name="colorSend">#D32F2F</color>
<color name="colorSendDark">#B71C1C</color>
<color name="semiTransparent">#2888</color>
</resources>

View file

@ -12,6 +12,9 @@
<dimen name="spacing_different_topic">24dp</dimen>
<dimen name="spacing_different_section">40dp</dimen>
<!-- Home -->
<dimen name="fab_elevation">16dp</dimen>
<!-- Initial setup -->
<dimen name="logo_size">180dp</dimen>

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@
<style name="Theme.Bitsy.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar"/>
<!-- Base application dark theme. -->
<style name="Theme.Bitsy.Dark" parent="Theme.MaterialComponents">
<style name="Theme.Bitsy.Dark" parent="Theme.MaterialComponents.Bridge">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>

View file

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.0'
ext.kotlin_version = '1.3.11'
repositories {
google()
jcenter()
@ -11,9 +11,9 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0-rc02'
classpath 'com.android.tools.build:gradle:3.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha08"
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha09"
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.27.0'

@ -1 +1 @@
Subproject commit 101b4a5aba31aa738bc92abeb47e9db66ab5af6a
Subproject commit 4c7c7b29b2d403e8f44a2a955e0ba22169d02a48