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
@@ -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);
@@ -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";
}
@@ -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) {
@@ -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;
}
@@ -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);
}
@@ -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 {
@@ -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 {
@@ -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();
@@ -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();
@@ -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;
}
@@ -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;
@@ -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));
@@ -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);
}
}
@@ -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() {
@@ -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());
@@ -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()) {
@@ -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());
@@ -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);
}
@@ -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)
}
}
}
@@ -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);
@@ -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() {
@@ -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
)
}
@@ -193,6 +193,14 @@ public class OutgoingMediaMessage {
return false;
}
public boolean isRequestToActivatePayments() {
return false;
}
public boolean isPaymentsActivated() {
return false;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
@@ -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
}
}
@@ -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);
@@ -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();
}
}
@@ -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);
@@ -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() &&
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="14dp"
android:viewportWidth="16"
android:viewportHeight="14">
<path
android:pathData="M13.35,1H2.65C2.08,1 1.533,1.241 1.13,1.669C0.727,2.097 0.5,2.678 0.5,3.283V10.717C0.5,11.322 0.727,11.903 1.13,12.331C1.533,12.759 2.08,13 2.65,13H13.35C13.92,13 14.467,12.759 14.87,12.331C15.274,11.903 15.5,11.322 15.5,10.717V3.283C15.5,2.678 15.274,2.097 14.87,1.669C14.467,1.241 13.92,1 13.35,1ZM1.3,4.823V3.283C1.3,3.097 1.335,2.913 1.404,2.742C1.472,2.571 1.572,2.417 1.698,2.288C1.824,2.159 1.974,2.058 2.137,1.992C2.301,1.926 2.475,1.896 2.65,1.903H13.35C13.525,1.896 13.699,1.926 13.863,1.992C14.026,2.058 14.176,2.159 14.302,2.288C14.428,2.417 14.528,2.571 14.597,2.742C14.665,2.913 14.7,3.097 14.7,3.283V4.823H1.3ZM2.65,12.097C2.475,12.104 2.301,12.074 2.137,12.008C1.974,11.942 1.824,11.841 1.698,11.712C1.572,11.583 1.472,11.429 1.404,11.258C1.335,11.087 1.3,10.903 1.3,10.717V6.522H14.7V10.717C14.7,10.903 14.665,11.087 14.597,11.258C14.528,11.429 14.428,11.583 14.302,11.712C14.176,11.841 14.026,11.942 13.863,12.008C13.699,12.074 13.525,12.104 13.35,12.097H2.65Z"
android:strokeWidth="0.25"
android:fillColor="#5E5E5E"
android:strokeColor="#5E5E5E"/>
</vector>
+29
View File
@@ -105,6 +105,14 @@
<string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">Signal requires the Storage permission in order to attach photos, videos, or audio, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Storage\".</string>
<string name="AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information">Signal requires Contacts permission in order to attach contact information, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Contacts\".</string>
<string name="AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location">Signal requires Location permission in order to attach a location, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Location\".</string>
<!-- Alert dialog title to show the recipient has not activated payments -->
<string name="AttachmentManager__not_activated_payments">%1$s hasn\t activated Payments </string>
<!-- Alert dialog description to send the recipient a request to activate payments -->
<string name="AttachmentManager__request_to_activate_payments">Do you want to send them a request to activate Payments?</string>
<!-- Alert dialog button to send request -->
<string name="AttachmentManager__send_request">Send request</string>
<!-- Alert dialog button to cancel dialog -->
<string name="AttachmentManager__cancel">Cancel</string>
<!-- AttachmentUploadJob -->
<string name="AttachmentUploadJob_uploading_media">Uploading media…</string>
@@ -1411,6 +1419,14 @@
<string name="MessageRecord_your_message_history_with_s_and_their_number_s_has_been_merged">Your message history with %1$s and their number %2$s has been merged.</string>
<!-- Update item message shown when we merge two threads together and we don't know the phone number of the other thread -->
<string name="MessageRecord_your_message_history_with_s_and_another_chat_has_been_merged">Your message history with %1$s and another chat that belonged to them has been merged.</string>
<!-- Message to notify sender that activate payments request has been sent to the recipient -->
<string name="MessageRecord_you_sent_request">You sent %s a request to activate Payments</string>
<!-- Request message from recipient to activate payments -->
<string name="MessageRecord_wants_you_to_activate_payments">%s wants you to activate Payments. Only send payments to people you trust.</string>
<!-- Message to inform user that payments was activated-->
<string name="MessageRecord_you_activated_payments">You activated Payments</string>
<!-- Message to inform sender that recipient can now accept payments -->
<string name="MessageRecord_can_accept_payments">%s can now accept Payments</string>
<!-- Group Calling update messages -->
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
@@ -1904,6 +1920,14 @@
<string name="ThreadRecord_view_once_media">View-once media</string>
<string name="ThreadRecord_this_message_was_deleted">This message was deleted.</string>
<string name="ThreadRecord_you_deleted_this_message">You deleted this message.</string>
<!-- Displayed in the notification when the user sends a request to activate payments -->
<string name="ThreadRecord_you_sent_request">You sent a request to activate Payments</string>
<!-- Displayed in the notification when the recipient wants to activate payments -->
<string name="ThreadRecord_wants_you_to_activate_payments">%s wants you to activate Payments</string>
<!-- Displayed in the notification when the user activates payments -->
<string name="ThreadRecord_you_activated_payments">You activated Payments</string>
<!-- Displayed in the notification when the recipient can accept payments -->
<string name="ThreadRecord_can_accept_payments">%s can now accept Payments</string>
<string name="ThreadRecord_s_is_on_signal">%s is on Signal!</string>
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
@@ -2260,6 +2284,11 @@
<string name="ConversationUpdateItem_the_disappearing_message_time_will_be_set_to_s_when_you_message_them">The disappearing message time will be set to %1$s when you message them.</string>
<!-- Update item button text to show to boost a feature -->
<string name="ConversationUpdateItem_donate">Donate</string>
<!-- Update item button text to send payment -->
<string name="ConversationUpdateItem_send_payment">Send payment</string>
<!-- Update item button text to activate payments -->
<string name="ConversationUpdateItem_activate_payments">Activate payments</string>
<!-- audio_view -->
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
@@ -52,7 +52,9 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PUSH_MESSAGE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SECURE_MESSAGE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SMS_EXPORT_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPES_MASK
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_GIFT_BADGE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_STORY_REACTION
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.THREAD_MERGE_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.UNSUPPORTED_MESSAGE_TYPE
@@ -119,6 +121,8 @@ object MessageBitmaskColumnTransformer : ColumnTransformer {
isSpecialType:${type and SPECIAL_TYPES_MASK != 0L}
isStoryReaction:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_STORY_REACTION}
isGiftBadge:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_GIFT_BADGE}
isRequestToActivatePayments:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST}
isPaymentsActivated:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_PAYMENTS_ACTIVATED}
""".trimIndent()
return "$type<br><br>" + describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "<br>")
@@ -78,6 +78,7 @@ fun Cursor.readToSingleLong(defaultValue: Long = 0): Long {
}
}
@JvmOverloads
inline fun <T> Cursor.readToList(predicate: (T) -> Boolean = { true }, mapper: (Cursor) -> T): List<T> {
val list = mutableListOf<T>()
use {
@@ -1028,8 +1028,11 @@ public class SignalServiceMessageSender {
.setMobileCoin(mobileCoinPayment);
builder.setPayment(DataMessage.Payment.newBuilder().setNotification(paymentBuilder));
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.PAYMENTS_VALUE, builder.getRequiredProtocolVersion()));
} else if (payment.getPaymentActivation().isPresent()) {
DataMessage.Payment.Activation.Builder activationBuilder = DataMessage.Payment.Activation.newBuilder().setType(payment.getPaymentActivation().get().getType());
builder.setPayment(DataMessage.Payment.newBuilder().setActivation(activationBuilder));
}
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.PAYMENTS_VALUE, builder.getRequiredProtocolVersion()));
}
if (message.getStoryContext().isPresent()) {
@@ -1240,8 +1240,12 @@ public final class SignalServiceContent {
SignalServiceProtos.DataMessage.Payment payment = content.getPayment();
switch (payment.getItemCase()) {
case NOTIFICATION: return new SignalServiceDataMessage.Payment(createPaymentNotification(payment));
default : throw new InvalidMessageStructureException("Unknown payment item");
case NOTIFICATION:
return new SignalServiceDataMessage.Payment(createPaymentNotification(payment), null);
case ACTIVATION:
return new SignalServiceDataMessage.Payment(null, createPaymentActivation(payment));
default:
throw new InvalidMessageStructureException("Unknown payment item");
}
}
@@ -1290,6 +1294,20 @@ public final class SignalServiceContent {
return new SignalServiceDataMessage.PaymentNotification(payment.getMobileCoin().getReceipt().toByteArray(), payment.getNote());
}
private static SignalServiceDataMessage.PaymentActivation createPaymentActivation(SignalServiceProtos.DataMessage.Payment content)
throws InvalidMessageStructureException
{
if (!content.hasActivation() ||
content.getItemCase() != SignalServiceProtos.DataMessage.Payment.ItemCase.ACTIVATION)
{
throw new InvalidMessageStructureException("Badly-formatted payment activation!");
}
SignalServiceProtos.DataMessage.Payment.Activation payment = content.getActivation();
return new SignalServiceDataMessage.PaymentActivation(payment.getType());
}
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException {
if (content.getContactCount() <= 0) return null;
@@ -161,6 +161,18 @@ public class SignalServiceDataMessage {
return expirationUpdate;
}
public boolean isActivatePaymentsRequest() {
return getPayment().isPresent() &&
getPayment().get().getPaymentActivation().isPresent() &&
getPayment().get().getPaymentActivation().get().getType().equals(SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST);
}
public boolean isPaymentsActivated() {
return getPayment().isPresent() &&
getPayment().get().getPaymentActivation().isPresent() &&
getPayment().get().getPaymentActivation().get().getType().equals(SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED);
}
public boolean isProfileKeyUpdate() {
return profileKeyUpdate;
}
@@ -655,16 +667,34 @@ public class SignalServiceDataMessage {
}
}
public static class PaymentActivation {
private final SignalServiceProtos.DataMessage.Payment.Activation.Type type;
public PaymentActivation(SignalServiceProtos.DataMessage.Payment.Activation.Type type) {
this.type = type;
}
public SignalServiceProtos.DataMessage.Payment.Activation.Type getType() {
return type;
}
}
public static class Payment {
private final Optional<PaymentNotification> paymentNotification;
private final Optional<PaymentActivation> paymentActivation;
public Payment(PaymentNotification paymentNotification) {
this.paymentNotification = Optional.of(paymentNotification);
public Payment(PaymentNotification paymentNotification, PaymentActivation paymentActivation) {
this.paymentNotification = Optional.ofNullable(paymentNotification);
this.paymentActivation = Optional.ofNullable(paymentActivation);
}
public Optional<PaymentNotification> getPaymentNotification() {
return paymentNotification;
}
public Optional<PaymentActivation> getPaymentActivation() {
return paymentActivation;
}
}
public static class StoryContext {
@@ -292,8 +292,18 @@ message DataMessage {
optional string note = 2;
}
message Activation {
enum Type {
REQUEST = 0;
ACTIVATED = 1;
}
optional Type type = 1;
}
oneof Item {
Notification notification = 1;
Activation activation = 2;
}
}