Add pre-alpha receive support for remote delete.

This commit is contained in:
Greyson Parrelli 2020-04-15 14:56:58 -04:00
parent 456bcf3d57
commit 6ecd3b59fd
29 changed files with 595 additions and 82 deletions

View File

@ -110,6 +110,7 @@ import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -602,6 +603,14 @@ public class ConversationFragment extends Fragment
}
private void handleDeleteMessages(final Set<MessageRecord> messageRecords) {
if (FeatureFlags.remoteDelete()) {
buildRemoteDeleteConfirmationDialog(messageRecords).show();
} else {
buildLegacyDeleteConfirmationDialog(messageRecords).show();
}
}
private AlertDialog.Builder buildLegacyDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
int messagesCount = messageRecords.size();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
@ -610,40 +619,87 @@ public class ConversationFragment extends Fragment
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ProgressDialogAsyncTask<MessageRecord, Void, Void>(getActivity(),
R.string.ConversationFragment_deleting,
R.string.ConversationFragment_deleting_messages)
{
@Override
protected Void doInBackground(MessageRecord... messageRecords) {
for (MessageRecord messageRecord : messageRecords) {
boolean threadDeleted;
builder.setPositiveButton(R.string.delete, (dialog, which) -> {
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
R.string.ConversationFragment_deleting,
R.string.ConversationFragment_deleting_messages)
{
@Override
protected Void doInBackground(Void... voids) {
for (MessageRecord messageRecord : messageRecords) {
boolean threadDeleted;
if (messageRecord.isMms()) {
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
} else {
threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
}
if (threadDeleted) {
threadId = -1;
listener.setThreadId(threadId);
}
if (messageRecord.isMms()) {
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
} else {
threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
}
return null;
if (threadDeleted) {
threadId = -1;
listener.setThreadId(threadId);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, messageRecords.toArray(new MessageRecord[messageRecords.size()]));
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
return builder;
}
private AlertDialog.Builder buildRemoteDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
Context context = requireActivity();
int messagesCount = messageRecords.size();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true);
builder.setPositiveButton(R.string.ConversationFragment_delete_for_me, (dialog, which) -> {
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
R.string.ConversationFragment_deleting,
R.string.ConversationFragment_deleting_messages)
{
@Override
protected Void doInBackground(Void... voids) {
for (MessageRecord messageRecord : messageRecords) {
boolean threadDeleted;
if (messageRecord.isMms()) {
threadDeleted = DatabaseFactory.getMmsDatabase(context).delete(messageRecord.getId());
} else {
threadDeleted = DatabaseFactory.getSmsDatabase(context).deleteMessage(messageRecord.getId());
}
if (threadDeleted) {
threadId = -1;
listener.setThreadId(threadId);
}
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis())) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
SignalExecutors.BOUNDED.execute(() -> {
for (MessageRecord message : messageRecords) {
MessageSender.sendRemoteDelete(context, message.getId(), message.isMms());
}
});
});
}
builder.setNegativeButton(android.R.string.cancel, null);
return builder;
}
private void handleDisplayDetails(MessageRecord message) {
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
@ -1086,6 +1142,7 @@ public class ConversationFragment extends Fragment
if (actionMode != null) return;
if (messageRecord.isSecure() &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isUpdate() &&
!recipient.get().isBlocked() &&
!messageRequestViewModel.shouldShowMessageRequest() &&

View File

@ -36,6 +36,8 @@ import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.AttributeSet;
@ -519,7 +521,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyText.setFocusable(false);
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
if (isCaptionlessMms(messageRecord)) {
if (messageRecord.isRemoteDelete()) {
String deletedMessage = context.getString(R.string.ConversationItem_this_message_was_deleted);
SpannableString italics = new SpannableString(deletedMessage);
italics.setSpan(new RelativeSizeSpan(0.9f), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
bodyText.setText(italics);
} else if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty());

View File

@ -432,6 +432,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());
toolbar.getMenu().findItem(R.id.action_forward).setVisible(menuState.shouldShowForwardAction());
toolbar.getMenu().findItem(R.id.action_reply).setVisible(menuState.shouldShowReplyAction());
}
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {

View File

@ -59,6 +59,7 @@ final class MenuState {
boolean hasText = false;
boolean sharedContact = false;
boolean viewOnce = false;
boolean remoteDelete = false;
for (MessageRecord messageRecord : messageRecords) {
if (isActionMessage(messageRecord))
@ -77,6 +78,10 @@ final class MenuState {
if (messageRecord.isViewOnce()) {
viewOnce = true;
}
if (messageRecord.isRemoteDelete()) {
remoteDelete = true;
}
}
if (messageRecords.size() > 1) {
@ -89,26 +94,27 @@ final class MenuState {
MessageRecord messageRecord = messageRecords.iterator().next();
builder.shouldShowResendAction(messageRecord.isFailed())
.shouldShowSaveAttachmentAction(!actionMessage &&
!viewOnce &&
messageRecord.isMms() &&
!messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).containsMediaSlide() &&
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce)
.shouldShowSaveAttachmentAction(!actionMessage &&
!viewOnce &&
messageRecord.isMms() &&
!messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).containsMediaSlide() &&
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete)
.shouldShowDetailsAction(!actionMessage)
.shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest));
}
return builder.shouldShowCopyAction(!actionMessage && hasText)
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
.build();
}
static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
return !actionMessage &&
!messageRecord.isPending() &&
!messageRecord.isFailed() &&
!isDisplayingMessageRequest &&
return !actionMessage &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isPending() &&
!messageRecord.isFailed() &&
!isDisplayingMessageRequest &&
messageRecord.isSecure();
}

View File

@ -50,6 +50,9 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markAsSent(long messageId, boolean secure);
public abstract void markUnidentified(long messageId, boolean unidentified);
public abstract void markAsSending(long messageId);
public abstract void markAsRemoteDelete(long messageId);
final int getInsecureMessagesSentForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = new String[]{"COUNT(*)"};

View File

@ -168,7 +168,8 @@ public class MmsDatabase extends MessagingDatabase {
VIEW_ONCE + " INTEGER DEFAULT 0, " +
REACTIONS + " BLOB DEFAULT NULL, " +
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1);";
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
REMOTE_DELETED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@ -193,6 +194,7 @@ public class MmsDatabase extends MessagingDatabase {
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
REMOTE_DELETED,
"json_group_array(json_object(" +
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
@ -473,6 +475,7 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
@Override
public void markAsSending(long messageId) {
long threadId = getThreadIdForMessage(messageId);
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId));
@ -492,6 +495,29 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId);
}
@Override
public void markAsRemoteDelete(long messageId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
values.putNull(QUOTE_BODY);
values.putNull(QUOTE_AUTHOR);
values.putNull(QUOTE_ATTACHMENT);
values.putNull(QUOTE_ID);
values.putNull(LINK_PREVIEWS);
values.putNull(SHARED_CONTACTS);
values.putNull(REACTIONS);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
long threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
}
public void markDownloadState(long messageId, long state) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
@ -1506,7 +1532,8 @@ public class MmsDatabase extends MessagingDatabase {
message.getSharedContacts(),
message.getLinkPreviews(),
false,
Collections.emptyList());
Collections.emptyList(),
false);
}
}
@ -1597,6 +1624,7 @@ public class MmsDatabase extends MessagingDatabase {
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REMOTE_DELETED)) == 1;
List<ReactionRecord> reactions = parseReactions(cursor);
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
@ -1618,7 +1646,8 @@ public class MmsDatabase extends MessagingDatabase {
addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount,
threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted,
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions);
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions,
remoteDelete);
}
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {

View File

@ -24,6 +24,7 @@ public interface MmsSmsColumns {
public static final String REACTIONS = "reactions";
public static final String REACTIONS_UNREAD = "reactions_unread";
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
public static final String REMOTE_DELETED = "remote_deleted";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;

View File

@ -28,6 +28,7 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.Pair;
@ -87,7 +88,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.READ,
MmsSmsColumns.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN};
MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.REMOTE_DELETED};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
@ -395,7 +397,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.DATE_SERVER };
MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED };
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -426,7 +429,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.DATE_SERVER };
MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED };
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@ -478,6 +482,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.REACTIONS);
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
Set<String> smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID);
@ -503,6 +508,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(SmsDatabase.REACTIONS);
smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD);
smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN);
smsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
@SuppressWarnings("deprecation")
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null);

