Add in-chat payment activation requests.

Co-authored-by: Varsha <varsha@mobilecoin.com>
This commit is contained in:
Cody Henthorne
2022-11-01 11:50:41 -04:00
parent 8c915572fb
commit 77beeda62a
36 changed files with 595 additions and 51 deletions

View File

@@ -104,6 +104,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onBlockJoinRequest(@NonNull Recipient recipient);
void onRecipientNameClicked(@NonNull RecipientId target);
void onInviteToSignalClicked();
void onActivatePaymentsClicked();
void onSendPaymentClicked(@NonNull RecipientId recipientId);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -9,4 +9,5 @@ public final class EmojiStrings {
public static final String FILE = "\uD83D\uDCCE";
public static final String STICKER = "\u2B50";
public static final String GIFT = "\uD83C\uDF81";
public static final String CARD = "\uD83D\uDCB3";
}

View File

@@ -142,6 +142,7 @@ import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment;
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
@@ -149,6 +150,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -174,6 +176,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
@@ -2104,6 +2107,17 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
conversationViewModel.markGiftBadgeRevealed(messageRecord.getId());
}
}
@Override
public void onActivatePaymentsClicked() {
Intent intent = new Intent(requireContext(), PaymentsActivity.class);
startActivity(intent);
}
@Override
public void onSendPaymentClicked(@NonNull RecipientId recipientId) {
AttachmentManager.selectPayment(ConversationFragment.this, recipient.get());
}
}
private boolean isUnopenedGift(View itemView, MessageRecord messageRecord) {

View File

@@ -249,6 +249,7 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.payments.CanNotSendPaymentDialog;
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
@@ -294,6 +295,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SmsUtil;
@@ -1217,11 +1219,7 @@ public class ConversationParentFragment extends Fragment
AttachmentManager.selectLocation(this, PICK_LOCATION, getSendButtonColor(sendButton.getSelectedSendType()));
break;
case PAYMENT:
if (ExpiringProfileCredentialUtil.isValid(recipient.get().getExpiringProfileKeyCredential())) {
AttachmentManager.selectPayment(this, recipient.getId());
} else {
CanNotSendPaymentDialog.show(requireActivity());
}
AttachmentManager.selectPayment(this, recipient.get());
break;
}

View File

@@ -544,7 +544,23 @@ public final class ConversationUpdateItem extends FrameLayout
});
actionButton.setText(R.string.ConversationActivity__invite_to_signal);
} else {
} else if (conversationMessage.getMessageRecord().isRequestToActivatePayments() && !conversationMessage.getMessageRecord().isOutgoing() && !SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) {
actionButton.setText(R.string.ConversationUpdateItem_activate_payments);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onActivatePaymentsClicked();
}
});
} else if (conversationMessage.getMessageRecord().isPaymentsActivated() && !conversationMessage.getMessageRecord().isOutgoing()) {
actionButton.setText(R.string.ConversationUpdateItem_send_payment);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onSendPaymentClicked(conversationMessage.getMessageRecord().getIndividualRecipient().getId());
}
});
} else{
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}

View File

