diff --git a/.gitignore b/.gitignore index 18de017..0054ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,7 @@ app/fabric.properties # Allocation tracker /captures/* + +# Google services info +app/google-services.json + diff --git a/app/build.gradle b/app/build.gradle index c6a5677..65bfcac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b243968..1dd134f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,8 +44,6 @@ - -

                  

BiTSy Terms and Conditions of Use

 

BiTSy is a software Application (also known as an “App”) developed by the International Business Company “AGORISE, LTD.”, which has been established under Cypriot law. Any person wishing to use this App is obliged to accept the following Terms and Conditions of Use, before any such use:

 

Part I - Terminology

 

The following Terminology applies to these Terms and Conditions of Use (hereinafter referred to as “Terms”), the Privacy and Transparency Statement, and all other agreements between You and Us:

  • The terms “Client”, “Customer”, “Merchant”, “User”, “You” and “Your” refer to you, the person accessing the BiTSy software Application (hereinafter referred to as “App”) and hereby accepting our Terms.
  • The terms “Agorise”, “The Company”, “Our”, “Ourselves”, “We” and “Us” collectively refer to the App and to its owners, freelancers, developers, designers, contractors, directors, officers, employees, agents, insurers, suppliers, and attorneys.
  • The term “Party” refers to either You or Us.
  • In all the above-mentioned Terms, unless otherwise specified, words using the singular include the plural and vice versa and words using gender include all genders.
  • The terms “digital asset”, “asset”, “coin”, “cryptocurrency”, “ledger entry”, “altcoin” and “token” refer to blockchain-based software ledger data entries.
  • The term “Blockchain” refers to a chain of blocks, processed by hundreds of computers around the world. A blockchain (like “Bitshares”) is a decentralized network of computers, an immutable distributed ledger. As each block of transactions is confirmed every 3 (three) seconds, it is hashed together with the latest block in the chain, forming the next block in the chain. Hence the term blockchain.
  • The term “Ledger” refers to a digital version of the “big book” of blocks of transactions taking place on a blockchain. The Distributed ledger keeps track of all transactions on a blockchain network.
  • The name “Morphit” refers to the name chosen by our Company for the Bridge.
  • The term “Bridge” refers to a company or autonomous software (like the “Morphit” feature of our App) which converts/morphs one cryptocurrency into another by way of seeking the best possible price for the Customer by using various third-party exchanges.
  • The terms “Teller” or “Gateway” refers to a person or company (or autonomous software) that converts “fiat” (USD, EUR, CNY, etc) into cryptocurrencies, and vice-versa, whether by cash, card, Swift or SEPA, etc.
  • The term “Multisig” refers to Multiple Signatories. Multiple people or accounts that are assigned to validate a transaction before it can be allowed to be processed, and entered into a block on the chain. Typically, two or more people sign the transaction with their private key (Public Key Infrastructure (PKI)) which will approve the transaction to be sent.
  • For the term “Bitshares” more information can usually be found at the website: http://www.bitshares.org
  • The term “Coin” refers to a digital token. It is underlined that this term is still loosely defined in legislation. It’s a unit of value (if it has any value at all, as determined by the free market) which is running on a blockchain.
  • The term “BTS” refers to the core token that the Bitshares platform uses to determine fair-market values for tokens.

 

Part II - Terms and Conditions of Use

 

By using the App, you represent and warrant that you are:

  1. at least 18 (eighteen) years old and have full capacity to contract under the applicable law;
  2. only transacting with the App with legally-obtained funds that belong to you;
  3. not furthering, performing, undertaking, engaging in, aiding, or abetting any unlawful activity through your relationship with Us or through your use of the App (for example: money laundering, etc); and,
  4. comporting with and obeying all applicable laws.

 

We reserve the right to terminate your access to the App for any breach or our Terms, in our sole and absolute discretion, if the App ceases to exist and/or following a decision of the Company. Use of the App is void where prohibited by applicable law.

 

1. Terms

 

1.1 By accessing the App, you agree to be bound by our Terms, all applicable laws and regulations in Cyprus, and you agree that you are responsible for compliance with, and that you are compliant with applicable law.

 

1.2 If you do not agree with any of our Terms, you are prohibited from using or accessing the App. Your only recourse is to stop using the App. Any use of the App after accepting these Terms is considered to be a deemed acceptance of our Terms, as they may be modified and amended from time to time.

 

1.3 The materials contained in the App are protected by applicable copyright and trademark laws in Cyprus and EU and international treaties. The updates of our Terms, as they appear on our website, take into account the legislative modifications.

 

1.4 By accepting our Terms, you expressly accept that data of transactions made when accessing the App might be exported outside the jurisdiction in which you reside or are located when you access the App. This export is an inherent part of our App and is necessary for its functioning and does not include personal data of Users in any way.

 

1.5 By using any of the third-parties linked to within the App (such as Tellers or Gateways), it is not an endorsement of those third-parties, nor is it a guarantee of any kind that using those third-parties will not result in loss of funds or other damages. Use those third-parties at your own risk.

 

2. Limitations

 