View File

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
import java.security.SecureRandom;
@ -103,7 +104,8 @@ public class SmsDatabase extends MessagingDatabase {
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
REACTIONS + " BLOB DEFAULT NULL, " +
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1);";
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
REMOTE_DELETED + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@ -124,7 +126,8 @@ public class SmsDatabase extends MessagingDatabase {
PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT,
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED,
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
REMOTE_DELETED
};
private final String OUTGOING_INSECURE_MESSAGE_CLAUSE = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + TYPE + " & " + Types.SECURE_MESSAGE_BIT + ")";
@ -314,6 +317,7 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (isSecure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0));
}
@Override
public void markAsSending(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE);
}
@ -322,6 +326,21 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.TOTAL_MASK, Types.MISSED_CALL_TYPE);
}
@Override
public void markAsRemoteDelete(long id) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
long threadId = getThreadIdForMessage(id);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
}
@Override
public void markUnidentified(long id, boolean unidentified) {
ContentValues contentValues = new ContentValues(1);
@ -913,7 +932,8 @@ public class SmsDatabase extends MessagingDatabase {
System.currentTimeMillis(),
0,
false,
Collections.emptyList());
Collections.emptyList(),
false);
}
}
@ -955,6 +975,7 @@ public class SmsDatabase extends MessagingDatabase {
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.REMOTE_DELETED)) == 1;
List<ReactionRecord> reactions = parseReactions(cursor);
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
@ -970,7 +991,7 @@ public class SmsDatabase extends MessagingDatabase {
dateSent, dateReceived, dateServer, deliveryReceiptCount, type,
threadId, status, mismatches, subscriptionId,
expiresIn, expireStarted,
readReceiptCount, unidentified, reactions);
readReceiptCount, unidentified, reactions, remoteDelete);
}
private List<IdentityKeyMismatch> getMismatches(String document) {

View File

@ -773,8 +773,10 @@ public class ThreadDatabase extends Database {
return Extra.forMessageRequest();
}
if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
return Extra.forRevealable();
if (record.isViewOnce()) {
return Extra.forViewOnce();
} else if (record.isRemoteDelete()) {
return Extra.forRemoteDelete();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
return Extra.forSticker();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
@ -899,43 +901,50 @@ public class ThreadDatabase extends Database {
@JsonProperty private final boolean isRevealable;
@JsonProperty private final boolean isSticker;
@JsonProperty private final boolean isAlbum;
@JsonProperty private final boolean isRemoteDelete;
@JsonProperty private final boolean isMessageRequestAccepted;
@JsonProperty private final String groupAddedBy;
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
@JsonProperty("isSticker") boolean isSticker,
@JsonProperty("isAlbum") boolean isAlbum,
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
@JsonProperty("groupAddedBy") String groupAddedBy)
{
this.isRevealable = isRevealable;
this.isSticker = isSticker;
this.isAlbum = isAlbum;
this.isRemoteDelete = isRemoteDelete;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.groupAddedBy = groupAddedBy;
}
public static @NonNull Extra forRevealable() {
return new Extra(true, false, false, true, null);
public static @NonNull Extra forViewOnce() {
return new Extra(true, false, false, false, true, null);
}
public static @NonNull Extra forSticker() {
return new Extra(false, true, false, true, null);
return new Extra(false, true, false, false, true, null);
}
public static @NonNull Extra forAlbum() {
return new Extra(false, false, true, true, null);
return new Extra(false, false, true, false, true, null);
}
public static @NonNull Extra forRemoteDelete() {
return new Extra(false, false, false, true, true, null);
}
public static @NonNull Extra forMessageRequest() {
return new Extra(false, false, false, false, null);
return new Extra(false, false, false, false, false, null);
}
public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
return new Extra(false, false, false, false, recipientId.serialize());
return new Extra(false, false, false, false, false, recipientId.serialize());
}
public boolean isRevealable() {
public boolean isViewOnce() {
return isRevealable;
}
@ -947,6 +956,10 @@ public class ThreadDatabase extends Database {
return isAlbum;
}
public boolean isRemoteDelete() {
return isRemoteDelete;
}
public boolean isMessageRequestAccepted() {
return isMessageRequestAccepted;
}

View File

@ -129,8 +129,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int ATTACHMENT_CDN_NUMBER = 57;
private static final int JOB_INPUT_DATA = 58;
private static final int SERVER_TIMESTAMP = 59;
private static final int REMOTE_DELETE = 60;
private static final int DATABASE_VERSION = 59;
private static final int DATABASE_VERSION = 60;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -882,6 +883,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("CREATE INDEX IF NOT EXISTS mms_date_server_index ON mms (date_server)");
}
if (oldVersion < REMOTE_DELETE) {
db.execSQL("ALTER TABLE sms ADD COLUMN remote_deleted INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE mms ADD COLUMN remote_deleted INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -70,12 +70,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews,
boolean unidentified,
@NonNull List<ReactionRecord> reactions)
@NonNull List<ReactionRecord> reactions,
boolean remoteDelete)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions);
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete);
this.partCount = partCount;
}

View File

@ -55,6 +55,7 @@ public abstract class MessageRecord extends DisplayRecord {
private final boolean unidentified;
private final List<ReactionRecord> reactions;
private final long serverTimestamp;
private final boolean remoteDelete;
MessageRecord(long id, String body, Recipient conversationRecipient,
Recipient individualRecipient, int recipientDeviceId,
@ -64,7 +65,7 @@ public abstract class MessageRecord extends DisplayRecord {
List<NetworkFailure> networkFailures,
int subscriptionId, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified,
@NonNull List<ReactionRecord> reactions)
@NonNull List<ReactionRecord> reactions, boolean remoteDelete)
{
super(body, conversationRecipient, dateSent, dateReceived,
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount);
@ -79,6 +80,7 @@ public abstract class MessageRecord extends DisplayRecord {
this.unidentified = unidentified;
this.reactions = reactions;
this.serverTimestamp = dateServer;
this.remoteDelete = remoteDelete;
}
public abstract boolean isMms();
@ -259,6 +261,10 @@ public abstract class MessageRecord extends DisplayRecord {
return false;
}
public boolean isRemoteDelete() {
return remoteDelete;
}
public @NonNull List<ReactionRecord> getReactions() {
return reactions;
}

View File

@ -33,9 +33,9 @@ public abstract class MmsMessageRecord extends MessageRecord {
@NonNull SlideDeck slideDeck, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
@NonNull List<ReactionRecord> reactions)
@NonNull List<ReactionRecord> reactions, boolean remoteDelete)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete);
this.slideDeck = slideDeck;
this.quote = quote;