@@ -205,7 +205,9 @@ final class MenuState {
messageRecord.isChatSessionRefresh() ||
messageRecord.isInMemoryMessageRecord() ||
messageRecord.isChangeNumber() ||
messageRecord.isBoostRequest();
messageRecord.isBoostRequest() ||
messageRecord.isRequestToActivatePayments() ||
messageRecord.isPaymentsActivated();
}
private final static class Builder {

View File

@@ -228,6 +228,16 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
dismiss()
getAdapterListener().onViewGiftBadgeClicked(messageRecord)
}
override fun onActivatePaymentsClicked() {
dismiss()
getAdapterListener().onActivatePaymentsClicked()
}
override fun onSendPaymentClicked(recipientId: RecipientId) {
dismiss()
getAdapterListener().onSendPaymentClicked(recipientId)
}
}
interface Callback {

View File

@@ -12,6 +12,7 @@ import com.google.android.mms.pdu_alt.NotificationInd;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.CursorExtensionsKt;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SQLiteDatabaseExtensionsKt;
import org.signal.core.util.SqlUtil;
@@ -517,6 +518,15 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
return data;
}
public List<Long> getIncomingPaymentRequestThreads() {
Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "DISTINCT " + THREAD_ID)
.from(getTableName())
.where("(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE + " AND (" + getTypeField() + " & ?) != 0", Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST)
.run();
return CursorExtensionsKt.readToList(cursor, c -> CursorUtil.requireLong(c, THREAD_ID));
}
@Override
public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) {
ContentValues values = new ContentValues();

View File

@@ -78,6 +78,8 @@ import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingPaymentsActivatedMessages;
import org.thoughtcrime.securesms.mms.OutgoingRequestToActivatePaymentMessages;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -1794,6 +1796,10 @@ public class MmsDatabase extends MessageDatabase {
return new OutgoingGroupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
} else if (Types.isRequestToActivatePayments(outboxType)) {
return new OutgoingRequestToActivatePaymentMessages(recipient, timestamp, expiresIn);
} else if (Types.isPaymentsActivated(outboxType)) {
return new OutgoingPaymentsActivatedMessages(recipient, timestamp, expiresIn);
}
GiftBadge giftBadge = null;
@@ -1987,7 +1993,7 @@ public class MmsDatabase extends MessageDatabase {
updateThread);
boolean isNotStoryGroupReply = retrieved.getParentStoryId() == null || !retrieved.getParentStoryId().isGroupReply();
if (!Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) {
if (!Types.isPaymentsActivated(mailbox) && !Types.isRequestToActivatePayments(mailbox) && !Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) {
boolean incrementUnreadMentions = !retrieved.getMentions().isEmpty() && retrieved.getMentions().stream().anyMatch(m -> m.getRecipientId().equals(Recipient.self().getId()));
SignalDatabase.threads().incrementUnread(threadId, 1, incrementUnreadMentions ? 1 : 0);
SignalDatabase.threads().update(threadId, true);
@@ -2013,6 +2019,14 @@ public class MmsDatabase extends MessageDatabase {
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
}
if (retrieved.isActivatePaymentsRequest()) {
type |= Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
}
if (retrieved.isPaymentsActivated()) {
type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED;
}
return insertMessageInbox(retrieved, contentLocation, threadId, type);
}
@@ -2044,6 +2058,20 @@ public class MmsDatabase extends MessageDatabase {
type |= Types.SPECIAL_TYPE_GIFT_BADGE;
}
if (retrieved.isActivatePaymentsRequest()) {
if (hasSpecialType) {
throw new MmsException("Cannot insert message with multiple special types.");
}
type |= Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
}
if (retrieved.isPaymentsActivated()) {
if (hasSpecialType) {
throw new MmsException("Cannot insert message with multiple special types.");
}
type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED;
}
return insertMessageInbox(retrieved, "", threadId, type);
}
@@ -2211,6 +2239,20 @@ public class MmsDatabase extends MessageDatabase {
type |= Types.SPECIAL_TYPE_GIFT_BADGE;
}
if (message.isRequestToActivatePayments()) {
if (hasSpecialType) {
throw new MmsException("Cannot insert message with multiple special types.");
}
type |= Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
}
if (message.isPaymentsActivated()) {
if (hasSpecialType) {
throw new MmsException("Cannot insert message with multiple special types.");
}
type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED;
}
Map<RecipientId, EarlyReceiptCache.Receipt> earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis());
ContentValues contentValues = new ContentValues();

View File

