diff --git a/wallet/build.gradle b/wallet/build.gradle index 01887d5e50..56e7d66178 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -78,8 +78,8 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.core:core-ktx:1.2.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.4" + implementation "androidx.core:core-ktx:1.3.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8" implementation 'com.google.firebase:firebase-analytics:17.2.0' implementation 'com.github.bumptech.glide:glide:4.11.0' diff --git a/wallet/res/layout/activity_dashpay_user.xml b/wallet/res/layout/activity_dashpay_user.xml index 9cc43b9d7c..829b839612 100644 --- a/wallet/res/layout/activity_dashpay_user.xml +++ b/wallet/res/layout/activity_dashpay_user.xml @@ -237,4 +237,15 @@ + + \ No newline at end of file diff --git a/wallet/res/layout/dialog_confirm_transaction.xml b/wallet/res/layout/dialog_confirm_transaction.xml index 624b6dd8ba..ef98469a10 100644 --- a/wallet/res/layout/dialog_confirm_transaction.xml +++ b/wallet/res/layout/dialog_confirm_transaction.xml @@ -104,6 +104,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wallet/res/layout/transaction_result_content.xml b/wallet/res/layout/transaction_result_content.xml index 289bc916cf..5d8af9ed0f 100644 --- a/wallet/res/layout/transaction_result_content.xml +++ b/wallet/res/layout/transaction_result_content.xml @@ -119,6 +119,61 @@ + + + + + + + + + + + + Sent from Sent to Received at + Received from Moved from Internally moved to @@ -408,4 +409,8 @@ You have accepted the contact request from %s Your username %s has been successfully created on the Dash Network + + Not a valid Dash Username or Identity\n\n%s + Sending to + \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/WalletApplication.java b/wallet/src/de/schildbach/wallet/WalletApplication.java index eb9f823d3b..95c6e13a0e 100644 --- a/wallet/src/de/schildbach/wallet/WalletApplication.java +++ b/wallet/src/de/schildbach/wallet/WalletApplication.java @@ -66,7 +66,6 @@ import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletProtobufSerializer; import org.dashevo.dashpay.BlockchainIdentity; -import org.dashevo.platform.Platform; import org.dash.wallet.common.Configuration; import org.dash.wallet.common.ResetAutoLogoutTimerHandler; import org.dash.wallet.integration.uphold.data.UpholdClient; @@ -245,8 +244,6 @@ public void uncaughtException(final Thread thread, final Throwable throwable) { activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); blockchainServiceIntent = new Intent(this, BlockchainServiceImpl.class); - - PlatformRepo.Companion.initPlatformRepo(this); } public void setWallet(Wallet newWallet) { @@ -306,7 +303,7 @@ private void initUphold() { } private void initPlatform() { - PlatformRepo.Companion.getInstance().startUpdateTimer(); + PlatformRepo.initPlatformRepo(this); } public void maybeStartAutoLogoutTimer() { diff --git a/wallet/src/de/schildbach/wallet/data/DashPayContactRequestDao.kt b/wallet/src/de/schildbach/wallet/data/DashPayContactRequestDao.kt index 4b16c9114e..8e987d4639 100644 --- a/wallet/src/de/schildbach/wallet/data/DashPayContactRequestDao.kt +++ b/wallet/src/de/schildbach/wallet/data/DashPayContactRequestDao.kt @@ -14,6 +14,18 @@ interface DashPayContactRequestDao { @Query("SELECT * FROM dashpay_contact_request") fun loadAll(): LiveData + @Query("SELECT * FROM dashpay_contact_request WHERE userId = :userId") + fun loadToOthers(userId: String): LiveData + + @Query("SELECT * FROM dashpay_contact_request WHERE toUserId = :toUserId") + fun loadFromOthers(toUserId: String): LiveData + + fun loadDistinctToOthers(id: String): + LiveData = loadToOthers(id).getDistinct() + + fun loadDistinctFromOthers(id: String): + LiveData = loadFromOthers(id).getDistinct() + @Query("DELETE FROM dashpay_contact_request") fun clear() } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/data/DashPayProfileDao.kt b/wallet/src/de/schildbach/wallet/data/DashPayProfileDao.kt index f44c8f5735..a9d85828eb 100644 --- a/wallet/src/de/schildbach/wallet/data/DashPayProfileDao.kt +++ b/wallet/src/de/schildbach/wallet/data/DashPayProfileDao.kt @@ -14,6 +14,9 @@ interface DashPayProfileDao { @Query("SELECT * FROM dashpay_profile where userId = :userId") fun load(userId: String): LiveData + fun loadDistinct(userId: String): + LiveData = load(userId).getDistinct() + @Query("DELETE FROM dashpay_profile") fun clear() } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/data/NotificationItem.kt b/wallet/src/de/schildbach/wallet/data/NotificationItem.kt new file mode 100644 index 0000000000..937cee328a --- /dev/null +++ b/wallet/src/de/schildbach/wallet/data/NotificationItem.kt @@ -0,0 +1,31 @@ +package de.schildbach.wallet.data + +import org.bitcoinj.core.Transaction +import java.util.* + +data class NotificationItem private constructor(val type: Type, + val usernameSearchResult: UsernameSearchResult? = null, + val tx: Transaction? = null) { + + constructor(usernameSearchResult: UsernameSearchResult) : this(Type.CONTACT_REQUEST, usernameSearchResult = usernameSearchResult) + + constructor(tx: Transaction) : this(Type.PAYMENT, tx = tx) + + enum class Type { + CONTACT_REQUEST, + CONTACT, + PAYMENT + } + + /* date is in milliseconds */ + val date = when (type) { + Type.CONTACT_REQUEST, Type.CONTACT -> usernameSearchResult!!.date * 1000 + else -> tx!!.updateTime.time * 1000 + } + + val id = when (type) { + Type.CONTACT_REQUEST -> usernameSearchResult!!.fromContactRequest!!.userId + Type.CONTACT -> usernameSearchResult!!.toContactRequest!!.toUserId + Type.PAYMENT -> tx!!.txId.toString() + } +} diff --git a/wallet/src/de/schildbach/wallet/data/PaymentIntent.java b/wallet/src/de/schildbach/wallet/data/PaymentIntent.java index fb20136819..78ebf9aa41 100644 --- a/wallet/src/de/schildbach/wallet/data/PaymentIntent.java +++ b/wallet/src/de/schildbach/wallet/data/PaymentIntent.java @@ -21,6 +21,7 @@ import java.util.Arrays; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bitcoinj.core.Address; @@ -163,18 +164,16 @@ private Output(final Parcel in) { @Nullable public final byte[] paymentRequestHash; - public boolean useInstantX = false; - - public void setInstantX(boolean set) { - useInstantX = set; - } + @Nullable + public final String payeeUserId; private static final Logger log = LoggerFactory.getLogger(PaymentIntent.class); public PaymentIntent(@Nullable final Standard standard, @Nullable final String payeeName, @Nullable final String payeeVerifiedBy, @Nullable final Output[] outputs, @Nullable final String memo, @Nullable final String paymentUrl, @Nullable final byte[] payeeData, - @Nullable final String paymentRequestUrl, @Nullable final byte[] paymentRequestHash) { + @Nullable final String paymentRequestUrl, @Nullable final byte[] paymentRequestHash, + @Nullable final String payeeUserId) { this.standard = standard; this.payeeName = payeeName; this.payeeVerifiedBy = payeeVerifiedBy; @@ -184,37 +183,40 @@ public PaymentIntent(@Nullable final Standard standard, @Nullable final String p this.payeeData = payeeData; this.paymentRequestUrl = paymentRequestUrl; this.paymentRequestHash = paymentRequestHash; - } - - public PaymentIntent(@Nullable final Standard standard, @Nullable final String payeeName, @Nullable final String payeeVerifiedBy, - @Nullable final Output[] outputs, @Nullable final String memo, @Nullable final String paymentUrl, @Nullable final byte[] payeeData, - @Nullable final String paymentRequestUrl, @Nullable final byte[] paymentRequestHash, boolean useInstantX) { - this(standard, payeeName, payeeVerifiedBy, outputs, memo, paymentUrl, payeeData, paymentRequestUrl, paymentRequestHash); - this.useInstantX = useInstantX; + this.payeeUserId = payeeUserId; } private PaymentIntent(final Address address, @Nullable final String addressLabel) { - this(null, null, null, buildSimplePayTo(Coin.ZERO, address), addressLabel, null, null, null, null); + this(null, null, null, buildSimplePayTo(Coin.ZERO, address), addressLabel, null, null, null, null, null); } + public static PaymentIntent blank() { - return new PaymentIntent(null, null, null, null, null, null, null, null, null); + return new PaymentIntent(null, null, null, null, null, null, null, null, null, null); } public static PaymentIntent fromAddress(final Address address, @Nullable final String addressLabel) { return new PaymentIntent(address, addressLabel); } + public static PaymentIntent fromAddressWithIdentity(final Address address, @Nullable final String payeeUserId) { + return new PaymentIntent(null, null, null, buildSimplePayTo(Coin.ZERO, address), null, null, null, null, null, payeeUserId); + } + public static PaymentIntent fromAddress(final String address, @Nullable final String addressLabel) throws AddressFormatException { return new PaymentIntent(Address.fromString(Constants.NETWORK_PARAMETERS, address), addressLabel); } + public static PaymentIntent fromUserId(final String payeeUserId) { + return new PaymentIntent(null, null, null, null, null, null, null, null, null, payeeUserId); + } + public static PaymentIntent from(final String address, @Nullable final String addressLabel, @Nullable final Coin amount) throws AddressFormatException { return new PaymentIntent(null, null, null, buildSimplePayTo(amount, Address.fromString(Constants.NETWORK_PARAMETERS, address)), addressLabel, null, - null, null, null); + null, null, null, null); } public static PaymentIntent fromBitcoinUri(final BitcoinURI bitcoinUri) { @@ -223,11 +225,10 @@ public static PaymentIntent fromBitcoinUri(final BitcoinURI bitcoinUri) { final String bluetoothMac = (String) bitcoinUri.getParameterByName(Bluetooth.MAC_URI_PARAM); final String paymentRequestHashStr = (String) bitcoinUri.getParameterByName("h"); final byte[] paymentRequestHash = paymentRequestHashStr != null ? base64UrlDecode(paymentRequestHashStr) : null; - boolean useInstantSend = bitcoinUri.getRequestInstantSend(); return new PaymentIntent(PaymentIntent.Standard.BIP21, null, null, outputs, bitcoinUri.getLabel(), bluetoothMac != null ? "bt:" + bluetoothMac : null, null, bitcoinUri.getPaymentRequestUrl(), - paymentRequestHash, useInstantSend); + paymentRequestHash, null); } private static final BaseEncoding BASE64URL = BaseEncoding.base64Url().omitPadding(); @@ -263,7 +264,7 @@ public PaymentIntent mergeWithEditedValues(@Nullable final Coin editedAmount, outputs = buildSimplePayTo(editedAmount, editedAddress); } - return new PaymentIntent(standard, payeeName, payeeVerifiedBy, outputs, memo, null, payeeData, null, null, useInstantX); + return new PaymentIntent(standard, payeeName, payeeVerifiedBy, outputs, memo, null, payeeData, null, null, null); } public SendRequest toSendRequest() { @@ -366,6 +367,10 @@ public boolean isBluetoothPaymentRequestUrl() { return Bluetooth.isBluetoothUrl(paymentRequestUrl); } + public boolean isIdentityPaymentRequest() { + return payeeUserId != null && !payeeUserId.isEmpty(); + } + /** * Check if given payment intent is only extending on this one, that is it does not alter any of * the fields. Address and amount fields must be equal, respectively (non-existence included). @@ -475,7 +480,7 @@ public void writeToParcel(final Parcel dest, final int flags) { } else { dest.writeInt(0); } - dest.writeByte(useInstantX ? (byte) 1 : (byte) 0); + dest.writeString(payeeUserId); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @@ -525,10 +530,7 @@ private PaymentIntent(final Parcel in) { } else { paymentRequestHash = null; } - useInstantX = in.readByte() == 1; - } - public boolean getUseInstantSend() { - return useInstantX; + payeeUserId = in.readString(); } } diff --git a/wallet/src/de/schildbach/wallet/data/RoomDatabaseExtentions.kt b/wallet/src/de/schildbach/wallet/data/RoomDatabaseExtentions.kt new file mode 100644 index 0000000000..024c023016 --- /dev/null +++ b/wallet/src/de/schildbach/wallet/data/RoomDatabaseExtentions.kt @@ -0,0 +1,25 @@ +package de.schildbach.wallet.data + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer + +fun LiveData.getDistinct(): LiveData { + val distinctLiveData = MediatorLiveData() + distinctLiveData.addSource(this, object : Observer { + private var initialized = false + private var lastObj: T? = null + override fun onChanged(obj: T?) { + if (!initialized) { + initialized = true + lastObj = obj + distinctLiveData.postValue(lastObj) + } else if ((obj == null && lastObj != null) + || obj != lastObj) { + lastObj = obj + distinctLiveData.postValue(lastObj) + } + } + }) + return distinctLiveData +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/livedata/EncryptWalletLiveData.kt b/wallet/src/de/schildbach/wallet/livedata/EncryptWalletLiveData.kt index 6cb8232da6..bee70b7647 100644 --- a/wallet/src/de/schildbach/wallet/livedata/EncryptWalletLiveData.kt +++ b/wallet/src/de/schildbach/wallet/livedata/EncryptWalletLiveData.kt @@ -95,10 +95,6 @@ class EncryptWalletLiveData(application: Application) : MutableLiveData getConnectedPeers(); diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java index a3c3aff506..54b98232f5 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java @@ -875,6 +875,13 @@ public int onStartCommand(final Intent intent, final int flags, final int startI log.info("peergroup not available, not broadcasting transaction " + tx.getHashAsString()); tx.getConfidence().setPeerInfo(0, 1); } + } else if(BlockchainService.ACTION_RESET_BLOOMFILTERS.equals(action)) { + if (peerGroup != null) { + log.info("recalulating bloom filters"); + peerGroup.recalculateFastCatchupAndFilter(PeerGroup.FilterRecalculateMode.SEND_IF_CHANGED); + } else { + log.info("peergroup not available, not resetting bloom filers"); + } } } else { log.warn("service restart, although it was started as non-sticky"); diff --git a/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt b/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt index 903d09f0db..f2c532443c 100644 --- a/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/DashPayUserActivity.kt @@ -17,7 +17,6 @@ package de.schildbach.wallet.ui -import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.drawable.AnimationDrawable @@ -26,22 +25,37 @@ import android.view.View import android.widget.Toast import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide -import de.schildbach.wallet.data.DashPayContactRequest -import de.schildbach.wallet.data.DashPayProfile +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.* import de.schildbach.wallet.livedata.Resource import de.schildbach.wallet.livedata.Status import de.schildbach.wallet.ui.dashpay.DashPayViewModel +import de.schildbach.wallet.ui.dashpay.NotificationsAdapter +import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.ui.send.SendCoinsInternalActivity import de.schildbach.wallet_test.R import kotlinx.android.synthetic.main.activity_dashpay_user.* +import kotlinx.coroutines.runBlocking +import org.bitcoinj.core.PrefixedChecksummedBytes +import org.bitcoinj.core.Transaction +import org.bitcoinj.core.VerificationException import org.dash.wallet.common.InteractionAwareActivity +import kotlin.collections.ArrayList -class DashPayUserActivity : InteractionAwareActivity() { +class DashPayUserActivity : InteractionAwareActivity(), + NotificationsAdapter.OnItemClickListener, + NotificationsAdapter.OnContactRequestButtonClickListener { private lateinit var dashPayViewModel: DashPayViewModel private val username by lazy { intent.getStringExtra(USERNAME) } private val profile: DashPayProfile by lazy { intent.getParcelableExtra(PROFILE) as DashPayProfile } private val displayName by lazy { profile.displayName } + private val notificationsAdapter: NotificationsAdapter = NotificationsAdapter(this, WalletApplication.getInstance().wallet, this) + private var contactRequestReceived: Boolean = false + private var contactRequestSent: Boolean = false + private var sendingRequest: Boolean = true companion object { private const val USERNAME = "username" @@ -70,6 +84,10 @@ class DashPayUserActivity : InteractionAwareActivity() { setContentView(R.layout.activity_dashpay_user) close.setOnClickListener { finish() } + contactRequestSent = intent.getBooleanExtra(CONTACT_REQUEST_SENT, false) + contactRequestReceived = intent.getBooleanExtra(CONTACT_REQUEST_RECEIVED, false) + + dashPayViewModel = ViewModelProvider(this).get(DashPayViewModel::class.java) val defaultAvatar = UserAvatarPlaceholderDrawable.getDrawable(this, username[0]) if (profile.avatarUrl.isNotEmpty()) { @@ -86,10 +104,15 @@ class DashPayUserActivity : InteractionAwareActivity() { } updateContactRelationUi() - dashPayViewModel = ViewModelProvider(this).get(DashPayViewModel::class.java) - - sendContactRequestBtn.setOnClickListener { sendContactRequest(profile.userId) } - accept.setOnClickListener { sendContactRequest(profile.userId) } + sendContactRequestBtn.setOnClickListener { + sendingRequest = true + sendContactRequest(profile.userId) + } + accept.setOnClickListener { + sendingRequest = false + sendContactRequest(profile.userId) + } + payContactBtn.setOnClickListener { startPayActivity() } dashPayViewModel.getContactRequestLiveData.observe(this, object : Observer> { override fun onChanged(it: Resource?) { @@ -104,7 +127,9 @@ class DashPayUserActivity : InteractionAwareActivity() { } Status.SUCCESS -> { setResult(RESULT_CODE_CHANGED) - intent.putExtra(CONTACT_REQUEST_SENT, true) + if (sendingRequest) + contactRequestSent = true + else contactRequestReceived = true updateContactRelationUi() dashPayViewModel.getContactRequestLiveData.removeObserver(this) } @@ -112,6 +137,20 @@ class DashPayUserActivity : InteractionAwareActivity() { } } }) + + notifications_rv.layoutManager = LinearLayoutManager(this) + notifications_rv.adapter = this.notificationsAdapter + this.notificationsAdapter.itemClickListener = this + + if (contactRequestReceived && contactRequestSent) { + dashPayViewModel.notificationsForUserLiveData.observe(this, Observer { + if (Status.SUCCESS == it.status) { + if (it.data != null) { + processResults(it.data) + } + } + }) + } } private fun startLoading() { @@ -126,9 +165,6 @@ class DashPayUserActivity : InteractionAwareActivity() { } private fun updateContactRelationUi() { - val contactRequestSent = intent.getBooleanExtra(CONTACT_REQUEST_SENT, false) - val contactRequestReceived = intent.getBooleanExtra(CONTACT_REQUEST_RECEIVED, false) - listOf(sendContactRequestBtn, sendingContactRequestBtn, contactRequestSentBtn, contactRequestReceivedContainer, payContactBtn).forEach { it.visibility = View.GONE } @@ -136,20 +172,25 @@ class DashPayUserActivity : InteractionAwareActivity() { //No Relationship false to false -> { sendContactRequestBtn.visibility = View.VISIBLE + notifications_rv.visibility = View.GONE } //Contact Established true to true -> { payContactBtn.visibility = View.VISIBLE + notifications_rv.visibility = View.VISIBLE + dashPayViewModel.searchNotificationsForUser(profile.userId) } //Request Sent / Pending true to false -> { contactRequestSentBtn.visibility = View.VISIBLE + notifications_rv.visibility = View.GONE } //Request Received false to true -> { payContactBtn.visibility = View.VISIBLE contactRequestReceivedContainer.visibility = View.VISIBLE requestTitle.text = getString(R.string.contact_request_received_title, username) + notifications_rv.visibility = View.GONE } } } @@ -159,4 +200,71 @@ class DashPayUserActivity : InteractionAwareActivity() { overridePendingTransition(R.anim.activity_stay, R.anim.slide_out_bottom) } + private fun startPayActivity() { + handleString(profile.userId, true, R.string.scan_to_pay_username_dialog_message) + finish() + } + + private fun handleString(input: String, fireAction: Boolean, errorDialogTitleResId: Int) { + object : InputParser.StringInputParser(input, true) { + + override fun handlePaymentIntent(paymentIntent: PaymentIntent) { + if (fireAction) { + SendCoinsInternalActivity.start(this@DashPayUserActivity, paymentIntent, true) + } + } + + override fun error(ex: Exception?, messageResId: Int, vararg messageArgs: Any) { + if (fireAction) { + dialog(this@DashPayUserActivity, null, errorDialogTitleResId, messageResId, *messageArgs) + } + } + + override fun handlePrivateKey(key: PrefixedChecksummedBytes) { + // ignore + } + + @Throws(VerificationException::class) + override fun handleDirectTransaction(tx: Transaction) { + // ignore + } + }.parse() + } + + override fun onItemClicked(view: View, usernameSearchResult: UsernameSearchResult) { + //do nothing if an item is clicked for now + } + + override fun onAcceptRequest(usernameSearchResult: UsernameSearchResult, position: Int) { + // do nothing + } + + override fun onIgnoreRequest(usernameSearchResult: UsernameSearchResult, position: Int) { + //do nothing if an item is clicked for now + } + + private fun processResults(data: List) { + + val results = ArrayList() + + data.forEach { results.add(NotificationsAdapter.ViewItem(it, getViewType(it), false)) } + + notificationsAdapter.results = results + } + + private fun getViewType(notificationItem: NotificationItem): Int { + return when (notificationItem.type) { + NotificationItem.Type.CONTACT_REQUEST, + NotificationItem.Type.CONTACT -> return when (notificationItem.usernameSearchResult!!.requestSent to notificationItem.usernameSearchResult.requestReceived) { + true to true -> { + NotificationsAdapter.NOTIFICATION_CONTACT_ADDED + } + false to true -> { + NotificationsAdapter.NOTIFICATION_CONTACT_REQUEST_RECEIVED + } + else -> throw IllegalArgumentException("View not supported") + } + NotificationItem.Type.PAYMENT -> NotificationsAdapter.NOTIFICATION_PAYMENT + } + } } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/EnterAmountFragment.kt b/wallet/src/de/schildbach/wallet/ui/EnterAmountFragment.kt index fdc53ff6bf..53c5d11553 100644 --- a/wallet/src/de/schildbach/wallet/ui/EnterAmountFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/EnterAmountFragment.kt @@ -23,6 +23,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import com.bumptech.glide.Glide import de.schildbach.wallet.Constants import de.schildbach.wallet.ui.send.EnterAmountSharedViewModel import de.schildbach.wallet.ui.widget.NumericKeyboardView @@ -271,6 +272,24 @@ class EnterAmountFragment : Fragment() { } applyNewValue(it.toPlainString()) }) + userinfo.visibility = View.GONE + sharedViewModel.dashPayProfileData.observe(viewLifecycleOwner, Observer { + userinfo.visibility = View.VISIBLE + displayname.text = if (it.displayName.isNotEmpty()) + it.displayName + else + it.username + + val defaultAvatar = UserAvatarPlaceholderDrawable.getDrawable(context!!, + it.username[0]) + + if(it.avatarUrl.isNotEmpty()) { + Glide.with(avatar).load(it.avatarUrl).circleCrop() + .placeholder(defaultAvatar).into(avatar) + } else { + avatar.background = defaultAvatar + } + }) } private fun applyCurrencySymbol(symbol: String) { diff --git a/wallet/src/de/schildbach/wallet/ui/InputParser.java b/wallet/src/de/schildbach/wallet/ui/InputParser.java index 8090bba682..a611ee9354 100644 --- a/wallet/src/de/schildbach/wallet/ui/InputParser.java +++ b/wallet/src/de/schildbach/wallet/ui/InputParser.java @@ -130,6 +130,14 @@ public void parse() { } catch (final AddressFormatException x) { log.info("got invalid address", x); + error(x, R.string.input_parser_invalid_address); + } + } else if (PATTERN_DASH_IDENTITY.matcher(input).matches()) { + try { + handlePaymentIntent(PaymentIntent.fromUserId(input)); + } catch (final AddressFormatException x) { + log.info("got invalid dash identity", x); + error(x, R.string.input_parser_invalid_address); } } else if (PATTERN_DUMPED_PRIVATE_KEY_UNCOMPRESSED.matcher(input).matches() @@ -390,7 +398,7 @@ public static PaymentIntent parsePaymentRequest(final byte[] serializedPaymentRe final PaymentIntent paymentIntent = new PaymentIntent(PaymentIntent.Standard.BIP70, pkiName, pkiCaName, outputs.toArray(new PaymentIntent.Output[0]), memo, paymentUrl, merchantData, null, - paymentRequestHash); + paymentRequestHash, null); if (paymentIntent.hasPaymentUrl() && !paymentIntent.isSupportedPaymentUrl()) throw new PaymentProtocolException.InvalidPaymentURL( @@ -447,4 +455,6 @@ public static void dialog(final Context context, @Nullable final OnClickListener .compile("6P" + "[" + new String(Base58.ALPHABET) + "]{56}"); private static final Pattern PATTERN_TRANSACTION = Pattern .compile("[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$\\*\\+\\-\\.\\/\\:]{100,}"); + private static final Pattern PATTERN_DASH_IDENTITY = Pattern + .compile("[" + new String(Base58.ALPHABET) + "]{43,44}"); } diff --git a/wallet/src/de/schildbach/wallet/ui/SetPinViewModel.kt b/wallet/src/de/schildbach/wallet/ui/SetPinViewModel.kt index f9eb497037..ad41f17e0e 100644 --- a/wallet/src/de/schildbach/wallet/ui/SetPinViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/SetPinViewModel.kt @@ -76,6 +76,7 @@ class SetPinViewModel(application: Application) : AndroidViewModel(application) } fun initWallet() { + walletApplication.saveWalletAndFinalizeInitialization() startNextActivity.call(walletApplication.configuration.getRemindBackupSeed()) } diff --git a/wallet/src/de/schildbach/wallet/ui/TransactionDetailsDialogFragment.kt b/wallet/src/de/schildbach/wallet/ui/TransactionDetailsDialogFragment.kt index 8ff2ef01f1..b2afa73736 100644 --- a/wallet/src/de/schildbach/wallet/ui/TransactionDetailsDialogFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/TransactionDetailsDialogFragment.kt @@ -9,13 +9,18 @@ import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Observer +import de.schildbach.wallet.AppDatabase import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.DashPayProfile +import de.schildbach.wallet.ui.dashpay.PlatformRepo import de.schildbach.wallet.util.WalletUtils import de.schildbach.wallet_test.R import kotlinx.android.synthetic.main.transaction_details_dialog.* import kotlinx.android.synthetic.main.transaction_result_content.* import org.bitcoinj.core.Sha256Hash import org.bitcoinj.core.Transaction +import org.dashevo.dashpay.BlockchainIdentity import org.slf4j.LoggerFactory /** @@ -46,7 +51,31 @@ class TransactionDetailsDialogFragment : DialogFragment() { super.onViewCreated(view, savedInstanceState) tx = wallet.getTransaction(txId) - val transactionResultViewBinder = TransactionResultViewBinder(transaction_result_container) + val blockchainIdentity: BlockchainIdentity? = PlatformRepo.getInstance().getBlockchainIdentity() + + var profile: DashPayProfile? = null + var userId: String? = null + if (blockchainIdentity != null) { + userId = blockchainIdentity.getContactForTransaction(tx!!) + if (userId != null) { + AppDatabase.getAppDatabase().dashPayProfileDao().loadDistinct(userId).observe(viewLifecycleOwner, Observer { + if (it != null) { + profile = it + finishInitialization(profile) + } + }) + } + } + + if (blockchainIdentity == null || userId == null) + finishInitialization(null) + + view_on_explorer.setOnClickListener { viewOnBlockExplorer() } + transaction_close_btn.setOnClickListener { dismissAnimation() } + } + + private fun finishInitialization(dashPayProfile: DashPayProfile?) { + val transactionResultViewBinder = TransactionResultViewBinder(transaction_result_container, dashPayProfile) if (tx != null) { transactionResultViewBinder.bind(tx!!) } else { @@ -54,8 +83,6 @@ class TransactionDetailsDialogFragment : DialogFragment() { dismiss() return } - view_on_explorer.setOnClickListener { viewOnBlockExplorer() } - transaction_close_btn.setOnClickListener { dismissAnimation() } showAnimation() } diff --git a/wallet/src/de/schildbach/wallet/ui/TransactionResultActivity.kt b/wallet/src/de/schildbach/wallet/ui/TransactionResultActivity.kt index a8a10fead7..f148b75981 100644 --- a/wallet/src/de/schildbach/wallet/ui/TransactionResultActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/TransactionResultActivity.kt @@ -23,13 +23,23 @@ import android.graphics.drawable.Animatable import android.os.Bundle import android.view.View import androidx.core.content.ContextCompat +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import de.schildbach.wallet.AppDatabase import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.DashPayProfile +import de.schildbach.wallet.data.UsernameSearchResult +import de.schildbach.wallet.livedata.Resource +import de.schildbach.wallet.livedata.Status +import de.schildbach.wallet.ui.dashpay.DashPayViewModel +import de.schildbach.wallet.ui.dashpay.PlatformRepo import de.schildbach.wallet.util.WalletUtils import de.schildbach.wallet_test.R import kotlinx.android.synthetic.main.activity_successful_transaction.* import kotlinx.android.synthetic.main.transaction_result_content.* import org.bitcoinj.core.Sha256Hash import org.bitcoinj.core.Transaction +import org.dashevo.dashpay.BlockchainIdentity import org.slf4j.LoggerFactory /** @@ -44,10 +54,16 @@ class TransactionResultActivity : AbstractWalletActivity() { const val EXTRA_USER_AUTHORIZED_RESULT_EXTRA = "user_authorized_result_extra" private const val EXTRA_PAYMENT_MEMO = "payee_name" private const val EXTRA_PAYEE_VERIFIED_BY = "payee_verified_by" + private const val EXTRA_USERID = "payee_userid" @JvmStatic fun createIntent(context: Context, action: String? = null, transaction: Transaction, userAuthorized: Boolean): Intent { - return createIntent(context, action, transaction, userAuthorized, null, null) + return createIntent(context, action, transaction, userAuthorized, null, null, null) + } + + @JvmStatic + fun createIntent(context: Context, action: String? = null, transaction: Transaction, userAuthorized: Boolean, userId: String? = null): Intent { + return createIntent(context, action, transaction, userAuthorized, null, null, userId) } @JvmStatic @@ -57,17 +73,20 @@ class TransactionResultActivity : AbstractWalletActivity() { } fun createIntent(context: Context, action: String?, transaction: Transaction, userAuthorized: Boolean, - paymentMemo: String? = null, payeeVerifiedBy: String? = null): Intent { + paymentMemo: String? = null, payeeVerifiedBy: String? = null, userId: String? = null): Intent { return Intent(context, TransactionResultActivity::class.java).apply { setAction(action) putExtra(EXTRA_TX_ID, transaction.txId) putExtra(EXTRA_USER_AUTHORIZED_RESULT_EXTRA, userAuthorized) putExtra(EXTRA_PAYMENT_MEMO, paymentMemo) putExtra(EXTRA_PAYEE_VERIFIED_BY, payeeVerifiedBy) + putExtra(EXTRA_USERID, userId) } } } + lateinit var dashPayViewModel: DashPayViewModel + @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -75,8 +94,30 @@ class TransactionResultActivity : AbstractWalletActivity() { val txId = intent.getSerializableExtra(EXTRA_TX_ID) as Sha256Hash setContentView(R.layout.activity_successful_transaction) - val transactionResultViewBinder = TransactionResultViewBinder(container) + val blockchainIdentity: BlockchainIdentity? = PlatformRepo.getInstance().getBlockchainIdentity() + val tx = WalletApplication.getInstance().wallet.getTransaction(txId) + + var profile: DashPayProfile? = null + var userId: String? = null + if (blockchainIdentity != null) { + userId = blockchainIdentity.getContactForTransaction(tx!!) + if (userId != null) { + AppDatabase.getAppDatabase().dashPayProfileDao().loadDistinct(userId).observe(this, Observer { + if (it != null) { + profile = it + finishInitialization(txId, tx, profile) + } + }) + } + } + + if (blockchainIdentity == null || userId == null) + finishInitialization(txId, tx!!, null) + } + + private fun finishInitialization(txId: Sha256Hash, tx: Transaction, dashPayProfile: DashPayProfile?) { + val transactionResultViewBinder = TransactionResultViewBinder(container, dashPayProfile) if (tx != null) { val payeeName = intent.getStringExtra(EXTRA_PAYMENT_MEMO) val payeeVerifiedBy = intent.getStringExtra(EXTRA_PAYEE_VERIFIED_BY) @@ -87,6 +128,17 @@ class TransactionResultActivity : AbstractWalletActivity() { intent.action == Intent.ACTION_VIEW -> { finish() } + intent.getStringExtra(EXTRA_USERID) != null -> { + finish() + val userId = intent.getStringExtra(EXTRA_USERID) + dashPayViewModel.getContact(userId) + dashPayViewModel.getContactLiveData.observe(this, Observer> { + if (it != null && it.status == Status.SUCCESS && it.data != null) { + startActivity(DashPayUserActivity.createIntent(this@TransactionResultActivity, + it.data.username, it.data.dashPayProfile, it.data.requestSent, it.data.requestReceived)) + } + }) + } intent.getBooleanExtra(EXTRA_USER_AUTHORIZED_RESULT_EXTRA, false) -> { startActivity(MainActivity.createIntent(this)) } @@ -95,6 +147,7 @@ class TransactionResultActivity : AbstractWalletActivity() { } } } + } else { log.error("Transaction not found. TxId:", txId) finish() @@ -107,6 +160,8 @@ class TransactionResultActivity : AbstractWalletActivity() { check_icon.visibility = View.VISIBLE (check_icon.drawable as Animatable).start() }, 400) + + dashPayViewModel = ViewModelProvider(this).get(DashPayViewModel::class.java) } private fun viewOnExplorer(tx: Transaction) { diff --git a/wallet/src/de/schildbach/wallet/ui/TransactionResultViewBinder.kt b/wallet/src/de/schildbach/wallet/ui/TransactionResultViewBinder.kt index 5a3f91d75a..c1e5f368f6 100644 --- a/wallet/src/de/schildbach/wallet/ui/TransactionResultViewBinder.kt +++ b/wallet/src/de/schildbach/wallet/ui/TransactionResultViewBinder.kt @@ -22,9 +22,12 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat +import com.bumptech.glide.Glide import de.schildbach.wallet.Constants import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.DashPayProfile import de.schildbach.wallet.util.* import de.schildbach.wallet_test.R import org.bitcoinj.core.Address @@ -34,7 +37,7 @@ import org.dash.wallet.common.ui.CurrencyTextView /** * @author Samuel Barbosa */ -class TransactionResultViewBinder(private val containerView: View) { +class TransactionResultViewBinder(private val containerView: View, private val profile: DashPayProfile?) { private val ctx by lazy { containerView.context } private val checkIcon by lazy { containerView.findViewById(R.id.check_icon) } @@ -65,6 +68,10 @@ class TransactionResultViewBinder(private val containerView: View) { private val paymentMemoContainer by lazy { containerView.findViewById(R.id.payment_memo_container) } private val payeeSecuredByContainer by lazy { containerView.findViewById(R.id.payee_verified_by_container) } private val payeeSecuredBy by lazy { containerView.findViewById(R.id.payee_secured_by) } + private val sendToUserContainer by lazy { containerView.findViewById(R.id.user_container) } + private val userLabel by lazy { containerView.findViewById(R.id.user_label) } + private val userAvatar by lazy { containerView.findViewById(R.id.avatar) } + private val userDisplayName by lazy { containerView.findViewById(R.id.displayname) } fun bind(tx: Transaction, payeeName: String? = null, payeeSecuredBy: String? = null) { val noCodeFormat = WalletApplication.getInstance().configuration.format.noCode() @@ -187,6 +194,31 @@ class TransactionResultViewBinder(private val containerView: View) { } } + // handle dashpay + + if (profile != null) { + sendToUserContainer.visibility = View.VISIBLE + inputsContainer.visibility = View.GONE + outputsContainer.visibility = View.GONE + userLabel.text = ctx.getString(if (tx.getValue(wallet).isNegative) R.string.transaction_details_sent_to else R.string.transaction_details_received_from) + + userDisplayName.text = if (profile.displayName.isNotEmpty()) + profile.displayName + else + profile.username + + val defaultAvatar = UserAvatarPlaceholderDrawable.getDrawable(ctx!!, profile.username[0]) + + if (profile.avatarUrl.isNotEmpty()) { + Glide.with(userAvatar).load(profile.avatarUrl).circleCrop() + .placeholder(defaultAvatar).into(userAvatar) + } else { + userAvatar.background = defaultAvatar + } + } else { + sendToUserContainer.visibility = View.GONE + } + setTransactionDirection(tx) } diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/ContactsFragment.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/ContactsFragment.kt index f7561ef701..3b19c29809 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/ContactsFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/ContactsFragment.kt @@ -33,13 +33,19 @@ import androidx.recyclerview.widget.LinearLayoutManager import de.schildbach.wallet.AppDatabase import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.data.DashPayContactRequest +import de.schildbach.wallet.data.PaymentIntent import de.schildbach.wallet.data.UsernameSearchResult import de.schildbach.wallet.data.UsernameSortOrderBy import de.schildbach.wallet.livedata.Resource import de.schildbach.wallet.livedata.Status import de.schildbach.wallet.ui.DashPayUserActivity -import de.schildbach.wallet.ui.SearchUserActivity +import de.schildbach.wallet.ui.InputParser +import de.schildbach.wallet.ui.send.SendCoinsInternalActivity import de.schildbach.wallet.ui.setupActionBarWithTitle +import org.bitcoinj.core.PrefixedChecksummedBytes +import org.bitcoinj.core.Transaction +import org.bitcoinj.core.VerificationException +import de.schildbach.wallet.ui.SearchUserActivity import de.schildbach.wallet_test.R import kotlinx.android.synthetic.main.contacts_empty_state_layout.* import kotlinx.android.synthetic.main.contacts_list_layout.* @@ -73,7 +79,6 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, private lateinit var searchContactsRunnable: Runnable private lateinit var contactsAdapter: ContactSearchResultsAdapter private var query = "" - private var blockchainIdentityId: String? = null private var direction = UsernameSortOrderBy.USERNAME private val mode by lazy { requireArguments().getInt(EXTRA_MODE, MODE_SEARCH_CONTACTS) } private var currentPosition = -1 @@ -139,14 +144,6 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, } } }) - AppDatabase.getAppDatabase().blockchainIdentityDataDao().load().observe(viewLifecycleOwner, Observer { - if (it != null) { - //TODO: we don't have an easy way of getting the identity id (userId) - val tx = walletApplication.wallet.getTransaction(it.creditFundingTxId) - val cftx = walletApplication.wallet.getCreditFundingTransaction(tx) - blockchainIdentityId = cftx.creditBurnIdentityIdentifier.toStringBase58() - } - }) dashPayViewModel.getContactRequestLiveData.observe(viewLifecycleOwner, Observer> { it -> if (it != null && currentPosition != -1) { @@ -204,9 +201,11 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, private fun processResults(data: List) { val results = ArrayList() // process the requests - val requests = if (mode != MODE_SELECT_CONTACT) + val requests = if (mode != MODE_SELECT_CONTACT) { data.filter { r -> r.isPendingRequest }.toMutableList() - else ArrayList() + } else { + ArrayList() + } val requestCount = requests.size if (mode != MODE_VIEW_REQUESTS) { @@ -215,7 +214,7 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, } } - if (requests.isNotEmpty() && mode != MODE_VIEW_REQUESTS) + if (requests.isNotEmpty() && mode == MODE_SEARCH_CONTACTS) results.add(ContactSearchResultsAdapter.ViewItem(null, ContactSearchResultsAdapter.CONTACT_REQUEST_HEADER, requestCount = requestCount)) requests.forEach { r -> results.add(ContactSearchResultsAdapter.ViewItem(r, ContactSearchResultsAdapter.CONTACT_REQUEST)) } @@ -224,7 +223,7 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, data.filter { r -> r.requestSent && r.requestReceived } else ArrayList() - if (contacts.isNotEmpty()) + if (contacts.isNotEmpty() && mode != MODE_VIEW_REQUESTS) results.add(ContactSearchResultsAdapter.ViewItem(null, ContactSearchResultsAdapter.CONTACT_HEADER)) contacts.forEach { r -> results.add(ContactSearchResultsAdapter.ViewItem(r, ContactSearchResultsAdapter.CONTACT)) } @@ -269,19 +268,15 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, } override fun onItemClicked(view: View, usernameSearchResult: UsernameSearchResult) { - when { - usernameSearchResult.isPendingRequest -> { - startActivity(DashPayUserActivity.createIntent(requireContext(), - usernameSearchResult.username, usernameSearchResult.dashPayProfile, contactRequestSent = false, - contactRequestReceived = true)) - - } - !usernameSearchResult.isPendingRequest -> { - // How do we handle if this activity was started from the Payments Screen? + when (mode) { + MODE_SEARCH_CONTACTS, MODE_VIEW_REQUESTS -> { startActivity(DashPayUserActivity.createIntent(requireContext(), usernameSearchResult.username, usernameSearchResult.dashPayProfile, contactRequestSent = usernameSearchResult.requestSent, contactRequestReceived = usernameSearchResult.requestReceived)) } + MODE_SELECT_CONTACT -> { + handleString(usernameSearchResult.toContactRequest!!.toUserId, true, R.string.scan_to_pay_username_dialog_message) + } } } @@ -302,4 +297,34 @@ class ContactsFragment : Fragment(R.layout.fragment_contacts_root), TextWatcher, } } + private fun handleString(input: String, fireAction: Boolean, errorDialogTitleResId: Int) { + object : InputParser.StringInputParser(input, true) { + + override fun handlePaymentIntent(paymentIntent: PaymentIntent) { + if (fireAction) { + SendCoinsInternalActivity.start(context, paymentIntent, true) + } else { + + } + } + + override fun error(ex: Exception?, messageResId: Int, vararg messageArgs: Any) { + if (fireAction) { + dialog(context, null, errorDialogTitleResId, messageResId, *messageArgs) + } else { + + } + } + + override fun handlePrivateKey(key: PrefixedChecksummedBytes) { + // ignore + } + + @Throws(VerificationException::class) + override fun handleDirectTransaction(tx: Transaction) { + // ignore + } + }.parse() + } + } diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt index 864a54ce2d..835cb59864 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt @@ -317,6 +317,8 @@ class CreateIdentityService : LifecycleService() { platformRepo.updateCreationState(blockchainIdentityData, CreationState.DONE) } + PlatformRepo.getInstance().init() + // aaaand we're done :) log.info("username registration complete") } @@ -392,6 +394,7 @@ class CreateIdentityService : LifecycleService() { // Complete the entire process platformRepo.updateCreationState(blockchainIdentityData, CreationState.DONE_AND_DISMISS) + PlatformRepo.getInstance().init() } /** diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/DashPayViewModel.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/DashPayViewModel.kt index 1137d0e28a..46b8459bab 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/DashPayViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/DashPayViewModel.kt @@ -21,6 +21,7 @@ import android.os.Process import androidx.lifecycle.* import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.data.UsernameSearch +import de.schildbach.wallet.data.UsernameSearchResult import de.schildbach.wallet.data.UsernameSortOrderBy import de.schildbach.wallet.livedata.Resource import de.schildbach.wallet.ui.security.SecurityGuard @@ -28,6 +29,8 @@ import de.schildbach.wallet.ui.send.DeriveKeyTask import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.bitcoinj.core.Address import org.bitcoinj.crypto.KeyCrypterException import org.bouncycastle.crypto.params.KeyParameter import java.lang.Exception @@ -40,8 +43,11 @@ class DashPayViewModel(application: Application) : AndroidViewModel(application) private val usernameLiveData = MutableLiveData() private val userSearchLiveData = MutableLiveData() private val contactsLiveData = MutableLiveData() + private val contactUserIdLiveData = MutableLiveData() + val notificationCountLiveData = NotificationCountLiveData(walletApplication, platformRepo) val notificationsLiveData = NotificationsLiveData(walletApplication, platformRepo) + val notificationsForUserLiveData = NotificationsForUserLiveData(walletApplication, platformRepo) val contactsUpdatedLiveData = ContactsUpdatedLiveData(walletApplication, platformRepo) private val contactRequestLiveData = MutableLiveData>() @@ -50,6 +56,7 @@ class DashPayViewModel(application: Application) : AndroidViewModel(application) private var searchUsernamesJob = Job() private var searchContactsJob = Job() private var contactRequestJob = Job() + private var getContactJob = Job() val getUsernameLiveData = Transformations.switchMap(usernameLiveData) { username -> getUsernameJob.cancel() @@ -116,7 +123,11 @@ class DashPayViewModel(application: Application) : AndroidViewModel(application) notificationsLiveData.searchNotifications(text) } - fun getNotificationCount() { + fun searchNotificationsForUser(userId: String) { + notificationsForUserLiveData.searchNotifications(userId) + } + + fun getNotificationCount() { notificationCountLiveData.getNotificationCount() } @@ -132,6 +143,10 @@ class DashPayViewModel(application: Application) : AndroidViewModel(application) } } + fun getNextContactAddress(userId: String): Address { + return platformRepo.getNextContactAddress(userId) + } + //TODO: this can probably be simplified using coroutines private fun deriveEncryptionKey(onSuccess: (KeyParameter) -> Unit, onError: (Exception) -> Unit) { val walletApplication = WalletApplication.getInstance() @@ -170,4 +185,21 @@ class DashPayViewModel(application: Application) : AndroidViewModel(application) } } + val getContactLiveData = Transformations.switchMap(contactUserIdLiveData) { userId -> + getContactJob.cancel() + getContactJob = Job() + liveData(context = getContactJob + Dispatchers.IO) { + if (userId != null) { + emit(Resource.loading(null)) + emit(Resource.success(platformRepo.getLocalUsernameSearchResult(userId))) + } else { + emit(Resource.canceled()) + } + } + } + + fun getContact(username: String?) { + contactUserIdLiveData.value = username + } + } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsActivity.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsActivity.kt index 4db3c461c5..0135cafd74 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsActivity.kt @@ -18,6 +18,7 @@ package de.schildbach.wallet.ui.dashpay import android.content.Context import android.content.Intent +import android.content.res.Resources import android.os.Bundle import android.os.Handler import android.text.Editable @@ -33,6 +34,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import de.schildbach.wallet.AppDatabase import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.data.DashPayContactRequest +import de.schildbach.wallet.data.NotificationItem import de.schildbach.wallet.data.UsernameSearchResult import de.schildbach.wallet.data.UsernameSortOrderBy import de.schildbach.wallet.livedata.Resource @@ -70,9 +72,8 @@ class NotificationsActivity : InteractionAwareActivity(), TextWatcher, private lateinit var walletApplication: WalletApplication private var handler: Handler = Handler() private lateinit var searchContactsRunnable: Runnable - protected val notificationsAdapter: NotificationsAdapter = NotificationsAdapter(this) + private lateinit var notificationsAdapter: NotificationsAdapter private var query = "" - private var blockchainIdentityId: String? = null private var direction = UsernameSortOrderBy.DATE_ADDED private var mode = MODE_NOTIFICATIONS private var lastSeenNotificationTime = 0L @@ -84,6 +85,8 @@ class NotificationsActivity : InteractionAwareActivity(), TextWatcher, walletApplication = application as WalletApplication lastSeenNotificationTime = walletApplication.configuration.lastSeenNotificationTime + notificationsAdapter = NotificationsAdapter(this, walletApplication.wallet, this) + if (intent.extras != null && intent.extras!!.containsKey(EXTRA_MODE)) { mode = intent.extras.getInt(EXTRA_MODE) } @@ -127,15 +130,6 @@ class NotificationsActivity : InteractionAwareActivity(), TextWatcher, } } }) - //This is not used - AppDatabase.getAppDatabase().blockchainIdentityDataDao().load().observe(this, Observer { - if (it != null) { - //TODO: we don't have an easy way of getting the identity id (userId) - val tx = walletApplication.wallet.getTransaction(it.creditFundingTxId) - val cftx = walletApplication.wallet.getCreditFundingTransaction(tx) - blockchainIdentityId = cftx.creditBurnIdentityIdentifier.toStringBase58() - } - }) dashPayViewModel.getContactRequestLiveData.observe(this, object : Observer> { override fun onChanged(it: Resource?) { @@ -153,7 +147,7 @@ class NotificationsActivity : InteractionAwareActivity(), TextWatcher, } Status.SUCCESS -> { // update the data - notificationsAdapter.results[currentPosition].usernameSearchResult!!.toContactRequest = it.data!! + notificationsAdapter.results[currentPosition].notificationItem!!.usernameSearchResult!!.toContactRequest = it.data!! notificationsAdapter.notifyItemChanged(currentPosition) currentPosition = -1 lastSeenNotificationTime = it.data.timestamp.toLong() * 1000 @@ -164,19 +158,23 @@ class NotificationsActivity : InteractionAwareActivity(), TextWatcher, }) } - private fun getViewType(usernameSearchResult: UsernameSearchResult): Int { - return when (usernameSearchResult.requestSent to usernameSearchResult.requestReceived) { - true to true -> { - NotificationsAdapter.NOTIFICATION_CONTACT_ADDED - } - false to true -> { - NotificationsAdapter.NOTIFICATION_CONTACT_REQUEST_RECEIVED + private fun getViewType(notificationItem: NotificationItem): Int { + when (notificationItem.type) { + NotificationItem.Type.CONTACT_REQUEST, + NotificationItem.Type.CONTACT -> return when (notificationItem.usernameSearchResult!!.requestSent to notificationItem.usernameSearchResult.requestReceived) { + true to true -> { + NotificationsAdapter.NOTIFICATION_CONTACT_ADDED + } + false to true -> { + NotificationsAdapter.NOTIFICATION_CONTACT_REQUEST_RECEIVED + } + else -> throw IllegalArgumentException("View not supported") } - else -> throw IllegalArgumentException("View not supported") + NotificationItem.Type.PAYMENT -> throw IllegalStateException() } } - private fun processResults(data: List) { + private fun processResults(data: List) { val results = ArrayList() diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt index b584bba6b0..3398ca698c 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/NotificationsAdapter.kt @@ -16,24 +16,41 @@ */ package de.schildbach.wallet.ui.dashpay +import android.content.Context import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.cardview.widget.CardView import androidx.constraintlayout.widget.Guideline import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.AddressBookProvider +import de.schildbach.wallet.data.NotificationItem import de.schildbach.wallet.data.UsernameSearchResult import de.schildbach.wallet.ui.UserAvatarPlaceholderDrawable +import de.schildbach.wallet.util.TransactionUtil +import de.schildbach.wallet.util.WalletUtils import de.schildbach.wallet_test.R import kotlinx.android.synthetic.main.contact_request_row.view.* +import org.bitcoinj.core.Address +import org.bitcoinj.core.Coin +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.core.Transaction +import org.bitcoinj.utils.MonetaryFormat +import org.bitcoinj.wallet.Wallet +import org.dash.wallet.common.Constants +import org.dash.wallet.common.ui.CurrencyTextView +import org.dash.wallet.common.util.GenericUtils import org.dashevo.dpp.util.HashUtils import java.math.BigInteger +import java.util.* import kotlin.math.max -class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactRequestButtonClickListener) : RecyclerView.Adapter() { +class NotificationsAdapter(val context: Context, val wallet: Wallet, val onContactRequestButtonClickListener: OnContactRequestButtonClickListener) : RecyclerView.Adapter() { companion object { const val NOTIFICATION_NEW_HEADER = 4 @@ -41,9 +58,10 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq const val NOTIFICATION_EARLIER_HEADER = 6 const val NOTIFICATION_CONTACT_ADDED = 7 const val NOTIFICATION_CONTACT_REQUEST_RECEIVED = 8 + const val NOTIFICATION_PAYMENT = 9 } - class ViewItem(val usernameSearchResult: UsernameSearchResult?, + class ViewItem(val notificationItem: NotificationItem?, val viewType: Int, val isNew: Boolean = false) @@ -51,8 +69,20 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq fun onItemClicked(view: View, usernameSearchResult: UsernameSearchResult) } + // TransactionViewHolder related items + val colorBackground: Int by lazy { context.resources.getColor(R.color.bg_bright) } + val colorBackgroundSelected: Int by lazy { context.resources.getColor(R.color.bg_panel) } + val colorPrimaryStatus: Int by lazy { context.resources.getColor(R.color.primary_status) } + val colorSecondaryStatus: Int by lazy { context.resources.getColor(R.color.secondary_status) } + val colorInsignificant: Int by lazy { context.resources.getColor(R.color.fg_insignificant) } + val colorValuePositve: Int by lazy { context.resources.getColor(R.color.colorPrimary) } + val colorValueNegative: Int by lazy { context.resources.getColor(android.R.color.black) } + val colorError: Int by lazy { context.resources.getColor(R.color.fg_error) } + private var format: MonetaryFormat? = null + init { setHasStableIds(true) + format = WalletApplication.getInstance().configuration.format.noCode() } var itemClickListener: OnItemClickListener? = null @@ -69,6 +99,7 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq NOTIFICATION_CONTACT_REQUEST_RECEIVED -> ContactRequestViewHolder(LayoutInflater.from(parent.context), parent) NOTIFICATION_EARLIER_HEADER -> HeaderViewHolder(LayoutInflater.from(parent.context), parent) NOTIFICATION_CONTACT_ADDED -> ContactViewHolder(LayoutInflater.from(parent.context), parent) + NOTIFICATION_PAYMENT -> TransactionViewHolder(LayoutInflater.from(parent.context), parent) else -> throw IllegalArgumentException("Invalid viewType $viewType") } } @@ -87,9 +118,10 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq return when (results[position].viewType) { NOTIFICATION_NEW_HEADER -> 1L NOTIFICATION_NEW_EMPTY -> 2L - NOTIFICATION_CONTACT_REQUEST_RECEIVED -> getLongValue(results[position].usernameSearchResult!!.fromContactRequest!!.userId) + NOTIFICATION_CONTACT_REQUEST_RECEIVED -> getLongValue(results[position].notificationItem!!.id) NOTIFICATION_EARLIER_HEADER -> 3L - NOTIFICATION_CONTACT_ADDED -> getLongValue(results[position].usernameSearchResult!!.toContactRequest!!.toUserId) + NOTIFICATION_CONTACT_ADDED -> getLongValue(results[position].notificationItem!!.id) + NOTIFICATION_PAYMENT -> getLongValue(results[position].notificationItem!!.id) else -> throw IllegalArgumentException("Invalid viewType ${results[position].viewType}") } } @@ -97,10 +129,11 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq override fun onBindViewHolder(holder: ViewHolder, position: Int) { when (results[position].viewType) { NOTIFICATION_CONTACT_REQUEST_RECEIVED, - NOTIFICATION_CONTACT_ADDED -> holder.bind(results[position].usernameSearchResult!!, results[position].isNew) + NOTIFICATION_CONTACT_ADDED -> holder.bind(results[position].notificationItem!!.usernameSearchResult!!, results[position].isNew) NOTIFICATION_NEW_HEADER -> (holder as HeaderViewHolder).bind(R.string.notifications_new) NOTIFICATION_EARLIER_HEADER -> (holder as HeaderViewHolder).bind(R.string.notifications_earlier) NOTIFICATION_NEW_EMPTY -> (holder as ImageViewHolder).bind(R.drawable.ic_notification_new_empty, R.string.notifications_none_new) + NOTIFICATION_PAYMENT -> (holder as TransactionViewHolder).bind(results[position].notificationItem!!.tx!!) else -> throw IllegalArgumentException("Invalid viewType ${results[position].viewType}") } } @@ -117,12 +150,9 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq return results[position].viewType } - fun getItemPosition(usernameSearchResult: UsernameSearchResult): Int { - val viewItem = results.find { - val usernameSearchResult = it.usernameSearchResult ?: false - usernameSearchResult == it.usernameSearchResult - } - return results.indexOf(viewItem) + fun setFormat(format: MonetaryFormat) { + this.format = format.noCode() + notifyDataSetChanged() } open inner class ViewHolder(resId: Int, inflater: LayoutInflater, parent: ViewGroup) : @@ -132,7 +162,7 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq private val date by lazy { itemView.findViewById(R.id.date) } private val displayName by lazy { itemView.findViewById(R.id.displayName) } private val contactAdded by lazy { itemView.findViewById(R.id.contact_added) } - private val guildline by lazy { itemView.findViewById(R.id.center_guideline)} + private val guildline by lazy { itemView.findViewById(R.id.center_guideline) } private val dateFormat by lazy { itemView.context.getString(R.string.transaction_row_time_text) } private fun formatDate(timeStamp: Long): String { @@ -200,7 +230,6 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq } } - inner class ContactViewHolder(inflater: LayoutInflater, parent: ViewGroup) : ViewHolder(R.layout.notification_contact_added_row, inflater, parent) { @@ -268,4 +297,139 @@ class NotificationsAdapter(val onContactRequestButtonClickListener: OnContactReq fun onAcceptRequest(usernameSearchResult: UsernameSearchResult, position: Int) fun onIgnoreRequest(usernameSearchResult: UsernameSearchResult, position: Int) } + + private val transactionCache: HashMap = HashMap() + + private class TransactionCacheEntry constructor(val value: Coin, val sent: Boolean, val self: Boolean, val showFee: Boolean, val address: Address?, + val addressLabel: String?, val type: Transaction.Type) + + inner class TransactionViewHolder(inflater: LayoutInflater, parent: ViewGroup) : ViewHolder(R.layout.notification_transaction_row, inflater, parent) { + private val primaryStatusView: TextView = itemView.findViewById(R.id.transaction_row_primary_status) as TextView + private val secondaryStatusView: TextView = itemView.findViewById(R.id.transaction_row_secondary_status) as TextView + private val timeView: TextView = itemView.findViewById(R.id.transaction_row_time) as TextView + private val dashSymbolView: ImageView = itemView.findViewById(R.id.dash_amount_symbol) as ImageView + private val valueView: CurrencyTextView = itemView.findViewById(R.id.transaction_row_value) as CurrencyTextView + private val signalView: TextView = itemView.findViewById(R.id.transaction_amount_signal) as TextView + private val fiatView: CurrencyTextView = itemView.findViewById(R.id.transaction_row_fiat) as CurrencyTextView + private val rateNotAvailableView: TextView + + fun bind(tx: Transaction) { + if (itemView is CardView) { + itemView.setCardBackgroundColor(if (itemView.isActivated()) colorBackgroundSelected else colorBackground) + } + val confidence = tx.confidence + val fee = tx.fee + var txCache: TransactionCacheEntry? = transactionCache[tx.txId] + if (txCache == null) { + val value = tx.getValue(wallet) + val sent = value.signum() < 0 + val self = WalletUtils.isEntirelySelf(tx, wallet) + val showFee = sent && fee != null && !fee.isZero + val address: Address? + address = if (sent) { + val addresses = WalletUtils.getToAddressOfSent(tx, wallet) + if (addresses.isEmpty()) null else addresses[0] + } else { + WalletUtils.getWalletAddressOfReceived(tx, wallet) + } + val addressLabel = if (address != null) AddressBookProvider.resolveLabel(context, address.toBase58()) else null + val txType = tx.type + txCache = TransactionCacheEntry(value, sent, self, showFee, address, addressLabel, txType) + transactionCache.put(tx.txId, txCache) + } + + // + // Assign the colors of text and values + // + val primaryStatusColor: Int + val secondaryStatusColor: Int + val valueColor: Int + if (confidence.hasErrors()) { + primaryStatusColor = colorError + secondaryStatusColor = colorError + valueColor = colorError + } else { + primaryStatusColor = colorPrimaryStatus + secondaryStatusColor = colorSecondaryStatus + valueColor = if (txCache.sent) colorValueNegative else colorValuePositve + } + + // + // Set the time. eg. "On at