View File

@ -58,7 +58,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new LinkedList<>(), new LinkedList<>(), subscriptionId,
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false,
Collections.emptyList());
Collections.emptyList(), false);
this.contentLocation = contentLocation;
this.messageSize = messageSize;

View File

@ -49,12 +49,12 @@ public class SmsMessageRecord extends MessageRecord {
int status, List<IdentityKeyMismatch> mismatches,
int subscriptionId, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified,
@NonNull List<ReactionRecord> reactions)
@NonNull List<ReactionRecord> reactions, boolean remoteDelete)
{
super(id, body, recipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, dateServer, threadId, status, deliveryReceiptCount, type,
mismatches, new LinkedList<>(), subscriptionId,
expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete);
}
public long getType() {

View File

@ -127,8 +127,10 @@ public class ThreadRecord extends DisplayRecord {
if (TextUtils.isEmpty(getBody())) {
if (extra != null && extra.isSticker()) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
} else if (extra != null && extra.isRevealable()) {
} else if (extra != null && extra.isViewOnce()) {
return new SpannableString(emphasisAdded(getViewOnceDescription(context, contentType)));
} else if (extra != null && extra.isRemoteDelete()) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_this_message_was_deleted)));
} else {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
}

View File

@ -86,6 +86,7 @@ public final class JobManagerFactories {
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory());
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());