@@ -140,9 +140,11 @@ public interface MmsSmsColumns {
protected static final long ENCRYPTION_REMOTE_LEGACY_BIT = 0x02000000;
// Special message types
public static final long SPECIAL_TYPES_MASK = 0xF00000000L;
public static final long SPECIAL_TYPE_STORY_REACTION = 0x100000000L;
public static final long SPECIAL_TYPE_GIFT_BADGE = 0x200000000L;
public static final long SPECIAL_TYPES_MASK = 0xF00000000L;
public static final long SPECIAL_TYPE_STORY_REACTION = 0x100000000L;
public static final long SPECIAL_TYPE_GIFT_BADGE = 0x200000000L;
protected static final long SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST = 0x400000000L;
protected static final long SPECIAL_TYPE_PAYMENTS_ACTIVATED = 0x800000000L;
public static boolean isStoryReaction(long type) {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_STORY_REACTION;
@@ -152,6 +154,14 @@ public interface MmsSmsColumns {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_GIFT_BADGE;
}
public static boolean isRequestToActivatePayments(long type) {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
}
public static boolean isPaymentsActivated(long type) {
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_PAYMENTS_ACTIVATED;
}
public static boolean isDraftMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
}

View File

@@ -39,7 +39,6 @@ import org.signal.core.util.SQLiteDatabaseExtensionsKt;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.components.settings.app.chats.sms.SmsExportState;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;

View File

@@ -52,6 +52,10 @@ public final class ThreadBodyUtil {
return String.format("%s %s", EmojiStrings.GIFT, getGiftSummary(context, record));
} else if (MessageRecordUtil.isStoryReaction(record)) {
return getStoryReactionSummary(context, record);
} else if (MessageRecordUtil.isPaymentActivationRequest(record)) {
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivationRequestSummary(context, record));
} else if (MessageRecordUtil.isPaymentsActivated(record)) {
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivatedSummary(context, record));
}
boolean hasImage = false;
@@ -94,6 +98,22 @@ public final class ThreadBodyUtil {
return context.getString(R.string.ThreadRecord__reacted_s_to_your_story, messageRecord.getDisplayBody(context));
}
}
private static @NonNull String getPaymentActivationRequestSummary(@NonNull Context context, @NonNull MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) {
return context.getString(R.string.ThreadRecord_you_sent_request);
} else {
return context.getString(R.string.ThreadRecord_wants_you_to_activate_payments, messageRecord.getRecipient().getShortDisplayName(context));
}
}
private static @NonNull String getPaymentActivatedSummary(@NonNull Context context, @NonNull MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) {
return context.getString(R.string.ThreadRecord_you_activated_payments);
} else {
return context.getString(R.string.ThreadRecord_can_accept_payments, messageRecord.getRecipient().getShortDisplayName(context));
}
}
private static @NonNull String format(@NonNull Context context, @NonNull MessageRecord record, @NonNull String emoji, @StringRes int defaultStringRes) {
return String.format("%s %s", emoji, getBodyOrDefault(context, record, defaultStringRes));

View File

@@ -22,6 +22,7 @@ import android.text.SpannableString;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -231,4 +232,12 @@ public abstract class DisplayRecord {
public boolean isPendingInsecureSmsFallback() {
return SmsDatabase.Types.isPendingInsecureSmsFallbackType(type);
}
public boolean isRequestToActivatePayments() {
return SmsDatabase.Types.isRequestToActivatePayments(type);
}
public boolean isPaymentsActivated() {
return SmsDatabase.Types.isPaymentsActivated(type);
}
}

View File

@@ -240,6 +240,12 @@ public abstract class MessageRecord extends DisplayRecord {
int messageResource = SignalStore.misc().getSmsExportPhase().isSmsSupported() ? R.string.MessageRecord__you_will_no_longer_be_able_to_send_sms_messages_from_signal_soon
: R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
return fromRecipient(getIndividualRecipient(), r -> context.getString(messageResource, r.getDisplayName(context)), R.drawable.ic_update_info_16);
} else if (isRequestToActivatePayments()) {
return isOutgoing() ? fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_sent_request, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments)
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_wants_you_to_activate_payments, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments);
} else if (isPaymentsActivated()) {
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_activated_payments), R.drawable.ic_card_activate_payments)
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_can_accept_payments, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments);
}
return null;
@@ -570,7 +576,8 @@ public abstract class MessageRecord extends DisplayRecord {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType();
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() ||
isRequestToActivatePayments() || isPaymentsActivated();
}
public boolean isMediaPending() {

View File

@@ -102,6 +102,7 @@ public final class JobManagerFactories {
put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory());
put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory());
put(GiftSendJob.KEY, new GiftSendJob.Factory());
put(SendPaymentsActivatedJob.KEY, new SendPaymentsActivatedJob.Factory());
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
put(GroupCallPeekJob.KEY, new GroupCallPeekJob.Factory());

View File

@@ -249,8 +249,7 @@ public class MmsDownloadJob extends BaseJob {
List<RecipientId> recipients = new ArrayList<>(members);
group = Optional.of(SignalDatabase.groups().getOrCreateMmsGroupForMembers(recipients));
}
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, TimeUnit.SECONDS.toMillis(retrieved.getDate()), -1, System.currentTimeMillis(), attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts));
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, TimeUnit.SECONDS.toMillis(retrieved.getDate()), -1, System.currentTimeMillis(), attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts), false, false);
Optional<InsertResult> insertResult = database.insertMessageInbox(message, contentLocation, threadId);
if (insertResult.isPresent()) {

View File

@@ -107,7 +107,7 @@ public final class PaymentNotificationSendJob extends BaseJob {
}
SignalServiceDataMessage dataMessage = SignalServiceDataMessage.newBuilder()
.withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote())))
.withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()), null))
.build();
SendMessageResult sendMessageResult = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.DEFAULT, dataMessage, IndividualSendEvents.EMPTY, false, recipient.needsPniSignature());