The use of this App may carry financial risk for any User, and is to be used as an experimental software utility only. In no event shall We be liable or responsible for any damages, claims, applications, losses, injuries, delays, accidents, costs, business interruption costs, or other expenses (including, without limitation, attorneys’ fees or the costs of any claim or suit), nor for any incidental, direct, general, indirect, special, punitive, exemplary, or consequential damages, loss of goodwill or business profits, loss of cryptocurrency or digital assets, work stoppage, computer or device failure or malfunction, or any other commercial or other losses directly or indirectly arising out of or related to:

  1. our Terms;
  2. the Privacy and Transparency Statement;
  3. any service We provide;
  4. the use of the App;
  5. any use of your digital assets or cryptocurrency with the App by any other party not authorized by you (collectively, all of the foregoing items shall be referred to herein as “Losses”).

 

We are hereby released by You from liability for any kind of the above-mentioned Losses. We disclaim any and all warranties or guarantees, including any warranty of merchantability and warranty of fitness for any particular purpose.

 

The foregoing limitations of liability shall apply whether the alleged liability or Losses are based on contract, negligence, tort, strict liability, or any other basis, even if We have been advised of or should have known of the possibility of such losses and damages, and without regard to the success or effectiveness of other remedies. Notwithstanding anything else in our Terms, in no event shall the combined aggregate liability for any Loss hereunder exceed € 50.00 (fifty euros).

 

3. Prices, Exchange Rates, Confirmations and Network Fees

 

3.1 Users are hereby informed that Cryptocurrencies and digital assets in general are highly experimental and risky. Our App attempts to provide accurate price and exchange rate information, but this information is as well highly volatile and can change quickly without Users or Us necessarily being aware of these changes.

 

3.2 The exchange rate that the Customer pays (if applicable) is calculated at the moment the App presents an amount due. Our App will always seek out the best price.

3.3 Confirmations on the Bitshares blockchain are 3 seconds on average, but may fluctuate if there is a network issue.

 

3.4 The Bitshares blockchain charges a small fee for every transaction and must be paid to the Bitshares blockchain. Additionally, a fee of 0.01% is charged which covers the cost of App development, maintenance and improvements. Both of those fees (typically around 1 cent, USD) are charged automatically upon transaction execution and are subject to change.

 

4. Returns and Refund Policy

 

4.1 Cryptocurrencies, tokens, and digital assets are, by their nature, generally irreversible, and their exchange rates are highly volatile and transitory. Once the User’s asset has been transmitted to the User’s address, no refund is possible, even if the wrong address was provided to the App. All sales after transmission are final and the Company is not in position to reverse or correct the process already undertaken by the App.

 

4.2 Surety can be 100% refunded at any time from within the App itself. With no pre-funded bridge Surety however, only EOS or Graphene-based cryptocurrencies should be accepted (such as Steem and/or Bitshares tokens).

 

5. Governing Law

 

5.1 These Terms are governed by the laws of Cyprus, and any and all laws applicable therein.

 

5.2 Our Terms are to be treated in all respects as a Cypriot contract. We and You irrevocably and unconditionally attorn to the non-exclusive jurisdiction, venue and forum of the courts of Nicosia, Cyprus, and all courts competent to hear appeals therefrom.

 

6. Permissible Use 

 

The App and all its services may be used only as a mechanism of software ledger entry translation between the User and the Bitshares blockchain. You are prohibited from using the App for the purpose of translating ledger entries with other parties, with the exception of explicit payment for goods and services.

 

7. Terms of Use Modifications 

 

7.1 We may revise our Terms at any time and without notice to you or third parties. By using the App, you agree to be bound by the then-current version of our Terms. All modifications generally apply ex nunc, unless otherwise specified in special cases, if this is justified by the circumstances.

 

7.2 The Terms as they appear on the website of our App are always up-to-date. Therefore, Users are advised to visit the relevant part of the website regularly in order to be informed of the latest changes.

 

8. Costs 

 

8.1 From time to time, We may need to spend time dealing with issues brought to Us by customers, for example requests of information.

 

8.2 Where any customer issue is not caused by our negligence or oversight, We reserve the right to recover reasonable administrative costs spent addressing the customer issue. These costs will be calculated by the Company taking into account the relevant administrative burden caused to it and will be communicated to the Customer.

 

8.3 No response will be sent to the Customer if this entails a payment from his side, before the cost is communicated to him by the email provided by him for this specific purpose.

 

9. Transparency Statement 

 

9.1 No personal data whatsoever of Users is registered, stored or processed in any way; Users are solely liable for keeping the “key” which is necessary to access the interface and in case of loss renders this access impossible. Therefore, the Regulation (EU) No 2016/679 (GDPR) is not applicable within the framework of this App.

 

9.2 We also do not in any way obscure the information that it does request or obtain. Due to the inherent transparency of blockchains, transactions to and from the App are public and easily correlated. Utilizing the App to obscure transactions or assets in any way is futile. Law enforcement has full access to blockchain information that goes in or out of the Bitshares network.

 

9.3 You accept that We will comply willingly with all legal requests for information from it. We reserve the right to provide information to law enforcement personnel and other third parties to answer inquiries; to respond to legal process; to respond to the order of a court of competent jurisdiction and those exercising the court’s authority; and, to protect Ourselves and our users.

 

Copyright 2019 AGORISE, LTD.

An International Business Co.

Cyprus Reg# HE375959

 

IF YOU AGREE TO ALL OF THESE TERMS AND CONDITIONS, PLEASE TAP ON THE GREEN BUTTON BELOW

\ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt index 9021743..c29f9c2 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ConnectedActivity.kt @@ -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() 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) } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/LicenseActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/LicenseActivity.kt deleted file mode 100644 index 5bf6d61..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/LicenseActivity.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/MainActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/MainActivity.kt index ca4c948..21cf69c 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/MainActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/MainActivity.kt @@ -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<*>) { - - } - - /** - * Private method called whenever there's an update to the connection status - * @param connectionStatusUpdate Connection status update. - */ - override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) { - + 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() + } } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/SplashActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/SplashActivity.kt index ccb60d4..5d0b2b0 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/SplashActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/SplashActivity.kt @@ -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() } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/BalancesDetailsAdapter.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/BalancesDetailsAdapter.kt index c939027..4428ddf 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/BalancesDetailsAdapter.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/BalancesDetailsAdapter.kt @@ -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) : ArrayAdapter(context, resource, data) { diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt index 42429da..7d0835a 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/adapters/TransfersDetailsAdapter.kt @@ -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 diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt index b966ae2..8de0030 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/AssetDao.kt @@ -15,6 +15,6 @@ interface AssetDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(assets: List) - @Query("SELECT * FROM assets") - fun getAll(): LiveData> + @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> } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/joins/BalanceDetailDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/joins/BalanceDetailDao.kt index dd7ec5d..e7f1614 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/joins/BalanceDetailDao.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/joins/BalanceDetailDao.kt @@ -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> } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseAccountFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseAccountFragment.kt new file mode 100644 index 0000000..5a10ead --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/BaseAccountFragment.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt new file mode 100644 index 0000000..65e4e92 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ConnectedFragment.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt new file mode 100644 index 0000000..467c7ed --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/CreateAccountFragment.kt @@ -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() + + 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 { + override fun onResponse(call: Call, response: Response) { + // 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, 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) + } + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt index d7c3b94..a513626 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/FilterOptionsDialog.kt @@ -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> { 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) } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/HomeFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/HomeFragment.kt index a531972..be56fa5 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/HomeFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/HomeFragment.kt @@ -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{ user -> - tvAccountName.text = user.name + mUserAccountViewModel.getUserAccount(userId).observe(this, Observer{ 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) } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt similarity index 50% rename from app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt rename to app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt index 135ef7b..690e727 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/activities/ImportBrainkeyActivity.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ImportBrainkeyFragment.kt @@ -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> - val accountList: List = 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 - if (accountPropertiesList.size > 1) { - val candidates = ArrayList() - 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> + val accountList: List = 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 + if (accountPropertiesList.size > 1) { + val candidates = ArrayList() + 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 { + 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) } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/LicenseFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/LicenseFragment.kt new file mode 100644 index 0000000..0d4a584 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/LicenseFragment.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt index 9b81fdb..19923dc 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/ReceiveTransactionFragment.kt @@ -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" - /** 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 RESPONSE_LIST_ASSETS = 1 + private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 - private val OTHER_ASSET = "other_asset" + /** Number of assets to request from the NetworkService to show as suggestions in the AutoCompleteTextView */ + private const val AUTO_SUGGEST_ASSET_LIMIT = 5 + + 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() - /* 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> { 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,50 +198,49 @@ 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] - when (responseType) { - RESPONSE_LIST_ASSETS -> handleListAssets(message.result as List) - } - responseMap.remove(message.id) - } - } else if (message is ConnectionStatusUpdate) { - if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) { - // If we got a disconnection notification, we should clear our response map, since - // all its stored request ids will now be reset - responseMap.clear() + override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) { + if (responseMap.containsKey(response.id)) { + val responseType = responseMap[response.id] + when (responseType) { + RESPONSE_LIST_ASSETS -> handleListAssets(response.result) } + responseMap.remove(response.id) } } - private fun handleListAssets(assetList: List) { - Log.d(TAG, "handleListAssets") - val assets = ArrayList() - for (_asset in assetList) { - val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset( - _asset.objectId, - _asset.symbol, - _asset.precision, - _asset.description ?: "", - _asset.bitassetId ?: "" - ) - - assets.add(asset) + 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() + } + } + + /** + * 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 + Log.d(TAG, "handleListAssets") + val assets = ArrayList() + for (_asset in assetList) { + val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset( + _asset.objectId, + _asset.symbol, + _asset.precision, + _asset.description ?: "", + _asset.bitassetId ?: "" + ) + + assets.add(asset) + } + mAutoSuggestAssetAdapter.setData(assets) + mAutoSuggestAssetAdapter.notifyDataSetChanged() } - mAutoSuggestAssetAdapter.setData(assets) - mAutoSuggestAssetAdapter.notifyDataSetChanged() } private fun updateQR() { @@ -325,14 +324,18 @@ class ReceiveTransactionFragment : Fragment(), ServiceConnection { * @param account Account to pay total */ private fun updateAmountAddressUI(total: AssetAmount, account: String) { - val df = DecimalFormat("####."+("#".repeat(total.asset.precision))) - df.roundingMode = RoundingMode.CEILING - df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) + val 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) + 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 - } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt index 80db5b1..d0551ac 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SendTransactionFragment.kt @@ -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 { - // Camera Permission - private val REQUEST_CAMERA_PERMISSION = 1 + companion object { + private const val TAG = "SendTransactionFragment" - 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 + // Camera Permission + private const val REQUEST_CAMERA_PERMISSION = 1 + // 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() + /** 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] - when (responseType) { - RESPONSE_GET_ACCOUNT_BY_NAME -> handleAccountName(message.result) - RESPONSE_GET_DYNAMIC_GLOBAL_PARAMETERS -> handleDynamicGlobalProperties(message.result) - RESPONSE_GET_REQUIRED_FEES -> handleRequiredFees(message.result) - RESPONSE_BROADCAST_TRANSACTION -> handleBroadcastTransaction(message) - } - responseMap.remove(message.id) - } - } else if (message is ConnectionStatusUpdate) { - if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) { - // If we got a disconnection notification, we should clear our response map, since - // all its stored request ids will now be reset - responseMap.clear() - } + /** 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() } } - private fun handleAccountName(result: Any?) { + override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) { + if (responseMap.containsKey(response.id)) { + val responseType = responseMap[response.id] + when (responseType) { + 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(response.id) + } + } + + 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() + } + } + + /** 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) // 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, 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" - isAmountCorrect = false - } else { - tilAmount.isErrorEnabled = false - isAmountCorrect = true + 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 + } + 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() - 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 - } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt index 5cd77c5..fd81fc4 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/SettingsFragment.kt @@ -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() } } } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt index 817f4ec..3f5ccb0 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/TransactionsFragment.kt @@ -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) } diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetAccount.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetAccount.java new file mode 100644 index 0000000..911a784 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetAccount.java @@ -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; + } +} + diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetRequest.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetRequest.java new file mode 100644 index 0000000..0103ea9 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetRequest.java @@ -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); + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetResponse.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetResponse.java new file mode 100644 index 0000000..c7d7967 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/models/FaucetResponse.java @@ -0,0 +1,10 @@ +package cy.agorise.bitsybitshareswallet.models; + +public class FaucetResponse { + public FaucetAccount account; + public Error error; + + public class Error { + public String[] base; + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/network/FaucetService.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/FaucetService.kt new file mode 100644 index 0000000..c266eb0 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/FaucetService.kt @@ -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 + + @Headers("Content-Type: application/json") + @POST("/api/v1/accounts") + fun registerPrivateAccount(@Body faucetRequest: FaucetRequest): Call +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/network/ServiceGenerator.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/ServiceGenerator.java new file mode 100644 index 0000000..6554677 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/network/ServiceGenerator.java @@ -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, 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, 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 void setService(Class klass, T thing) { + Services.put(klass, thing); + } + + public T getService(Class serviceClass) { + + T service = serviceClass.cast(Services.get(serviceClass)); + if (service == null) { + service = createService(serviceClass); + setService(serviceClass, service); + } + return service; + } + + public static S createService(Class 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); + } +} diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt index 60aa72d..8ddd294 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/AssetRepository.kt @@ -16,8 +16,8 @@ class AssetRepository internal constructor(context: Context) { mAssetDao = db!!.assetDao() } - fun getAll(): LiveData> { - return mAssetDao.getAll() + fun getAllNonZero(): LiveData> { + return mAssetDao.getAllNonZero() } fun insertAll(assets: List) { diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt index f404a82..05d3041 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Constants.kt @@ -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. diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Extensions.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Extensions.kt new file mode 100644 index 0000000..3d80e05 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/Extensions.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/AssetViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/AssetViewModel.kt index 26fb10f..43c201c 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/AssetViewModel.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/AssetViewModel.kt @@ -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> { - return mRepository.getAll() + internal fun getAllNonZero(): LiveData> { + return mRepository.getAllNonZero() } } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/views/DatePickerFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/views/DatePickerFragment.kt new file mode 100644 index 0000000..312d3af --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/views/DatePickerFragment.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/views/MyTextInputEditText.java b/app/src/main/java/cy/agorise/bitsybitshareswallet/views/MyTextInputEditText.java deleted file mode 100644 index d5d17d6..0000000 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/views/MyTextInputEditText.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/views/MyTextInputEditText.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/views/MyTextInputEditText.kt new file mode 100644 index 0000000..6286704 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/views/MyTextInputEditText.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml index e65e525..62d678d 100644 --- a/app/src/main/res/anim/slide_in_left.xml +++ b/app/src/main/res/anim/slide_in_left.xml @@ -3,5 +3,5 @@ + android:duration="300"/> \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml index f387f84..33362c0 100644 --- a/app/src/main/res/anim/slide_in_right.xml +++ b/app/src/main/res/anim/slide_in_right.xml @@ -3,5 +3,5 @@ + android:duration="300"/> diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml index 3dfd3b2..06b83ad 100644 --- a/app/src/main/res/anim/slide_out_left.xml +++ b/app/src/main/res/anim/slide_out_left.xml @@ -3,5 +3,5 @@ + android:duration="300"/> diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml index 63a2226..a271820 100644 --- a/app/src/main/res/anim/slide_out_right.xml +++ b/app/src/main/res/anim/slide_out_right.xml @@ -3,5 +3,5 @@ + android:duration="300"/> \ No newline at end of file diff --git a/app/src/main/res/animator/button_state_list_anim.xml b/app/src/main/res/animator/button_state_list_anim.xml index efbc1fa..819b05e 100644 --- a/app/src/main/res/animator/button_state_list_anim.xml +++ b/app/src/main/res/animator/button_state_list_anim.xml @@ -69,7 +69,7 @@ diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 0000000..af0d4d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/outline_rounded_corners.xml b/app/src/main/res/drawable/outline_rounded_corners.xml new file mode 100644 index 0000000..38a5c24 --- /dev/null +++ b/app/src/main/res/drawable/outline_rounded_corners.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/send_fab_background.xml b/app/src/main/res/drawable/send_fab_background.xml index a8c2324..ea2a04d 100644 --- a/app/src/main/res/drawable/send_fab_background.xml +++ b/app/src/main/res/drawable/send_fab_background.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/send_fab_background_disabled.xml b/app/src/main/res/drawable/send_fab_background_disabled.xml new file mode 100644 index 0000000..a4b4821 --- /dev/null +++ b/app/src/main/res/drawable/send_fab_background_disabled.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_filter_options.xml b/app/src/main/res/layout/dialog_filter_options.xml index 15cc40a..e6352f0 100644 --- a/app/src/main/res/layout/dialog_filter_options.xml +++ b/app/src/main/res/layout/dialog_filter_options.xml @@ -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"/> + android:text="@string/text__sent"/> + android:text="@string/text__received"/> @@ -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 @@ - + + app:layout_constraintTop_toBottomOf="@id/cbAsset"/> + + + + @@ -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"/> - - - - - - - + - - - - + @@ -170,10 +183,10 @@ - - - - + @@ -184,6 +197,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_send_transaction_info.xml b/app/src/main/res/layout/dialog_send_transaction_info.xml new file mode 100644 index 0000000..09e76e2 --- /dev/null +++ b/app/src/main/res/layout/dialog_send_transaction_info.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_import_brainkey.xml b/app/src/main/res/layout/fragment_create_account.xml similarity index 52% rename from app/src/main/res/layout/activity_import_brainkey.xml rename to app/src/main/res/layout/fragment_create_account.xml index 87d1449..1ae07fd 100644 --- a/app/src/main/res/layout/activity_import_brainkey.xml +++ b/app/src/main/res/layout/fragment_create_account.xml @@ -2,30 +2,49 @@ + 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"> + + + + + - - - - - + android:background="@drawable/outline_rounded_corners" + android:gravity="center" + android:padding="8dp" + android:layout_marginTop="@dimen/spacing_different_topic" + tools:text="SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY SAMPLE BRAINKEY" + android:textAppearance="@style/TextAppearance.Bitsy.Body1"/> - + + + - - - - - - - - + android:layout_alignParentEnd="true" + android:layout_marginBottom="4dp" + android:text="@string/button__create"/> + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index d26ae91..0c0e2e4 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -37,11 +37,14 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_license.xml b/app/src/main/res/layout/fragment_license.xml similarity index 100% rename from app/src/main/res/layout/activity_license.xml rename to app/src/main/res/layout/fragment_license.xml diff --git a/app/src/main/res/layout/fragment_receive_transaction.xml b/app/src/main/res/layout/fragment_receive_transaction.xml index fefdfc9..aa79dbc 100644 --- a/app/src/main/res/layout/fragment_receive_transaction.xml +++ b/app/src/main/res/layout/fragment_receive_transaction.xml @@ -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"> - + android:layout_height="match_parent" + tools:context=".fragments.SendTransactionFragment"> + android:focusable="true" + android:focusableInTouchMode="true" + android:clickable="true"> - - - + app:layout_constraintTop_toBottomOf="@+id/tvScan"/> @@ -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" /> diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 905d393..29ba248 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -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"/> diff --git a/app/src/main/res/layout/item_transaction.xml b/app/src/main/res/layout/item_transaction.xml index 967998c..b6e5e7b 100644 --- a/app/src/main/res/layout/item_transaction.xml +++ b/app/src/main/res/layout/item_transaction.xml @@ -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"/> diff --git a/app/src/main/res/menu/menu_send_transaction.xml b/app/src/main/res/menu/menu_send_transaction.xml new file mode 100644 index 0000000..6703b30 --- /dev/null +++ b/app/src/main/res/menu/menu_send_transaction.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 1e2c596..aa1bb35 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -35,6 +35,11 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right"/> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..f1242d8 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,109 @@ + + + BiTSy + + + Aceptar + Cancelar + + + PIN de 6+ dígitos + El PIN es muy corto + Confirmar PIN + El PIN no concuerda + BrainKey + Por favor ingresa un brainkey correcto, debe de tener entre 12 y 16 palabras. + Importar cuenta existente + O + Crear cuenta nueva + No se encontraron cuentas controladas por el brainkey dado, por favor revisa tu brainkey por errores de escritura + Por favor intenta nuevamente después de 5 minutos + Por favor selecciona una cuenta + Las claves derivadas de este brainkey son usadas para controlar más de una cuenta, por favor selecciona la cuenta que deseas importar + + + Cuenta de BitShares + Error al leer el archivo de diccionario + La cuenta debe de tener más de 8 caracteres, contener un número o no contener vocales. El guion bajo no es permitido. + Verificando disponibilidad de cuenta… + Cuenta no disponible + Cuenta disponible + Error + 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. + El faucet regresó un error. Msj: %1$s + La aplicación no pudo obtener la información sobre la cuenta recién creada + Crear + + + Transacciones + Comerciantes + Recibir + Balances + Enviar + Valor neto + Próximamente + + + Buscar + Filtrar + Exportar + + + Opciones de filtrado + Todas + Enviadas + Recibidas + Rango de fechas + Monto fiat + Ignorar cuotas de red + Filtrar + + + Info + A + Cantidad + Memo + Escanear QR + Cuenta inválida + Sin fondos suficientes + El permiso de cámara es necesario para leer códigos QR. + ¡Transacción enviada! + No se pudo enviar la transacción + + + Escribe la cuenta BitShares de la persona a la que le deseas enviar fondos.\nPor ejemplo: agorise-faucet + Balance del activo + 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. + 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. + Cuota de red + 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). + Código QR + 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. + + + Activo + Otro… + Por favor enviar: %1$s %2$s + Cualquier Cantidad + To: %1$s + Invoice BiTSy de %1$s + Compartir + Compartir con + El permiso de almacenamiento es necesario para compartir imágenes. + + + Ajustes + General + Cerrar BiTSy automáticamente después de 3 minutos de inactividad + Modo nocturno + Ver Estatus de Red + Respaldo + BrainKey. Palabras para respaldar cuenta que pueden ser capturadas o copiadas, pero no editadas. + ¡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! + Copiado + Ver y Copiar + Errores o Ideas? + Telegram: https://t.me/Agorise\nKeybase: https://keybase.io/team/Agorise + Bloque: %1$s + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 41ef6c5..91ec463 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,13 +6,18 @@ #424242 + #424242 + #212121 #000 #888 #139657 - #e0e0e0 + #aaa + #d0d0d0 #616161 - #669900 - #DC473A + #388E3C + #1B5E20 + #D32F2F + #B71C1C #2888 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 6f512c5..c262699 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -12,6 +12,9 @@ 24dp 40dp + + 16dp + 180dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 84af3e9..9f05609 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,41 +2,37 @@ BiTSy - -

                  

BiTSy Terms and Conditions of Use

 

BiTSy is a software Application (also known as an “App”) developed by the International Business Company “AGORISE, LTD.”, which has been established under Cypriot law. Any person wishing to use this App is obliged to accept the following Terms and Conditions of Use, before any such use:

 

Part I - Terminology

 

The following Terminology applies to these Terms and Conditions of Use (hereinafter referred to as “Terms”), the Privacy and Transparency Statement, and all other agreements between You and Us:

  • The terms “Client”, “Customer”, “Merchant”, “User”, “You” and “Your” refer to you, the person accessing the BiTSy software Application (hereinafter referred to as “App”) and hereby accepting our Terms.
  • The terms “Agorise”, “The Company”, “Our”, “Ourselves”, “We” and “Us” collectively refer to the App and to its owners, freelancers, developers, designers, contractors, directors, officers, employees, agents, insurers, suppliers, and attorneys.
  • The term “Party” refers to either You or Us.
  • In all the above-mentioned Terms, unless otherwise specified, words using the singular include the plural and vice versa and words using gender include all genders.
  • The terms “digital asset”, “asset”, “coin”, “cryptocurrency”, “ledger entry”, “altcoin” and “token” refer to blockchain-based software ledger data entries.
  • The term “Blockchain” refers to a chain of blocks, processed by hundreds of computers around the world. A blockchain (like “Bitshares”) is a decentralized network of computers, an immutable distributed ledger. As each block of transactions is confirmed every 3 (three) seconds, it is hashed together with the latest block in the chain, forming the next block in the chain. Hence the term blockchain.
  • The term “Ledger” refers to a digital version of the “big book” of blocks of transactions taking place on a blockchain. The Distributed ledger keeps track of all transactions on a blockchain network.
  • The name “Morphit” refers to the name chosen by our Company for the Bridge.
  • The term “Bridge” refers to a company or autonomous software (like the “Morphit” feature of our App) which converts/morphs one cryptocurrency into another by way of seeking the best possible price for the Customer by using various third-party exchanges.
  • The terms “Teller” or “Gateway” refers to a person or company (or autonomous software) that converts “fiat” (USD, EUR, CNY, etc) into cryptocurrencies, and vice-versa, whether by cash, card, Swift or SEPA, etc.
  • The term “Multisig” refers to Multiple Signatories. Multiple people or accounts that are assigned to validate a transaction before it can be allowed to be processed, and entered into a block on the chain. Typically, two or more people sign the transaction with their private key (Public Key Infrastructure (PKI)) which will approve the transaction to be sent.
  • For the term “Bitshares” more information can usually be found at the website: http://www.bitshares.org
  • The term “Coin” refers to a digital token. It is underlined that this term is still loosely defined in legislation. It’s a unit of value (if it has any value at all, as determined by the free market) which is running on a blockchain.
  • The term “BTS” refers to the core token that the Bitshares platform uses to determine fair-market values for tokens.

 

Part II - Terms and Conditions of Use

 

By using the App, you represent and warrant that you are:

  1. at least 18 (eighteen) years old and have full capacity to contract under the applicable law;
  2. only transacting with the App with legally-obtained funds that belong to you;
  3. not furthering, performing, undertaking, engaging in, aiding, or abetting any unlawful activity through your relationship with Us or through your use of the App (for example: money laundering, etc); and,
  4. comporting with and obeying all applicable laws.

 

We reserve the right to terminate your access to the App for any breach or our Terms, in our sole and absolute discretion, if the App ceases to exist and/or following a decision of the Company. Use of the App is void where prohibited by applicable law.

 

1. Terms

 

1.1 By accessing the App, you agree to be bound by our Terms, all applicable laws and regulations in Cyprus, and you agree that you are responsible for compliance with, and that you are compliant with applicable law.

 

1.2 If you do not agree with any of our Terms, you are prohibited from using or accessing the App. Your only recourse is to stop using the App. Any use of the App after accepting these Terms is considered to be a deemed acceptance of our Terms, as they may be modified and amended from time to time.

 

1.3 The materials contained in the App are protected by applicable copyright and trademark laws in Cyprus and EU and international treaties. The updates of our Terms, as they appear on our website, take into account the legislative modifications.

 

1.4 By accepting our Terms, you expressly accept that data of transactions made when accessing the App might be exported outside the jurisdiction in which you reside or are located when you access the App. This export is an inherent part of our App and is necessary for its functioning and does not include personal data of Users in any way.

 

1.5 By using any of the third-parties linked to within the App (such as Tellers or Gateways), it is not an endorsement of those third-parties, nor is it a guarantee of any kind that using those third-parties will not result in loss of funds or other damages. Use those third-parties at your own risk.

 

2. Limitations

 

The use of this App may carry financial risk for any User, and is to be used as an experimental software utility only. In no event shall We be liable or responsible for any damages, claims, applications, losses, injuries, delays, accidents, costs, business interruption costs, or other expenses (including, without limitation, attorneys’ fees or the costs of any claim or suit), nor for any incidental, direct, general, indirect, special, punitive, exemplary, or consequential damages, loss of goodwill or business profits, loss of cryptocurrency or digital assets, work stoppage, computer or device failure or malfunction, or any other commercial or other losses directly or indirectly arising out of or related to:

  1. our Terms;
  2. the Privacy and Transparency Statement;
  3. any service We provide;
  4. the use of the App;
  5. any use of your digital assets or cryptocurrency with the App by any other party not authorized by you (collectively, all of the foregoing items shall be referred to herein as “Losses”).

 

We are hereby released by You from liability for any kind of the above-mentioned Losses. We disclaim any and all warranties or guarantees, including any warranty of merchantability and warranty of fitness for any particular purpose.

 

The foregoing limitations of liability shall apply whether the alleged liability or Losses are based on contract, negligence, tort, strict liability, or any other basis, even if We have been advised of or should have known of the possibility of such losses and damages, and without regard to the success or effectiveness of other remedies. Notwithstanding anything else in our Terms, in no event shall the combined aggregate liability for any Loss hereunder exceed € 50.00 (fifty euros).

 

3. Prices, Exchange Rates, Surety and Confirmations

 

3.1 Users are hereby informed that Cryptocurrencies and digital assets in general are highly experimental and risky. Our App attempts to provide accurate price and exchange rate information, but this information is as well highly volatile and can change quickly without Users or Us necessarily being aware of these changes.

 

3.2 The exchange rate that the Customer pays (if applicable) is calculated at the moment the App presents the amount due to the customer (via the QR Code or NFC signal). Our App will always seek out the best price for the Customer.

3.3 The confirmation time of some cryptocurrencies can be very lengthy. For example, if a customer pays with a slow coin such as the Petro (Venezuela) but pays a minimal miner fee when sending their funds to the App, then that customer’s payment may not be processed by that network for many hours, if at all.

 

Due to this risk, the App offers a Surety feature, allowing You to voluntarily pre-fund a Multisig blockchain account (for example: joes-grocery-morphit) with some Bitshares (BTS) tokens, and then voluntarily share that account with the Morphit bridge algorithm in the App temporarily.

 

This way, the bridge can send the necessary BTS or smartcoins to the Merchant instantly, be protected from customer minimal miner fee or double-spend scams, and confirm the customers "completed" payment so that the customer doesn't have to stand there waiting for their (slow coin) payment to confirm. The customer can then theoretically pay with any supported coin and leave in as little as 3 seconds. Payments in a Bitshares token do not require use of a bridge or surety at all since that chain is so fast. Steem payments don't need surety either, but the bridge is used real quick to convert it to the merchants desired BTS or smartcoin. The merchant can empty their surety account any time they like, or add more funds to it for high volume (such as grocery chains) or larger transactions (such as homes or cars).

 

If however, the customer’s payment is never confirmed by their network, then that amount of Surety that was shared with the bridge will not be released back to You. The bridge will not assume any risk of Your customers payments not clearing.

 

Due to individual blockchain specifications, the Customer payment is typically considered “accepted” by the bridge at two block confirmations. If a bridge had to be used, the bridge will then automatically release the Surety back to you that was shared while the bridge was awaiting those two block confirmations. It is important to note that a payment being broadcast to a blockchain network does not constitute an acceptance by the App of that payment.

 

4. Returns and Refund Policy

 

4.1 Cryptocurrencies, tokens, and digital assets are, by their nature, generally irreversible, and their exchange rates are highly volatile and transitory. Once the User’s asset has been transmitted to the User’s address, no refund is possible, even if the wrong address was provided to the App. All sales after transmission are final and the Company is not in position to reverse or correct the process already undertaken by the App.

 

4.2 Surety can be 100% refunded at any time from within the App itself. With no pre-funded bridge Surety however, only EOS or Graphene-based cryptocurrencies should be accepted (such as Steem and/or Bitshares tokens).

 

5. Governing Law

 

5.1 These Terms are governed by the laws of Cyprus, and any and all laws applicable therein.

 

5.2 Our Terms are to be treated in all respects as a Cypriot contract. We and You irrevocably and unconditionally attorn to the non-exclusive jurisdiction, venue and forum of the courts of Nicosia, Cyprus, and all courts competent to hear appeals therefrom.

 

6. Permissible Use 

 

The App and all its services may be used only as a mechanism of software ledger entry translation between the User and the Bitshares blockchain. You are prohibited from using the App for the purpose of translating ledger entries with other parties, with the exception of explicit payment for goods and services.

 

7. Terms of Use Modifications 

 

7.1 We may revise our Terms at any time and without notice to you or third parties. By using the App, you agree to be bound by the then-current version of our Terms. All modifications generally apply ex nunc, unless otherwise specified in special cases, if this is justified by the circumstances.

 

7.2 The Terms as they appear on the website of our App are always up-to-date. Therefore, Users are advised to visit the relevant part of the website regularly in order to be informed of the latest changes.

 

8. Costs 

 

8.1 From time to time, We may need to spend time dealing with issues brought to Us by customers, for example requests of information.

 

8.2 Where any customer issue is not caused by our negligence or oversight, We reserve the right to recover reasonable administrative costs spent addressing the customer issue. These costs will be calculated by the Company taking into account the relevant administrative burden caused to it and will be communicated to the Customer.

 

8.3 No response will be sent to the Customer if this entails a payment from his side, before the cost is communicated to him by the email provided by him for this specific purpose.

 

9. Transparency Statement 

 

9.1 No personal data whatsoever of Users is registered, stored or processed in any way; Users are solely liable for keeping the “key” which is necessary to access the interface and in case of loss renders this access impossible. Therefore, the Regulation (EU) No 2016/679 (GDPR) is not applicable within the framework of this App.

 

9.2 We also do not in any way obscure the information that it does request or obtain. Due to the inherent transparency of blockchains, transactions to and from the App are public and easily correlated. Utilizing the App to obscure transactions or assets in any way is futile. Law enforcement has full access to blockchain information that goes in or out of the Bitshares network.

 

9.3 You accept that We will comply willingly with all legal requests for information from it. We reserve the right to provide information to law enforcement personnel and other third parties to answer inquiries; to respond to legal process; to respond to the order of a court of competent jurisdiction and those exercising the court’s authority; and, to protect Ourselves and our users.

 

Copyright 2018 AGORISE, LTD.

An International Business Co.

Cyprus Reg# HE375959

 

IF YOU AGREE TO ALL OF THESE TERMS AND CONDITIONS, PLEASE TAP ON THE GREEN BUTTON BELOW

]]>
- Agree Disagree - - Loading Assets… - Performing a series of requests in order to get the complete list of all - existing assets - - Loaded %1$d assets into the database - Assets Loaded - Next, please setup your account… - Next - - + 6+ digits PIN PIN too short Confirm PIN PIN mismatch BrainKey - Please enter correct brainkey, it should have between 12 and 16 - words. - - Import - Create - - Invalid account, please check your brainkey for typing errors + Please enter correct brainkey, it should have 12 or 16 words. + Import existing account + Or + Create new account + No accounts controlled by the given brainkey where found, please check your brainkey for typing errors Please try again after 5 minutes Please select an account - The keys derived from this brainkey seem to be used to control - more than one account, please select which account you wish to import - + The keys derived from this brainkey seem to be used to control more than one account, please select which account you wish to import + + + BitShares account name + Error reading dictionary file + The account name has to either have more than 8 characters, contain a number or have no vowels. The underscore character is also not allowed. + Verifying account availability… + Account not available + Account is available + Error + The server returned an error. This might be due to a limitation purposefully set in place to disallow frequent requests coming from the same IP address in a short time lapse. Please wait 5 minutes and try again, or switch to a different network, for example from wifi to cell. + The faucet returned an error. Msg: %1$s + The app could not retrieve information about the newly created account + Create Transactions @@ -52,35 +48,62 @@ Filter Export - + + Filter options + All + Sent + Received + Date Range + Fiat Amount + Ignore Network fees + Filter + + + Info To Amount Memo Scan QR + Invalid account + Not enough funds + Camera permission is necessary to read QR codes. + Transaction sent! + Unable to send transaction + + + Type in the Bitshares account name of the person you want to send funds to.\nFor example: agorise-faucet + Asset Balance + You can tap on the balance displayed to send all of that asset. Doing so will fill in the Amount field for you. + Entering a Memo is not required, but taking notes on why you sent the funds is nice for future reference. Memos are only visible to you and the person whom you sent funds to. + Network Fee + The Network fee is included in the amount that you want to send. For example, if you want to send 50 BTS, BiTSy will actually send ~50.21 BTS. The added 0.21 in this example is the Bitshares transaction fee plus 0.01% to the BiTSy Developer Team (typically ~1 cent). + QR Code + You do not need to scan someone’s QR to send funds, but it does help you to avoid mistakes. Once you send funds from your account, they are gone forever, so always make sure the account name in the “To” field is correct. + + Asset - Please Pay: %1$s %2$s + Other… + Please Send: %1$s %2$s + Any Amount To: %1$s BiTSy invoice from %1$s Share Share with + Storage permission is necessary to share images. Settings General Automatically close BiTSy after 3 minutes of inactivity Night mode + View Network Status Backup - BrainKey. Account recovery words that can be captured or copied, but not - edited. - - Print this out, or write it down. Anyone with access to your recovery key will - have access to funds within this wallet. - - + BrainKey. Account recovery words that can be captured or copied, but not edited. + Write this down! Be sure you have 2 copies of this BrainKey in 2 secure locations in case of fire or loss. Security First! Anyone with access to your BrainKey can access the funds in your account! + Copied + Bugs or Ideas? - Telegram chat: http://t.me/Agorise\nEmail: Agorise@protonmail.ch\nOpen Source: - https://github.com/Agorise - + Telegram: https://t.me/Agorise\nKeybase: https://keybase.io/team/Agorise Block: %1$s
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a6268bc..115e364 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,7 +17,7 @@