View File

@ -79,6 +79,7 @@ import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional;
@ -115,6 +116,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public final class PushProcessMessageJob extends BaseJob {
@ -273,13 +275,14 @@ public final class PushProcessMessageJob extends BaseJob {
return;
}
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
handleUnknownGroupMessage(content, message.getGroupContext().get());
@ -606,7 +609,7 @@ public final class PushProcessMessageJob extends BaseJob {
Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor());
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId());
if (targetMessage != null) {
if (targetMessage != null && !targetMessage.isRemoteDelete()) {
Recipient reactionAuthor = Recipient.externalPush(context, content.getSender());
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
@ -618,12 +621,31 @@ public final class PushProcessMessageJob extends BaseJob {
db.addReaction(targetMessage.getId(), reactionRecord);
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false);
}
} else if (targetMessage != null) {
Log.w(TAG, "[handleReaction] Found a matching message, but it's flagged as remotely deleted. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
} else {
Log.w(TAG, "[handleReaction] Could not find matching message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
}
}
private void handleRemoteDelete(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
SignalServiceDataMessage.RemoteDelete delete = message.getRemoteDelete().get();
Recipient sender = Recipient.externalPush(context, content.getSender());
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(delete.getTargetSentTimestamp(), sender.getId());
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerTimestamp())) {
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
db.markAsRemoteDelete(targetMessage.getId());
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false);
} else if (targetMessage == null) {
Log.w(TAG, "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + sender.getId());
} else {
Log.w(TAG, String.format(Locale.ENGLISH, "[handleRemoteDelete] Invalid remote delete! deleteTime: %d, targetTime: %d, deleteAuthor: %s, targetAuthor: %s",
content.getServerTimestamp(), targetMessage.getServerTimestamp(), sender.getId(), targetMessage.getRecipient().getId()));
}
}
private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
IdentityUtil.processVerifiedMessage(context, verifiedMessage);
}
@ -751,6 +773,8 @@ public final class PushProcessMessageJob extends BaseJob {
handleReaction(content, message.getMessage());
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
threadId = threadId != -1 ? threadId : null;
} else if (message.getMessage().getRemoteDelete().isPresent()) {
handleRemoteDelete(content, message.getMessage());
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce()) {
threadId = handleSynchronizeSentMediaMessage(message);
} else {
@ -1392,7 +1416,7 @@ public final class PushProcessMessageJob extends BaseJob {
RecipientId author = Recipient.externalPush(context, quote.get().getAuthor()).getId();
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quote.get().getId(), author);
if (message != null) {
if (message != null && !message.isRemoteDelete()) {
Log.i(TAG, "Found matching message record...");
List<Attachment> attachments = new LinkedList<>();
@ -1415,6 +1439,8 @@ public final class PushProcessMessageJob extends BaseJob {
}
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
} else if (message != null) {
Log.w(TAG, "Found the target for the quote, but it's flagged as remotely deleted.");
}
Log.w(TAG, "Didn't find matching message record...");

View File

@ -0,0 +1,212 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class RemoteDeleteSendJob extends BaseJob {
public static final String KEY = "RemoteDeleteSendJob";
private static final String TAG = Log.tag(RemoteDeleteSendJob.class);
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_IS_MMS = "is_mms";
private static final String KEY_RECIPIENTS = "recipients";
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
private final long messageId;
private final boolean isMms;
private final List<RecipientId> recipients;
private final int initialRecipientCount;
@WorkerThread
public static @NonNull RemoteDeleteSendJob create(@NonNull Context context,
long messageId,
boolean isMms)
throws NoSuchMessageException
{
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
if (conversationRecipient == null) {
throw new AssertionError("We have a message, but couldn't find the thread!");
}
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList();
recipients.remove(Recipient.self().getId());
return new RemoteDeleteSendJob(messageId,
isMms,
recipients,
recipients.size(),
new Parameters.Builder()
.setQueue(conversationRecipient.getId().toQueueKey())
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build());
}
private RemoteDeleteSendJob(long messageId,
boolean isMms,
@NonNull List<RecipientId> recipients,
int initialRecipientCount,
@NonNull Parameters parameters)
{
super(parameters);
this.messageId = messageId;
this.isMms = isMms;
this.recipients = recipients;
this.initialRecipientCount = initialRecipientCount;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putBoolean(KEY_IS_MMS, isMms)
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
.putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
protected void onRun() throws Exception {
MessagingDatabase db;
MessageRecord message;
if (isMms) {
db = DatabaseFactory.getMmsDatabase(context);
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
} else {
db = DatabaseFactory.getSmsDatabase(context);
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
}
long targetSentTimestamp = message.getDateSent();
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
if (conversationRecipient == null) {
throw new AssertionError("We have a message, but couldn't find the thread!");
}
if (!message.isOutgoing()) {
throw new IllegalStateException("Cannot delete a message that isn't yours!");
}
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
List<Recipient> completions = deliver(conversationRecipient, destinations, targetSentTimestamp);
for (Recipient completion : completions) {
recipients.remove(completion.getId());
}
Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size());
if (recipients.isEmpty()) {
db.markAsSent(messageId, true);
} else {
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
throw new RetryLaterException();
}
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof IOException ||
e instanceof RetryLaterException;
}
@Override
public void onFailure() {
Log.w(TAG, "Failed to send the reaction to all recipients! (" + (initialRecipientCount - recipients.size() + "/" + initialRecipientCount + ")") );
}
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, long targetSentTimestamp)
throws IOException, UntrustedIdentityException
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = Stream.of(destinations).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
if (conversationRecipient.isGroup()) {
dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId()));
}
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
Stream.of(results)
.filter(r -> r.getIdentityFailure() != null)
.map(SendMessageResult::getAddress)
.map(a -> Recipient.externalPush(context, a))
.forEach(r -> Log.w(TAG, "Identity failure for " + r.getId()));
Stream.of(results)
.filter(SendMessageResult::isUnregisteredFailure)
.map(SendMessageResult::getAddress)
.map(a -> Recipient.externalPush(context, a))
.forEach(r -> Log.w(TAG, "Unregistered failure for " + r.getId()));
return Stream.of(results)
.filter(r -> r.getSuccess() != null || r.getIdentityFailure() != null || r.isUnregisteredFailure())
.map(SendMessageResult::getAddress)
.map(a -> Recipient.externalPush(context, a))
.toList();
}
public static class Factory implements Job.Factory<RemoteDeleteSendJob> {
@Override
public @NonNull RemoteDeleteSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
long messageId = data.getLong(KEY_MESSAGE_ID);
boolean isMms = data.getBoolean(KEY_IS_MMS);
List<RecipientId> recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS));
int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT);
return new RemoteDeleteSendJob(messageId, isMms, recipients, initialRecipientCount, parameters);
}
}
}

View File

@ -527,6 +527,8 @@ public class MessageNotifier {
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
} else if (record.isRemoteDelete()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted));;
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck();

View File

@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException;
@ -309,6 +310,19 @@ public class MessageSender {
}
}
public static void sendRemoteDelete(@NonNull Context context, long messageId, boolean isMms) {
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
db.markAsRemoteDelete(messageId);
db.markAsSending(messageId);
try {
ApplicationDependencies.getJobManager().add(RemoteDeleteSendJob.create(context, messageId, isMms));
onMessageSent();
} catch (NoSuchMessageException e) {
Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring.");
}
}
public static void resendGroupMessage(Context context, MessageRecord messageRecord, RecipientId filterRecipientId) {
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientId, Collections.emptyList());

View File

@ -55,6 +55,7 @@ public final class FeatureFlags {
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
private static final String ATTACHMENTS_V3 = "android.attachmentsV3";
private static final String REMOTE_DELETE = "android.remoteDelete";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -68,7 +69,8 @@ public final class FeatureFlags {
PINS_MEGAPHONE_KILL_SWITCH,
PROFILE_NAMES_MEGAPHONE,
MESSAGE_REQUESTS,
ATTACHMENTS_V3
ATTACHMENTS_V3,
REMOTE_DELETE
);
/**
@ -218,6 +220,11 @@ public final class FeatureFlags {
return getValue(ATTACHMENTS_V3, false);
}
/** Send support for remotely deleting a message. */
public static boolean remoteDelete() {
return getValue(REMOTE_DELETE, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
public final class RemoteDeleteUtil {
private static final long RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(1);
private static final long SEND_THRESHOLD = TimeUnit.MINUTES.toMillis(30);
private RemoteDeleteUtil() {}
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull Recipient deleteSender, long deleteServerTimestamp) {
boolean isValidSender = (deleteSender.isLocalNumber() && targetMessage.isOutgoing()) ||
(!deleteSender.isLocalNumber() && !targetMessage.isOutgoing());
return isValidSender &&
targetMessage.getIndividualRecipient().equals(deleteSender) &&
(deleteServerTimestamp - targetMessage.getServerTimestamp()) < RECEIVE_THRESHOLD;
}
public static boolean isValidSend(@NonNull Collection<MessageRecord> targetMessages, long currentTime) {
// TODO [greyson] [remote-delete] Update with server timestamp when available for outgoing messages
return Stream.of(targetMessages)
.allMatch(message -> message.isOutgoing() &&
!message.isRemoteDelete() &&
!message.isPending() &&
(currentTime - message.getDateSent()) < SEND_THRESHOLD);
}
}

View File

@ -186,6 +186,7 @@
<string name="ConversationItem_read_more">&#160; Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string>
<string name="ConversationItem_this_message_was_deleted">This message was deleted.</string>
<!-- ConversationActivity -->
<string name="ConversationActivity_reset_secure_session_question">Reset secure session?</string>
@ -298,6 +299,8 @@
<string name="ConversationFragment_sms">SMS</string>
<string name="ConversationFragment_deleting">Deleting</string>
<string name="ConversationFragment_deleting_messages">Deleting messages…</string>
<string name="ConversationFragment_delete_for_me">Delete for me</string>
<string name="ConversationFragment_delete_for_everyone">Delete for everyone</string>
<string name="ConversationFragment_quoted_message_not_found">Original message not found</string>
<string name="ConversationFragment_quoted_message_no_longer_available">Original message no longer available</string>
<string name="ConversationFragment_failed_to_open_message">Failed to open message</string>
@ -959,7 +962,7 @@
<string name="SmsMessageRecord_secure_session_reset_s">%s reset the secure session.</string>
<string name="SmsMessageRecord_duplicate_message">Duplicate message.</string>
<string name="SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version">This message could not be processed because it was sent from a newer version of Signal. You can ask your contact to send this message again after you update.</string>
<string name="SmsMessageRecord_error_handling_incoming_message">Error handling incoming message</string>
<string name="SmsMessageRecord_error_handling_incoming_message">Error handling incoming message.</string>
<!-- StickerManagementActivity -->
<string name="StickerManagementActivity_stickers">Stickers</string>
@ -1006,6 +1009,7 @@
<string name="ThreadRecord_view_once_photo">View-once photo</string>
<string name="ThreadRecord_view_once_video">View-once video</string>
<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_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>
@ -1131,6 +1135,7 @@
<string name="MessageNotifier_reacted_s_to_your_view_once_photo">Reacted %1$s to your view-once photo.</string>
<string name="MessageNotifier_reacted_s_to_your_view_once_video">Reacted %1$s to your view-once video.</string>
<string name="MessageNotifier_reacted_s_to_your_sticker">Reacted %1$s to your sticker.</string>
<string name="MessageNotifier_this_message_was_deleted">This message was deleted.</string>
<!-- Notification Channels -->
<string name="NotificationChannel_messages">Default</string>

View File

@ -639,6 +639,13 @@ public class SignalServiceMessageSender {
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.REACTIONS_VALUE, builder.getRequiredProtocolVersion()));
}
if (message.getRemoteDelete().isPresent()) {
DataMessage.Delete delete = DataMessage.Delete.newBuilder()
.setTargetSentTimestamp(message.getRemoteDelete().get().getTargetSentTimestamp())
.build();
builder.setDelete(delete);
}
builder.setTimestamp(message.getTimestamp());
return container.setDataMessage(builder).build().toByteArray();