View File

@@ -46,6 +46,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -212,6 +213,7 @@ public class PushMediaSendJob extends PushSendJob {
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<SignalServicePreview> previews = getPreviewsFor(message);
SignalServiceDataMessage.GiftBadge giftBadge = getGiftBadgeFor(message);
SignalServiceDataMessage.Payment payment = getPaymentActivation(message);
SignalServiceDataMessage.Builder mediaMessageBuilder = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(serviceAttachments)
@@ -223,7 +225,8 @@ public class PushMediaSendJob extends PushSendJob {
.withSharedContacts(sharedContacts)
.withPreviews(previews)
.withGiftBadge(giftBadge)
.asExpirationUpdate(message.isExpirationUpdate());
.asExpirationUpdate(message.isExpirationUpdate())
.withPayment(payment);
if (message.getParentStoryId() != null) {
try {
@@ -278,6 +281,22 @@ public class PushMediaSendJob extends PushSendJob {
}
}
private SignalServiceDataMessage.Payment getPaymentActivation(OutgoingMediaMessage message) {
SignalServiceProtos.DataMessage.Payment.Activation.Type type = null;
if (message.isRequestToActivatePayments()) {
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST;
} else if (message.isPaymentsActivated()) {
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED;
}
if (type != null) {
return new SignalServiceDataMessage.Payment(null, new SignalServiceDataMessage.PaymentActivation(type));
} else {
return null;
}
}
public static long getMessageId(@NonNull Data data) {
return data.getLong(KEY_MESSAGE_ID);
}

View File

@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingPaymentsActivatedMessages
import org.thoughtcrime.securesms.sms.MessageSender
/**
* Send payments activated message to all recipients of payment activation request
*/
class SendPaymentsActivatedJob(parameters: Parameters) : BaseJob(parameters) {
companion object {
private val TAG = Log.tag(SendPaymentsActivatedJob::class.java)
const val KEY = "SendPaymentsActivatedJob"
}
constructor() : this(parameters = Parameters.Builder().build())
override fun serialize(): Data = Data.Builder().build()
override fun getFactoryKey(): String = KEY
@Suppress("UsePropertyAccessSyntax")
override fun onRun() {
if (!SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) {
Log.w(TAG, "Payments aren't enabled, not going to attempt to send activation messages.")
return
}
val threadIds: List<Long> = SignalDatabase.mms.getIncomingPaymentRequestThreads()
for (threadId in threadIds) {
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)
if (recipient != null) {
MessageSender.send(
context,
OutgoingPaymentsActivatedMessages(recipient, System.currentTimeMillis(), 0),
threadId,
false,
null,
null
)
} else {
Log.w(TAG, "Unable to send activation message for thread: $threadId")
}
}
}
override fun onShouldRetry(e: Exception): Boolean {
return false
}
override fun onFailure() {
Log.w(TAG, "Failed to submit send of payments activated messages")
}
class Factory : Job.Factory<SendPaymentsActivatedJob> {
override fun create(parameters: Parameters, data: Data): SendPaymentsActivatedJob {
return SendPaymentsActivatedJob(parameters)
}
}
}

View File

@@ -295,6 +295,8 @@ public final class MessageContentProcessor {
else if (message.getReaction().isPresent() && message.getStoryContext().isPresent()) messageId = handleStoryReaction(content, message, senderRecipient);
else if (message.getReaction().isPresent()) messageId = handleReaction(content, message, senderRecipient);
else if (message.getRemoteDelete().isPresent()) messageId = handleRemoteDelete(content, message, senderRecipient);
else if (message.isActivatePaymentsRequest()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, true, false);
else if (message.isPaymentsActivated()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, false, true);
else if (message.getPayment().isPresent()) handlePayment(content, message, senderRecipient);
else if (message.getStoryContext().isPresent()) messageId = handleStoryReply(content, message, senderRecipient, receivedTime);
else if (message.getGiftBadge().isPresent()) messageId = handleGiftMessage(content, message, senderRecipient, threadRecipient, receivedTime);
@@ -809,6 +811,60 @@ public final class MessageContentProcessor {
}
}
/**
* @param isActivatePaymentsRequest True if payments activation request message.
* @param isPaymentsActivated True if payments activated message.
* @throws StorageFailedException
*/
private @Nullable MessageId handlePaymentActivation(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId,
@NonNull Recipient senderRecipient,
long receivedTime,
boolean isActivatePaymentsRequest,
boolean isPaymentsActivated)
throws StorageFailedException
{
try {
MessageDatabase database = SignalDatabase.mms();
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(),
content.getTimestamp(),
content.getServerReceivedTimestamp(),
receivedTime,
StoryType.NONE,
null,
false,
-1,
TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()),
false,
false,
content.isNeedsReceipt(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
content.getServerUuid(),
null,
isActivatePaymentsRequest,
isPaymentsActivated);
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
if (smsMessageId.isPresent()) {
SignalDatabase.sms().deleteMessage(smsMessageId.get());
}
if (insertResult.isPresent()) {
return new MessageId(insertResult.get().getMessageId(), true);
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
}
return null;
}
/**
* @param sideEffect True if the event is side effect of a different message, false if the message itself was an expiration update.
@@ -862,7 +918,9 @@ public final class MessageContentProcessor {
Optional.empty(),
Optional.empty(),
content.getServerUuid(),
null);
null,
false,
false);
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
@@ -1423,7 +1481,9 @@ public final class MessageContentProcessor {
Optional.empty(),
Optional.empty(),
content.getServerUuid(),
null);
null,
false,
false);
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
@@ -1587,7 +1647,9 @@ public final class MessageContentProcessor {
Optional.empty(),
Optional.empty(),
content.getServerUuid(),
null);
null,
false,
false);
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
@@ -1690,7 +1752,9 @@ public final class MessageContentProcessor {
getMentions(message.getMentions()),
Optional.empty(),
content.getServerUuid(),
null);
null,
false,
false);
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
@@ -1768,7 +1832,9 @@ public final class MessageContentProcessor {
Optional.empty(),
Optional.empty(),
content.getServerUuid(),
giftBadge);
giftBadge,
false,
false);
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
} catch (MmsException e) {
@@ -1832,7 +1898,9 @@ public final class MessageContentProcessor {
mentions,
sticker,
content.getServerUuid(),
null);
null,
false,
false);
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);

View File

@@ -39,7 +39,10 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -52,27 +55,36 @@ import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Fragment;
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
import org.thoughtcrime.securesms.payments.CanNotSendPaymentDialog;
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
import org.thoughtcrime.securesms.payments.PaymentsAddressException;
import org.thoughtcrime.securesms.payments.create.CreatePaymentFragmentArgs;
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog;
import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
import java.io.IOException;
import java.util.Collections;
@@ -81,6 +93,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class AttachmentManager {
@@ -419,11 +432,45 @@ public class AttachmentManager {
fragment.startActivityForResult(intent, requestCode);
}
public static void selectPayment(@NonNull Fragment fragment, @NonNull RecipientId recipientId) {
Intent intent = new Intent(fragment.requireContext(), PaymentsActivity.class);
intent.putExtra(PaymentsActivity.EXTRA_PAYMENTS_STARTING_ACTION, R.id.action_directly_to_createPayment);
intent.putExtra(PaymentsActivity.EXTRA_STARTING_ARGUMENTS, new CreatePaymentFragmentArgs.Builder(new PayeeParcelable(recipientId)).build().toBundle());
fragment.startActivity(intent);
public static void selectPayment(@NonNull Fragment fragment, @NonNull Recipient recipient) {
if (!ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential())) {
CanNotSendPaymentDialog.show(fragment.requireContext());
return;
}
SimpleTask.run(fragment.getViewLifecycleOwner().getLifecycle(),
() -> {
try {
return ProfileUtil.getAddressForRecipient(recipient);
} catch (IOException | PaymentsAddressException e) {
Log.w(TAG, "Could not get address for recipient: ", e);
return null;
}
},
(address) -> {
if (address != null) {
Intent intent = new Intent(fragment.requireContext(), PaymentsActivity.class);
intent.putExtra(PaymentsActivity.EXTRA_PAYMENTS_STARTING_ACTION, R.id.action_directly_to_createPayment);
intent.putExtra(PaymentsActivity.EXTRA_STARTING_ARGUMENTS, new CreatePaymentFragmentArgs.Builder(new PayeeParcelable(recipient.getId())).build().toBundle());
fragment.startActivity(intent);
} else if (FeatureFlags.paymentsRequestActivateFlow()) {
showRequestToActivatePayments(fragment.requireContext(), recipient);
} else {
RecipientHasNotEnabledPaymentsDialog.show(fragment.requireContext());
}
});
}
public static void showRequestToActivatePayments(@NonNull Context context, @NonNull Recipient recipient) {
new MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.AttachmentManager__not_activated_payments, recipient.getShortDisplayName(context)))
.setMessage(context.getString(R.string.AttachmentManager__request_to_activate_payments))
.setPositiveButton(context.getString(R.string.AttachmentManager__send_request), (dialog, which) -> {
OutgoingRequestToActivatePaymentMessages outgoingMessage = new OutgoingRequestToActivatePaymentMessages(recipient, System.currentTimeMillis(), 0);
MessageSender.send(context, outgoingMessage, SignalDatabase.threads().getOrCreateThreadIdFor(recipient), false, null, null);
})
.setNegativeButton(context.getString(R.string.AttachmentManager__cancel), null)
.show();
}
private @Nullable Uri getSlideUri() {

View File

@@ -38,7 +38,9 @@ class IncomingMediaMessage(
sharedContacts: List<Contact> = emptyList(),
linkPreviews: List<LinkPreview> = emptyList(),
mentions: List<Mention> = emptyList(),
val giftBadge: GiftBadge? = null
val giftBadge: GiftBadge? = null,
val isActivatePaymentsRequest: Boolean = false,
val isPaymentsActivated: Boolean = false
) {
val attachments: List<Attachment> = ArrayList(attachments)
@@ -61,7 +63,9 @@ class IncomingMediaMessage(
expirationUpdate: Boolean,
viewOnce: Boolean,
unidentified: Boolean,
sharedContacts: Optional<List<Contact>>
sharedContacts: Optional<List<Contact>>,
activatePaymentsRequest: Boolean,
paymentsActivated: Boolean
) : this(
from = from,
groupId = groupId.orElse(null),
@@ -79,6 +83,8 @@ class IncomingMediaMessage(
serverGuid = null,
attachments = attachments?.let { ArrayList<Attachment>(it) } ?: emptyList(),
sharedContacts = ArrayList<Contact>(sharedContacts.orElse(emptyList())),
isActivatePaymentsRequest = activatePaymentsRequest,
isPaymentsActivated = paymentsActivated
)
constructor(
@@ -103,7 +109,9 @@ class IncomingMediaMessage(
mentions: Optional<List<Mention>>,
sticker: Optional<Attachment>,
serverGuid: String?,
giftBadge: GiftBadge?
giftBadge: GiftBadge?,
activatePaymentsRequest: Boolean,
paymentsActivated: Boolean
) : this(
from = from,
groupId = if (group.isPresent) GroupId.v2(group.get().masterKey) else null,
@@ -126,6 +134,8 @@ class IncomingMediaMessage(
sharedContacts = sharedContacts.orElse(emptyList()),
linkPreviews = linkPreviews.orElse(emptyList()),
mentions = mentions.orElse(emptyList()),
giftBadge = giftBadge
giftBadge = giftBadge,
isActivatePaymentsRequest = activatePaymentsRequest,
isPaymentsActivated = paymentsActivated
)
}

View File

@@ -193,6 +193,14 @@ public class OutgoingMediaMessage {
return false;
}
public boolean isRequestToActivatePayments() {
return false;
}
public boolean isPaymentsActivated() {
return false;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.mms
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.LinkedList
/**
* Specialized message sent to request someone activate payments.
*/
class OutgoingRequestToActivatePaymentMessages(
recipient: Recipient,
sentTimeMillis: Long,
expiresIn: Long
) : OutgoingSecureMediaMessage(
recipient,
"",
LinkedList(),
sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION,
expiresIn,
false,
StoryType.NONE,
null,
false,
null, emptyList(), emptyList(), emptyList(),
null
) {
override fun isRequestToActivatePayments(): Boolean {
return true
}
}
/**
* Specialized message sent to indicate you activated payments. Intended to only
* be sent to those that sent requests prior to activation.
*/
class OutgoingPaymentsActivatedMessages(
recipient: Recipient,
sentTimeMillis: Long,
expiresIn: Long
) : OutgoingSecureMediaMessage(
recipient,
"",
LinkedList(),
sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION,
expiresIn,
false,
StoryType.NONE,
null,
false,
null, emptyList(), emptyList(), emptyList(),
null
) {
override fun isPaymentsActivated(): Boolean {
return true
}
}

View File

@@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.SendPaymentsActivatedJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.ProfileUtil;
@@ -25,7 +26,10 @@ public class PaymentsHomeRepository {
SignalStore.paymentsValues().setMobileCoinPaymentsEnabled(true);
try {
ProfileUtil.uploadProfile(ApplicationDependencies.getApplication());
ApplicationDependencies.getJobManager().add(PaymentLedgerUpdateJob.updateLedger());
ApplicationDependencies.getJobManager()
.startChain(PaymentLedgerUpdateJob.updateLedger())
.then(new SendPaymentsActivatedJob())
.enqueue();
callback.onComplete(null);
} catch (PaymentsRegionException e) {
SignalStore.paymentsValues().setMobileCoinPaymentsEnabled(false);

View File

@@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.payments.preferences;
import android.app.AlertDialog;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.R;
/**
@@ -19,16 +20,17 @@ public final class RecipientHasNotEnabledPaymentsDialog {
public static void show(@NonNull Context context) {
show(context, null);
}
public static void show(@NonNull Context context, @Nullable Runnable onDismissed) {
new AlertDialog.Builder(context).setTitle(R.string.ConfirmPaymentFragment__invalid_recipient)
.setMessage(R.string.ConfirmPaymentFragment__this_person_has_not_activated_payments)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
dialog.dismiss();
if (onDismissed != null) {
onDismissed.run();
}
})
.setCancelable(false)
.show();
new MaterialAlertDialogBuilder(context).setTitle(R.string.ConfirmPaymentFragment__invalid_recipient)
.setMessage(R.string.ConfirmPaymentFragment__this_person_has_not_activated_payments)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
dialog.dismiss();
if (onDismissed != null) {
onDismissed.run();
}
})
.setCancelable(false)
.show();
}
}

View File

@@ -104,6 +104,7 @@ public final class FeatureFlags {
private static final String HIDE_CONTACTS = "android.hide.contacts";
private static final String SMS_EXPORT_MEGAPHONE_DELAY_DAYS = "android.smsExport.megaphoneDelayDays.2";
public static final String CREDIT_CARD_PAYMENTS = "android.credit.card.payments";
private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -159,7 +160,8 @@ public final class FeatureFlags {
STORIES_LOCALE,
HIDE_CONTACTS,
SMS_EXPORT_MEGAPHONE_DELAY_DAYS,
CREDIT_CARD_PAYMENTS
CREDIT_CARD_PAYMENTS,
PAYMENTS_REQUEST_ACTIVATE_FLOW
);
@VisibleForTesting
@@ -222,7 +224,8 @@ public final class FeatureFlags {
RECIPIENT_MERGE_V2,
STORIES,
SMS_EXPORT_MEGAPHONE_DELAY_DAYS,
CREDIT_CARD_PAYMENTS
CREDIT_CARD_PAYMENTS,
PAYMENTS_REQUEST_ACTIVATE_FLOW
);
/**
@@ -570,6 +573,11 @@ public final class FeatureFlags {
return getBoolean(CREDIT_CARD_PAYMENTS, Environment.IS_STAGING);
}
/** Whether client supports sending a request to another to activate payments */
public static boolean paymentsRequestActivateFlow() {
return getBoolean(PAYMENTS_REQUEST_ACTIVATE_FLOW, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -44,6 +44,12 @@ fun MessageRecord.hasThumbnail(): Boolean =
fun MessageRecord.isStoryReaction(): Boolean =
isMms && MmsSmsColumns.Types.isStoryReaction((this as MmsMessageRecord).type)
fun MessageRecord.isPaymentActivationRequest(): Boolean =
isMms && MmsSmsColumns.Types.isRequestToActivatePayments((this as MmsMessageRecord).type)
fun MessageRecord.isPaymentsActivated(): Boolean =
isMms && MmsSmsColumns.Types.isPaymentsActivated((this as MmsMessageRecord).type)
fun MessageRecord.isBorderless(context: Context): Boolean {
return isCaptionlessMms(context) &&
hasThumbnail() &&