- Make an UX improvement to let the user import the account directly from the keyboard when typing the brainkey in the TextField.
- Add the MaterialDialogs library and use it to let the user choose the desired account to import when the brainkey controls more than one account.
This commit is contained in:
parent
22a0735379
commit
847e8a8d7f
8 changed files with 331 additions and 34 deletions
|
@ -22,6 +22,11 @@ android {
|
|||
resValue("string", "PORT_NUMBER", "8082")
|
||||
}
|
||||
}
|
||||
android.packagingOptions {
|
||||
exclude 'lib/x86_64/darwin/libscrypt.dylib'
|
||||
exclude 'lib/x86_64/freebsd/libscrypt.so'
|
||||
exclude 'lib/x86_64/linux/libscrypt.so'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -34,9 +39,6 @@ dependencies {
|
|||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.google.zxing:core:3.3.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
||||
|
@ -44,6 +46,11 @@ dependencies {
|
|||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
implementation 'org.bitcoinj:bitcoinj-core:0.14.3'
|
||||
implementation 'com.google.zxing:core:3.3.1'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation 'com.afollestad.material-dialogs:core:2.0.0-rc1'
|
||||
|
||||
// Android Debug Database
|
||||
debugImplementation 'com.amitshekhar.android:debug-db:1.0.4'
|
||||
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cy.agorise.bitsybitshareswallet">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cy.agorise.bitsybitshareswallet">
|
||||
|
||||
<application
|
||||
android:name=".utils.BitsyApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/bts_logo"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/bts_logo"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Bitsy"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
android:name=".utils.BitsyApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/bts_logo"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/bts_logo"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Bitsy"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity
|
||||
android:name=".activities.SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
android:name=".activities.SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
|
@ -22,13 +22,10 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/title_settings">
|
||||
</activity>
|
||||
<activity android:name=".activities.SendTransactionActivity">
|
||||
</activity>
|
||||
<activity android:name=".activities.ReceiveTransactionActivity">
|
||||
</activity>
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/title_settings"/>
|
||||
<activity android:name=".activities.SendTransactionActivity"/>
|
||||
<activity android:name=".activities.ReceiveTransactionActivity"/>
|
||||
<activity android:name=".activities.MainActivity"/>
|
||||
<activity android:name=".activities.LicenseActivity"/>
|
||||
<activity android:name=".activities.ImportBrainkeyActivity"/>
|
||||
|
|
|
@ -4,15 +4,21 @@ import android.content.ComponentName
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.preference.PreferenceManager
|
||||
import android.os.PersistableBundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.android.NetworkService
|
||||
import cy.agorise.graphenej.api.calls.GetFullAccounts
|
||||
import java.util.ArrayList
|
||||
import cy.agorise.graphenej.api.android.RxBus
|
||||
import cy.agorise.graphenej.models.FullAccountDetails
|
||||
import cy.agorise.graphenej.models.HistoryOperationDetail
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
/**
|
||||
* Class in charge of managing the connection to graphenej's NetworkService
|
||||
|
@ -23,6 +29,11 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
|
|||
|
||||
private val mHandler = Handler()
|
||||
|
||||
// Disposable returned at the bus subscription
|
||||
private var mDisposable: Disposable? = null
|
||||
|
||||
private var storedOpCount: Long = -1
|
||||
|
||||
/* Network service connection */
|
||||
protected var mNetworkService: NetworkService? = null
|
||||
|
||||
|
@ -31,6 +42,48 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
|
|||
*/
|
||||
private var mShouldUnbindNetwork: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mDisposable = RxBus.getBusInstance()
|
||||
.asFlowable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { message ->
|
||||
if (message is JsonRpcResponse<*>) {
|
||||
// Generic processing taken care by subclasses
|
||||
handleJsonRpcResponse(message)
|
||||
// Payment detection focused responses
|
||||
if (message.error == null) {
|
||||
// if (message.result is List<*> && (message.result as List<*>).size > 0) {
|
||||
// if ((message.result as List<*>)[0] is FullAccountDetails) {
|
||||
// if (message.id == recurrentAccountUpdateId) {
|
||||
// handleAccountDetails((message.result as List<*>)[0] as FullAccountDetails)
|
||||
// } else if (message.id == postProcessingAccountUpdateId) {
|
||||
// handleAccountUpdate((message.result as List<*>)[0] as FullAccountDetails)
|
||||
// }
|
||||
// }
|
||||
// } else if (message.result is HistoryOperationDetail && message.id == accountOpRequestId) {
|
||||
// handleNewOperations(message.result as HistoryOperationDetail)
|
||||
// }
|
||||
} else {
|
||||
// In case of error
|
||||
Log.e(TAG, "Got error message from full node. Msg: " + message.error.message)
|
||||
Toast.makeText(
|
||||
this@ConnectedActivity,
|
||||
String.format("Error from full node. Msg: %s", message.error.message),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
// } else if (message is ConnectionStatusUpdate) {
|
||||
// handleConnectionStatusUpdate(message)
|
||||
// if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
|
||||
// recurrentAccountUpdateId = -1
|
||||
// accountOpRequestId = -1
|
||||
// isProcessingTx = false
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
|
@ -81,4 +134,16 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
|
|||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
|
@ -1,13 +1,30 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.graphenej.Address
|
||||
import cy.agorise.graphenej.BrainKey
|
||||
import cy.agorise.graphenej.UserAccount
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.calls.GetAccounts
|
||||
import cy.agorise.graphenej.api.calls.GetKeyReferences
|
||||
import cy.agorise.graphenej.models.AccountProperties
|
||||
import cy.agorise.graphenej.models.JsonRpcResponse
|
||||
import kotlinx.android.synthetic.main.activity_import_brainkey.*
|
||||
import org.bitcoinj.core.ECKey
|
||||
import java.util.ArrayList
|
||||
|
||||
class ImportBrainkeyActivity : ConnectedActivity() {
|
||||
private val TAG = "ImportBrainkeyActivity"
|
||||
|
||||
/**
|
||||
* Private variable that will hold an instance of the [BrainKey] class
|
||||
|
@ -34,13 +51,19 @@ class ImportBrainkeyActivity : ConnectedActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_import_brainkey)
|
||||
|
||||
// Custom event to activate import account from the keyboard
|
||||
tietBrainKey.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
importAccount()
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
|
||||
btnImport.setOnClickListener { importAccount() }
|
||||
}
|
||||
|
||||
private fun importAccount() {
|
||||
val trimmedBrainKey = tietBrainKey.text!!.toString().trim { it <= ' ' }
|
||||
tietBrainKey.setText(trimmedBrainKey)
|
||||
|
||||
tilPin.isErrorEnabled = false
|
||||
tilPinConfirmation.isErrorEnabled = false
|
||||
tilBrainKey.isErrorEnabled = false
|
||||
|
@ -52,12 +75,166 @@ class ImportBrainkeyActivity : ConnectedActivity() {
|
|||
else if (tietBrainKey.text!!.isEmpty() || !tietBrainKey.text.toString().contains(" "))
|
||||
tilBrainKey.error = getString(R.string.error__enter_correct_brainkey)
|
||||
else {
|
||||
val brainKey = tietBrainKey.text.toString().split(" ")
|
||||
if (brainKey.size in 12..16) {
|
||||
// TODO verify brainkey
|
||||
} else
|
||||
val brainKey = tietBrainKey.text.toString()
|
||||
if (brainKey.split(" ").size in 12..16)
|
||||
verifyBrainKey(false)
|
||||
else
|
||||
tilBrainKey.error = getString(R.string.error__enter_correct_brainkey)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that will verify the provided brain key, and if valid will retrieve the account information
|
||||
* associated to the user id.
|
||||
*
|
||||
* This method will perform a network lookup to look which accounts use the public key associated
|
||||
* with the user provided brainkey.
|
||||
*
|
||||
* Some sources use brainkeys in capital letters, while others use lowercase. The activity should
|
||||
* initially call this method with the 'switchCase' parameter as false, in order to try the
|
||||
* brainkey as it was provided by the user.
|
||||
*
|
||||
* But in case this lookup fails, it is expected that the activity makes another attempt. This time
|
||||
* with the 'switchCase' argument set to true.
|
||||
*
|
||||
* If both attempts fail, then we can be certain that the provided brainkey is not currently
|
||||
* associated with any account.
|
||||
*
|
||||
* @param switchCase Whether to switch the case used in the brainkey or not.
|
||||
*/
|
||||
private fun verifyBrainKey(switchCase: Boolean) {
|
||||
//showDialog("", getString(R.string.importing_your_wallet))
|
||||
val brainKey = tietBrainKey.text.toString()
|
||||
// Should we switch the brainkey case?
|
||||
if (switchCase) {
|
||||
if (Character.isUpperCase(brainKey.toCharArray()[brainKey.length - 1])) {
|
||||
// If the last character is an uppercase, we assume the whole brainkey
|
||||
// was given in capital letters and turn it to lowercase
|
||||
getAccountFromBrainkey(brainKey.toLowerCase())
|
||||
} else {
|
||||
// Otherwise we turn the whole brainkey to capital letters
|
||||
getAccountFromBrainkey(brainKey.toUpperCase())
|
||||
}
|
||||
} else {
|
||||
// If no case switching should take place, we perform the network call with
|
||||
// the brainkey as it was provided to us.
|
||||
getAccountFromBrainkey(brainKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that will send a network request asking for all the accounts that make use of the
|
||||
* key derived from a give brain key.
|
||||
*
|
||||
* @param brainKey The brain key the user has just typed
|
||||
*/
|
||||
private fun getAccountFromBrainkey(brainKey: String) {
|
||||
mBrainKey = BrainKey(brainKey, 0)
|
||||
val address = Address(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
|
||||
Log.d(TAG, String.format("Brainkey would generate address: %s", address.toString()))
|
||||
keyReferencesRequestId = mNetworkService!!.sendMessage(GetKeyReferences(address), GetKeyReferences.REQUIRED_API)
|
||||
}
|
||||
|
||||
override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) {
|
||||
Log.d(TAG, "handleResponse.Thread: " + Thread.currentThread().name)
|
||||
if (response.id == keyReferencesRequestId) {
|
||||
val resp = response.result as List<List<UserAccount>>
|
||||
val accountList: List<UserAccount> = resp[0].distinct()
|
||||
if (accountList.isEmpty() && mKeyReferencesAttempts == 0) {
|
||||
mKeyReferencesAttempts++
|
||||
verifyBrainKey(true)
|
||||
} else {
|
||||
if (accountList.isEmpty()) {
|
||||
//hideDialog()
|
||||
Toast.makeText(applicationContext, R.string.error__invalid_brainkey, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
if (accountList.size == 1) {
|
||||
// If we only found one account linked to this key, then we just proceed
|
||||
// trying to find out the account name
|
||||
mUserAccount = accountList[0]
|
||||
getAccountsRequestId =
|
||||
mNetworkService!!.sendMessage(GetAccounts(mUserAccount), GetAccounts.REQUIRED_API)
|
||||
} else {
|
||||
// If we found more than one account linked to this key, we must also
|
||||
// find out the account names, but the procedure is a bit different in
|
||||
// that after having those, we must still ask the user to decide which
|
||||
// account should be imported.
|
||||
mUserAccountCandidates = accountList
|
||||
getAccountsRequestId = mNetworkService!!.sendMessage(
|
||||
GetAccounts(mUserAccountCandidates),
|
||||
GetAccounts.REQUIRED_API
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (response.id == getAccountsRequestId) {
|
||||
val accountPropertiesList = response.result as List<AccountProperties>
|
||||
if (accountPropertiesList.size > 1) {
|
||||
val candidates = ArrayList<String>()
|
||||
for (accountProperties in accountPropertiesList) {
|
||||
candidates.add(accountProperties.name)
|
||||
}
|
||||
// hideDialog()
|
||||
MaterialDialog(this)
|
||||
.title(R.string.dialog__account_candidates_title)
|
||||
.message(R.string.dialog__account_candidates_content)
|
||||
.listItems(items = candidates) { dialog, index, _ ->
|
||||
if (index >= 0) {
|
||||
// If one account was selected, we keep a reference to it and
|
||||
// store the account properties
|
||||
// TODO make sure this is reached
|
||||
mUserAccount = mUserAccountCandidates!![index]
|
||||
onAccountSelected(accountPropertiesList[index])
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
.negativeButton(android.R.string.cancel) { mKeyReferencesAttempts = 0 }
|
||||
.show()
|
||||
} else if (accountPropertiesList.size == 1) {
|
||||
onAccountSelected(accountPropertiesList[0])
|
||||
} else {
|
||||
Toast.makeText(applicationContext, R.string.error__try_again, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) {
|
||||
Log.d(TAG, "handleConnectionStatusUpdate. code: " + connectionStatusUpdate.updateCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called internally once an account has been detected. This method will store internally
|
||||
* the following details:
|
||||
*
|
||||
* - Account name in the database
|
||||
* - Account authorities in the database
|
||||
* - The current account id in the shared preferences
|
||||
*
|
||||
* @param accountProperties Account properties object
|
||||
*/
|
||||
private fun onAccountSelected(accountProperties: AccountProperties) {
|
||||
mUserAccount!!.name = accountProperties.name
|
||||
|
||||
Toast.makeText(this, "Account: "+accountProperties.name, Toast.LENGTH_SHORT).show()
|
||||
|
||||
val password = tietPin.text!!.toString()
|
||||
|
||||
// Stores the accounts this key refers to
|
||||
// database.putOwnedUserAccounts(applicationContext, mUserAccount, password)
|
||||
|
||||
// Stores the id of the currently active user account
|
||||
// PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
// .edit()
|
||||
// .putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId)
|
||||
// .apply()
|
||||
|
||||
// Trying to store all possible authorities (owner, active and memo)
|
||||
// for (i in 0..2) {
|
||||
// mBrainKey.setSequenceNumber(i)
|
||||
// saveAccountAuthorities(mBrainKey, accountProperties)
|
||||
// }
|
||||
|
||||
// TODO move to MainActivity
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@ object Constants {
|
|||
/** Version of the currently used license */
|
||||
const val CURRENT_LICENSE_VERSION = 1
|
||||
|
||||
/** 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
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package cy.agorise.bitsybitshareswallet.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
// An EditText that lets you use actions ("Done", "Go", etc.) on multi-line edits.
|
||||
public class MyTextInputEditText extends TextInputEditText
|
||||
{
|
||||
public MyTextInputEditText(Context context)
|
||||
{
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MyTextInputEditText(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MyTextInputEditText(Context context, AttributeSet attrs, int defStyle)
|
||||
{
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
InputConnection connection = super.onCreateInputConnection(outAttrs);
|
||||
int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION;
|
||||
if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) {
|
||||
// clear the existing action
|
||||
outAttrs.imeOptions ^= imeActions;
|
||||
// set the DONE action
|
||||
outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
|
||||
}
|
||||
if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
|
||||
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@
|
|||
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||
android:hint="@string/text__brain_key">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<cy.agorise.bitsybitshareswallet.views.MyTextInputEditText
|
||||
android:id="@+id/tietBrainKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -16,6 +16,11 @@
|
|||
<string name="error__enter_correct_brainkey">Please enter correct brainkey, it should have between 12 and 16 words.</string>
|
||||
<string name="button__import">Import</string>
|
||||
<string name="button__create">Create</string>
|
||||
<!-- TODO improve below error explanation -->
|
||||
<string name="error__invalid_brainkey">Invalid account, please check your brainkey for typing errors</string>
|
||||
<string name="error__try_again">Please try again after 5 minutes</string>
|
||||
<string name="dialog__account_candidates_title">Please select an account</string>
|
||||
<string name="dialog__account_candidates_content">The keys derived from this brainkey seem to be used to control more than one account, please select which account you wish to import</string>
|
||||
|
||||
<!-- Main Activity -->
|
||||
<string name="title_activity_main">MainActivity</string>
|
||||
|
|
Loading…
Reference in a new issue