View File

@ -293,6 +293,7 @@ public final class SignalServiceContent {
List<SignalServiceDataMessage.Preview> previews = createPreviews(content);
SignalServiceDataMessage.Sticker sticker = createSticker(content);
SignalServiceDataMessage.Reaction reaction = createReaction(content);
SignalServiceDataMessage.RemoteDelete remoteDelete = createRemoteDelete(content);
if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE) {
throw new UnsupportedDataMessageProtocolVersionException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE,
@ -326,7 +327,8 @@ public final class SignalServiceContent {
previews,
sticker,
content.getIsViewOnce(),
reaction);
reaction,
remoteDelete);
}
private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata,
@ -660,6 +662,16 @@ public final class SignalServiceContent {
reaction.getTargetSentTimestamp());
}
private static SignalServiceDataMessage.RemoteDelete createRemoteDelete(SignalServiceProtos.DataMessage content) {
if (!content.hasDelete() || !content.getDelete().hasTargetSentTimestamp()) {
return null;
}
SignalServiceProtos.DataMessage.Delete delete = content.getDelete();
return new SignalServiceDataMessage.RemoteDelete(delete.getTargetSentTimestamp());
}
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
if (content.getContactCount() <= 0) return null;

View File

@ -35,6 +35,7 @@ public class SignalServiceDataMessage {
private final Optional<Sticker> sticker;
private final boolean viewOnce;
private final Optional<Reaction> reaction;
private final Optional<RemoteDelete> remoteDelete;
/**
* Construct a SignalServiceDataMessage.
@ -53,7 +54,7 @@ public class SignalServiceDataMessage {
String body, boolean endSession, int expiresInSeconds,
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker, boolean viewOnce, Reaction reaction)
Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete)
{
try {
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
@ -72,6 +73,7 @@ public class SignalServiceDataMessage {
this.sticker = Optional.fromNullable(sticker);
this.viewOnce = viewOnce;
this.reaction = Optional.fromNullable(reaction);
this.remoteDelete = Optional.fromNullable(remoteDelete);
if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments);
@ -174,6 +176,10 @@ public class SignalServiceDataMessage {
return reaction;
}
public Optional<RemoteDelete> getRemoteDelete() {
return remoteDelete;
}
public static class Builder {
private List<SignalServiceAttachment> attachments = new LinkedList<>();
@ -193,6 +199,7 @@ public class SignalServiceDataMessage {
private Sticker sticker;
private boolean viewOnce;
private Reaction reaction;
private RemoteDelete remoteDelete;
private Builder() {}
@ -300,12 +307,17 @@ public class SignalServiceDataMessage {
return this;
}
public Builder withRemoteDelete(RemoteDelete remoteDelete) {
this.remoteDelete = remoteDelete;
return this;
}
public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis();
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews,
sticker, viewOnce, reaction);
sticker, viewOnce, reaction, remoteDelete);
}
}
@ -446,4 +458,16 @@ public class SignalServiceDataMessage {
return targetSentTimestamp;
}
}
public static class RemoteDelete {
private final long targetSentTimestamp;
public RemoteDelete(long targetSentTimestamp) {
this.targetSentTimestamp = targetSentTimestamp;
}
public long getTargetSentTimestamp() {
return targetSentTimestamp;
}
}
}

View File

@ -184,6 +184,10 @@ message DataMessage {
optional uint64 targetSentTimestamp = 5;
}
message Delete {
optional uint64 targetSentTimestamp = 1;
}
enum ProtocolVersion {
option allow_alias = true;
@ -211,6 +215,7 @@ message DataMessage {
optional uint32 requiredProtocolVersion = 12;
optional bool isViewOnce = 14;
optional Reaction reaction = 16;
optional Delete delete = 17;
}
message NullMessage {