- Simplified Database structure to include only what is going to be used for the wallet.
- Properly distroy NetworkService connection when ConnectedActivty is distroyed to avoid crashes. - Created UserAccountRepository and AuthorityRepository to handle their corresponding database operations in an asynchronous way, because Room enforces database operations to be done in a thread other than the UI thread. - In ImportBrainkeyActivity when a proper brainkey is given save the imported account into the database, put a flag that tells the app there is a current account imported and send the user to MainActivity.
This commit is contained in:
parent
847e8a8d7f
commit
175d48f8c6
14 changed files with 244 additions and 108 deletions
|
@ -47,6 +47,7 @@ dependencies {
|
|||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
implementation 'org.bitcoinj:bitcoinj-core:0.14.3'
|
||||
implementation 'com.moldedbits.r2d2:r2d2:1.0.1'
|
||||
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'
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.content.ServiceConnection
|
|||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.PersistableBundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -84,6 +83,11 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!mDisposable!!.isDisposed) mDisposable!!.dispose()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
|
@ -99,6 +103,16 @@ abstract class ConnectedActivity : AppCompatActivity(), ServiceConnection {
|
|||
// .getLong(Constants.KEY_ACCOUNT_OPERATION_COUNT, -1)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// Unbinding from network service
|
||||
if (mShouldUnbindNetwork) {
|
||||
unbindService(this)
|
||||
mShouldUnbindNetwork = false
|
||||
}
|
||||
// mHandler.removeCallbacks(mCheckMissingPaymentsTask)
|
||||
}
|
||||
|
||||
/**
|
||||
* Task used to perform a redundant payment check.
|
||||
*/
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package cy.agorise.bitsybitshareswallet.activities
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
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 com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import cy.agorise.bitsybitshareswallet.R
|
||||
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.models.Authority
|
||||
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
|
||||
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
|
||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||
import cy.agorise.graphenej.Address
|
||||
import cy.agorise.graphenej.BrainKey
|
||||
import cy.agorise.graphenej.UserAccount
|
||||
import cy.agorise.bitsybitshareswallet.utils.CryptoUtils
|
||||
import cy.agorise.graphenej.*
|
||||
import cy.agorise.graphenej.api.ConnectionStatusUpdate
|
||||
import cy.agorise.graphenej.api.calls.GetAccounts
|
||||
import cy.agorise.graphenej.api.calls.GetKeyReferences
|
||||
|
@ -23,6 +25,9 @@ import kotlinx.android.synthetic.main.activity_import_brainkey.*
|
|||
import org.bitcoinj.core.ECKey
|
||||
import java.util.ArrayList
|
||||
|
||||
// TODO Add method to load the 20? most important assets
|
||||
// TODO add progress bar or something while the user waits for the import response from the node
|
||||
|
||||
class ImportBrainkeyActivity : ConnectedActivity() {
|
||||
private val TAG = "ImportBrainkeyActivity"
|
||||
|
||||
|
@ -179,17 +184,19 @@ class ImportBrainkeyActivity : ConnectedActivity() {
|
|||
MaterialDialog(this)
|
||||
.title(R.string.dialog__account_candidates_title)
|
||||
.message(R.string.dialog__account_candidates_content)
|
||||
.listItems(items = candidates) { dialog, index, _ ->
|
||||
.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
|
||||
// TODO make sure this is reached
|
||||
mUserAccount = mUserAccountCandidates!![index]
|
||||
onAccountSelected(accountPropertiesList[index])
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
.negativeButton(android.R.string.cancel) { mKeyReferencesAttempts = 0 }
|
||||
.positiveButton(android.R.string.ok)
|
||||
.negativeButton(android.R.string.cancel) {
|
||||
mKeyReferencesAttempts = 0
|
||||
}
|
||||
.cancelable(false)
|
||||
.show()
|
||||
} else if (accountPropertiesList.size == 1) {
|
||||
onAccountSelected(accountPropertiesList[0])
|
||||
|
@ -216,25 +223,76 @@ class ImportBrainkeyActivity : ConnectedActivity() {
|
|||
private fun onAccountSelected(accountProperties: AccountProperties) {
|
||||
mUserAccount!!.name = accountProperties.name
|
||||
|
||||
Toast.makeText(this, "Account: "+accountProperties.name, Toast.LENGTH_SHORT).show()
|
||||
val encryptedPIN = CryptoUtils.encrypt(this, tietPin.text!!.toString())
|
||||
|
||||
val password = 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
|
||||
// database.putOwnedUserAccounts(applicationContext, mUserAccount, password)
|
||||
val id = accountProperties.id
|
||||
val name = accountProperties.name
|
||||
val isLTM = accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
|
||||
|
||||
val userAccount = cy.agorise.bitsybitshareswallet.models.UserAccount(id, name, isLTM)
|
||||
|
||||
val userAccountRepository = UserAccountRepository(application)
|
||||
userAccountRepository.insert(userAccount)
|
||||
|
||||
// Stores the id of the currently active user account
|
||||
// PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
// .edit()
|
||||
// .putString(Constants.KEY_CURRENT_ACCOUNT_ID, mUserAccount!!.objectId)
|
||||
// .apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.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)
|
||||
// }
|
||||
// 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
|
||||
|
||||
// TODO move to MainActivity
|
||||
for (i in 0..2) {
|
||||
mBrainKey!!.sequenceNumber = i
|
||||
val publicKey = PublicKey(ECKey.fromPublicOnly(mBrainKey!!.privateKey.pubKey))
|
||||
|
||||
if (ownerAuthority.keyAuths.keys.contains(publicKey)) {
|
||||
addAuthorityToDatabase(accountProperties.id, AuthorityType.OWNER.ordinal, mBrainKey!!)
|
||||
}
|
||||
if (activeAuthority.keyAuths.keys.contains(publicKey)) {
|
||||
addAuthorityToDatabase(accountProperties.id, AuthorityType.ACTIVE.ordinal, mBrainKey!!)
|
||||
}
|
||||
if (options.memoKey == publicKey) {
|
||||
addAuthorityToDatabase(accountProperties.id, AuthorityType.MEMO.ordinal, mBrainKey!!)
|
||||
}
|
||||
}
|
||||
|
||||
// Stores a flag into the SharedPreferences to tell the app there is an active account and there is no need
|
||||
// to show this activity again, until the account is removed.
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit()
|
||||
.putBoolean(Constants.KEY_INITIAL_SETUP_DONE, true)
|
||||
.apply()
|
||||
|
||||
// Send the user to the MainActivity
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given BrainKey encrypted as AuthorityType of userId.
|
||||
*/
|
||||
private fun addAuthorityToDatabase(userId: String, authorityType: Int, brainKey: BrainKey) {
|
||||
val brainKeyWords = brainKey.brainKey
|
||||
val sequenceNumber = brainKey.sequenceNumber
|
||||
|
||||
val encryptedBrainKey = CryptoUtils.encrypt(this, brainKeyWords)
|
||||
val encryptedSequenceNumber = CryptoUtils.encrypt(this, sequenceNumber.toString())
|
||||
|
||||
val authority = Authority(0, userId, authorityType, encryptedBrainKey, encryptedSequenceNumber)
|
||||
|
||||
val authorityRepository = AuthorityRepository(application)
|
||||
authorityRepository.insert(authority)
|
||||
}
|
||||
}
|
|
@ -10,24 +10,20 @@ import cy.agorise.bitsybitshareswallet.models.*
|
|||
Asset::class,
|
||||
Authority::class,
|
||||
Balance::class,
|
||||
BrainKey::class,
|
||||
EquivalentValue::class,
|
||||
Operation::class,
|
||||
Transfer::class,
|
||||
UserAccount::class,
|
||||
UserAccountAuthority::class
|
||||
UserAccount::class
|
||||
], version = 1, exportSchema = false)
|
||||
abstract class BitsyDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun assetDao(): AssetDao
|
||||
abstract fun authorityDao(): AuthorityDao
|
||||
abstract fun balanceDao(): BalanceDao
|
||||
abstract fun brainKeyDao(): BrainKeyDao
|
||||
abstract fun equivalentValueDao(): EquivalentValueDao
|
||||
abstract fun operationDao(): OperationDao
|
||||
abstract fun transferDao(): TransferDao
|
||||
abstract fun userAccountDao(): UserAccountDao
|
||||
abstract fun userAccountAuthorityDao(): UserAccountAuthorityDao
|
||||
|
||||
companion object {
|
||||
|
||||
|
@ -48,9 +44,5 @@ abstract class BitsyDatabase : RoomDatabase() {
|
|||
|
||||
return INSTANCE
|
||||
}
|
||||
|
||||
fun destroyInstance() {
|
||||
INSTANCE = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.models.BrainKey
|
||||
|
||||
@Dao
|
||||
interface BrainKeyDao {
|
||||
@Insert
|
||||
fun insert(brainKey: BrainKey)
|
||||
|
||||
@Query("SELECT * FROM brain_keys")
|
||||
fun getAllBrainKeys(): LiveData<List<BrainKey>>
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.daos
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import cy.agorise.bitsybitshareswallet.models.Authority
|
||||
import cy.agorise.bitsybitshareswallet.models.UserAccountAuthority
|
||||
|
||||
@Dao
|
||||
interface UserAccountAuthorityDao {
|
||||
@Insert
|
||||
fun insert(userAccountAuthority: UserAccountAuthority)
|
||||
|
||||
// @Query("SELECT * FROM authorities INNER JOIN user_accounts__authorities ON user_accounts.id=user_accounts__authorities.user_account_id WHERE user_accounts__authorities.user_account_id=:userAccountId")
|
||||
// fun getAuthoritiesForUserAccount(userAccountId: String): LiveData<List<Authority>>
|
||||
}
|
|
@ -8,6 +8,8 @@ import androidx.room.PrimaryKey
|
|||
data class Authority (
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "id") val id: Long,
|
||||
@ColumnInfo(name = "encrypted_private_key") val encryptedBrainkey: String,
|
||||
@ColumnInfo(name = "user_id") val userId: String
|
||||
@ColumnInfo(name = "user_id") val userId: String,
|
||||
@ColumnInfo(name = "authority_type") val authorityType: Int,
|
||||
@ColumnInfo(name = "encrypted_brain_key") val encryptedBrainKey: String,
|
||||
@ColumnInfo(name = "encrypted_sequence_number") val encryptedSequenceNumber: String
|
||||
)
|
|
@ -1,13 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName="brain_keys")
|
||||
data class BrainKey(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "public_key") val publicKey: String,
|
||||
@ColumnInfo(name = "encrypted_brain_key") val encryptedBrainKey: String,
|
||||
@ColumnInfo(name = "sequence_number") val sequenceNumber: Long
|
||||
)
|
|
@ -9,6 +9,5 @@ data class UserAccount (
|
|||
@PrimaryKey
|
||||
@ColumnInfo(name = "id") val id: String,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "is_ltm") val isLtm: Boolean,
|
||||
@ColumnInfo(name = "weight_threshold") val weightThreshold: Int
|
||||
@ColumnInfo(name = "is_ltm") val isLtm: Boolean
|
||||
)
|
|
@ -1,25 +0,0 @@
|
|||
package cy.agorise.bitsybitshareswallet.models
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
/**
|
||||
* Table to create a N:N relationship between [UserAccount] and [Authority]
|
||||
*/
|
||||
@Entity(tableName = "user_accounts__authorities",
|
||||
primaryKeys = ["user_account_id", "authority_id"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserAccount::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["user_account_id"]
|
||||
), ForeignKey(
|
||||
entity = Authority::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["authority_id"]
|
||||
)])
|
||||
data class UserAccountAuthority (
|
||||
@ColumnInfo(name = "user_account_id") val userAccountId: String,
|
||||
@ColumnInfo(name = "authority_id") val authorityId: Long,
|
||||
@ColumnInfo(name = "weight") val weight: Int
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.app.Application
|
||||
import android.os.AsyncTask
|
||||
import cy.agorise.bitsybitshareswallet.daos.AuthorityDao
|
||||
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.models.Authority
|
||||
|
||||
class AuthorityRepository internal constructor(application: Application) {
|
||||
|
||||
private val mAuthorityDao: AuthorityDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(application)
|
||||
mAuthorityDao = db!!.authorityDao()
|
||||
}
|
||||
|
||||
fun insert(authority: Authority) {
|
||||
insertAsyncTask(mAuthorityDao).execute(authority)
|
||||
}
|
||||
|
||||
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: AuthorityDao) :
|
||||
AsyncTask<Authority, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg authorities: Authority): Void? {
|
||||
mAsyncTaskDao.insert(authorities[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package cy.agorise.bitsybitshareswallet.repositories
|
||||
|
||||
import android.app.Application
|
||||
import android.os.AsyncTask
|
||||
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
|
||||
import cy.agorise.bitsybitshareswallet.daos.UserAccountDao
|
||||
import cy.agorise.bitsybitshareswallet.models.UserAccount
|
||||
|
||||
class UserAccountRepository internal constructor(application: Application) {
|
||||
|
||||
private val mUserAccountDao: UserAccountDao
|
||||
|
||||
init {
|
||||
val db = BitsyDatabase.getDatabase(application)
|
||||
mUserAccountDao = db!!.userAccountDao()
|
||||
}
|
||||
|
||||
fun insert(userAccount: UserAccount) {
|
||||
insertAsyncTask(mUserAccountDao).execute(userAccount)
|
||||
}
|
||||
|
||||
private class insertAsyncTask internal constructor(private val mAsyncTaskDao: UserAccountDao) :
|
||||
AsyncTask<UserAccount, Void, Void>() {
|
||||
|
||||
override fun doInBackground(vararg userAccounts: UserAccount): Void? {
|
||||
mAsyncTaskDao.insert(userAccounts[0])
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,15 @@ object Constants {
|
|||
/** The minimum required length for a PIN number */
|
||||
const val MIN_PIN_LENGTH = 6
|
||||
|
||||
/** The user selected encrypted PIN */
|
||||
const val KEY_ENCRYPTED_PIN = "key_encrypted_pin"
|
||||
|
||||
/**
|
||||
* LTM accounts come with an expiration date expressed as this string.
|
||||
* This is used to recognize such accounts from regular ones.
|
||||
*/
|
||||
const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59"
|
||||
|
||||
/** Key used to store if the initial setup is already done or not */
|
||||
const val KEY_INITIAL_SETUP_DONE = "key_initial_setup_done"
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package cy.agorise.bitsybitshareswallet.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
|
||||
import com.moldedbits.r2d2.R2d2
|
||||
|
||||
import javax.crypto.AEADBadTagException
|
||||
|
||||
/**
|
||||
* Class that provides encryption/decryption support by using the key management framework provided
|
||||
* by the KeyStore system.
|
||||
*
|
||||
* The implemented scheme was taken from [this](https://medium.com/@ericfu/securely-storing-secrets-in-an-android-application-501f030ae5a3)> blog post.
|
||||
*
|
||||
* @see [Android Keystore System](https://developer.android.com/training/articles/keystore.html)
|
||||
*/
|
||||
|
||||
object CryptoUtils {
|
||||
|
||||
/**
|
||||
* Encrypts and stores a key-value pair in the shared preferences
|
||||
* @param context The application context
|
||||
* @param key The key to be used to reference the data
|
||||
* @param value The actual value to be stored
|
||||
*/
|
||||
fun put(context: Context, key: String, value: String) {
|
||||
val r2d2 = R2d2(context)
|
||||
val encrypted = r2d2.encryptData(value)
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(key, encrypted)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and decrypts an encrypted value from the shared preferences
|
||||
* @param context The application context
|
||||
* @param key The key used to reference the data
|
||||
* @return The plaintext version of the encrypted data
|
||||
*/
|
||||
operator fun get(context: Context, key: String): String {
|
||||
val r2d2 = R2d2(context)
|
||||
val encrypted = PreferenceManager.getDefaultSharedPreferences(context).getString(key, null)
|
||||
return r2d2.decryptData(encrypted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts some data
|
||||
* @param context The application context
|
||||
* @param plaintext The plaintext version of the data
|
||||
* @return Encrypted data
|
||||
*/
|
||||
fun encrypt(context: Context, plaintext: String): String {
|
||||
val r2d2 = R2d2(context)
|
||||
return r2d2.encryptData(plaintext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts some data
|
||||
* @param context The application context
|
||||
* @param ciphertext The ciphertext version of the data
|
||||
* @return Decrypted data
|
||||
*/
|
||||
@Throws(AEADBadTagException::class)
|
||||
fun decrypt(context: Context, ciphertext: String): String {
|
||||
val r2d2 = R2d2(context)
|
||||
return r2d2.decryptData(ciphertext)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue