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.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil; import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -602,6 +603,14 @@ public class ConversationFragment extends Fragment
} }
private void handleDeleteMessages(final Set<MessageRecord> messageRecords) { 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(); int messagesCount = messageRecords.size();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 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.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true); builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { builder.setPositiveButton(R.string.delete, (dialog, which) -> {
@Override new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
public void onClick(DialogInterface dialog, int which) { R.string.ConversationFragment_deleting,
new ProgressDialogAsyncTask<MessageRecord, Void, Void>(getActivity(), R.string.ConversationFragment_deleting_messages)
R.string.ConversationFragment_deleting, {
R.string.ConversationFragment_deleting_messages) @Override
{ protected Void doInBackground(Void... voids) {
@Override for (MessageRecord messageRecord : messageRecords) {
protected Void doInBackground(MessageRecord... messageRecords) { boolean threadDeleted;
for (MessageRecord messageRecord : messageRecords) {
boolean threadDeleted;
if (messageRecord.isMms()) { if (messageRecord.isMms()) {
threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId()); threadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
} else { } else {
threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId()); threadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
}
if (threadDeleted) {
threadId = -1;
listener.setThreadId(threadId);
}
} }
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.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) { private void handleDisplayDetails(MessageRecord message) {
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class); Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId()); intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
@ -1086,6 +1142,7 @@ public class ConversationFragment extends Fragment
if (actionMode != null) return; if (actionMode != null) return;
if (messageRecord.isSecure() && if (messageRecord.isSecure() &&
!messageRecord.isRemoteDelete() &&
!messageRecord.isUpdate() && !messageRecord.isUpdate() &&
!recipient.get().isBlocked() && !recipient.get().isBlocked() &&
!messageRequestViewModel.shouldShowMessageRequest() && !messageRequestViewModel.shouldShowMessageRequest() &&

View File

@ -36,6 +36,8 @@ import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle; import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan; import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.AttributeSet; import android.util.AttributeSet;
@ -519,7 +521,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
bodyText.setFocusable(false); bodyText.setFocusable(false);
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context)); 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); bodyText.setVisibility(View.GONE);
} else { } else {
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty()); 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_copy).setVisible(menuState.shouldShowCopyAction());
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction()); 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_forward).setVisible(menuState.shouldShowForwardAction());
toolbar.getMenu().findItem(R.id.action_reply).setVisible(menuState.shouldShowReplyAction());
} }
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) { private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {

View File

@ -59,6 +59,7 @@ final class MenuState {
boolean hasText = false; boolean hasText = false;
boolean sharedContact = false; boolean sharedContact = false;
boolean viewOnce = false; boolean viewOnce = false;
boolean remoteDelete = false;
for (MessageRecord messageRecord : messageRecords) { for (MessageRecord messageRecord : messageRecords) {
if (isActionMessage(messageRecord)) if (isActionMessage(messageRecord))
@ -77,6 +78,10 @@ final class MenuState {
if (messageRecord.isViewOnce()) { if (messageRecord.isViewOnce()) {
viewOnce = true; viewOnce = true;
} }
if (messageRecord.isRemoteDelete()) {
remoteDelete = true;
}
} }
if (messageRecords.size() > 1) { if (messageRecords.size() > 1) {
@ -89,26 +94,27 @@ final class MenuState {
MessageRecord messageRecord = messageRecords.iterator().next(); MessageRecord messageRecord = messageRecords.iterator().next();
builder.shouldShowResendAction(messageRecord.isFailed()) builder.shouldShowResendAction(messageRecord.isFailed())
.shouldShowSaveAttachmentAction(!actionMessage && .shouldShowSaveAttachmentAction(!actionMessage &&
!viewOnce && !viewOnce &&
messageRecord.isMms() && messageRecord.isMms() &&
!messageRecord.isMmsNotification() && !messageRecord.isMmsNotification() &&
((MediaMmsMessageRecord)messageRecord).containsMediaSlide() && ((MediaMmsMessageRecord)messageRecord).containsMediaSlide() &&
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null) ((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce) .shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete)
.shouldShowDetailsAction(!actionMessage) .shouldShowDetailsAction(!actionMessage)
.shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest)); .shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest));
} }
return builder.shouldShowCopyAction(!actionMessage && hasText) return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
.build(); .build();
} }
static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) { static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
return !actionMessage && return !actionMessage &&
!messageRecord.isPending() && !messageRecord.isRemoteDelete() &&
!messageRecord.isFailed() && !messageRecord.isPending() &&
!isDisplayingMessageRequest && !messageRecord.isFailed() &&
!isDisplayingMessageRequest &&
messageRecord.isSecure(); 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 markAsSent(long messageId, boolean secure);
public abstract void markUnidentified(long messageId, boolean unidentified); 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) { final int getInsecureMessagesSentForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
String[] projection = new String[]{"COUNT(*)"}; String[] projection = new String[]{"COUNT(*)"};

View File

@ -168,7 +168,8 @@ public class MmsDatabase extends MessagingDatabase {
VIEW_ONCE + " INTEGER DEFAULT 0, " + VIEW_ONCE + " INTEGER DEFAULT 0, " +
REACTIONS + " BLOB DEFAULT NULL, " + REACTIONS + " BLOB DEFAULT NULL, " +
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", "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, 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, 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, SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
REMOTE_DELETED,
"json_group_array(json_object(" + "json_group_array(json_object(" +
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
@ -473,6 +475,7 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
} }
@Override
public void markAsSending(long messageId) { public void markAsSending(long messageId) {
long threadId = getThreadIdForMessage(messageId); long threadId = getThreadIdForMessage(messageId);
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId)); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId));
@ -492,6 +495,29 @@ public class MmsDatabase extends MessagingDatabase {
notifyConversationListeners(threadId); 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) { public void markDownloadState(long messageId, long state) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
@ -1506,7 +1532,8 @@ public class MmsDatabase extends MessagingDatabase {
message.getSharedContacts(), message.getSharedContacts(),
message.getLinkPreviews(), message.getLinkPreviews(),
false, false,
Collections.emptyList()); Collections.emptyList(),
false);
} }
} }
@ -1597,6 +1624,7 @@ public class MmsDatabase extends MessagingDatabase {
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED)); long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1; boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 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); List<ReactionRecord> reactions = parseReactions(cursor);
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
@ -1618,7 +1646,8 @@ public class MmsDatabase extends MessagingDatabase {
addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount, addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount,
threadId, body, slideDeck, partCount, box, mismatches, threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted, 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) { 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 = "reactions";
public static final String REACTIONS_UNREAD = "reactions_unread"; public static final String REACTIONS_UNREAD = "reactions_unread";
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
public static final String REMOTE_DELETED = "remote_deleted";
public static class Types { public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF; 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.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
@ -87,7 +88,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.READ, MmsSmsColumns.READ,
MmsSmsColumns.REACTIONS, MmsSmsColumns.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN}; MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.REMOTE_DELETED};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
@ -395,7 +397,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.REACTIONS, MmsDatabase.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.DATE_SERVER }; MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED };
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -426,7 +429,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.REACTIONS, MmsDatabase.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.DATE_SERVER }; MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED };
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@ -478,6 +482,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.REACTIONS); mmsColumnsPresent.add(MmsDatabase.REACTIONS);
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD); mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN); mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
Set<String> smsColumnsPresent = new HashSet<>(); Set<String> smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID); smsColumnsPresent.add(MmsSmsColumns.ID);
@ -503,6 +508,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(SmsDatabase.REACTIONS); smsColumnsPresent.add(SmsDatabase.REACTIONS);
smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD); smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD);
smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN); smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN);
smsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null); 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.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -103,7 +104,8 @@ public class SmsDatabase extends MessagingDatabase {
UNIDENTIFIED + " INTEGER DEFAULT 0, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " +
REACTIONS + " BLOB DEFAULT NULL, " + REACTIONS + " BLOB DEFAULT NULL, " +
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", "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, PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT, REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT,
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED, 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 + ")"; 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)); 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) { public void markAsSending(long id) {
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE); 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); 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 @Override
public void markUnidentified(long id, boolean unidentified) { public void markUnidentified(long id, boolean unidentified) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
@ -913,7 +932,8 @@ public class SmsDatabase extends MessagingDatabase {
System.currentTimeMillis(), System.currentTimeMillis(),
0, 0,
false, false,
Collections.emptyList()); Collections.emptyList(),
false);
} }
} }
@ -955,6 +975,7 @@ public class SmsDatabase extends MessagingDatabase {
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED)); long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)); String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1; boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.REMOTE_DELETED)) == 1;
List<ReactionRecord> reactions = parseReactions(cursor); List<ReactionRecord> reactions = parseReactions(cursor);
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
@ -970,7 +991,7 @@ public class SmsDatabase extends MessagingDatabase {
dateSent, dateReceived, dateServer, deliveryReceiptCount, type, dateSent, dateReceived, dateServer, deliveryReceiptCount, type,
threadId, status, mismatches, subscriptionId, threadId, status, mismatches, subscriptionId,
expiresIn, expireStarted, expiresIn, expireStarted,
readReceiptCount, unidentified, reactions); readReceiptCount, unidentified, reactions, remoteDelete);
} }
private List<IdentityKeyMismatch> getMismatches(String document) { private List<IdentityKeyMismatch> getMismatches(String document) {

View File

@ -773,8 +773,10 @@ public class ThreadDatabase extends Database {
return Extra.forMessageRequest(); return Extra.forMessageRequest();
} }
if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) { if (record.isViewOnce()) {
return Extra.forRevealable(); return Extra.forViewOnce();
} else if (record.isRemoteDelete()) {
return Extra.forRemoteDelete();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) { } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
return Extra.forSticker(); return Extra.forSticker();
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) { } 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 isRevealable;
@JsonProperty private final boolean isSticker; @JsonProperty private final boolean isSticker;
@JsonProperty private final boolean isAlbum; @JsonProperty private final boolean isAlbum;
@JsonProperty private final boolean isRemoteDelete;
@JsonProperty private final boolean isMessageRequestAccepted; @JsonProperty private final boolean isMessageRequestAccepted;
@JsonProperty private final String groupAddedBy; @JsonProperty private final String groupAddedBy;
public Extra(@JsonProperty("isRevealable") boolean isRevealable, public Extra(@JsonProperty("isRevealable") boolean isRevealable,
@JsonProperty("isSticker") boolean isSticker, @JsonProperty("isSticker") boolean isSticker,
@JsonProperty("isAlbum") boolean isAlbum, @JsonProperty("isAlbum") boolean isAlbum,
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted, @JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
@JsonProperty("groupAddedBy") String groupAddedBy) @JsonProperty("groupAddedBy") String groupAddedBy)
{ {
this.isRevealable = isRevealable; this.isRevealable = isRevealable;
this.isSticker = isSticker; this.isSticker = isSticker;
this.isAlbum = isAlbum; this.isAlbum = isAlbum;
this.isRemoteDelete = isRemoteDelete;
this.isMessageRequestAccepted = isMessageRequestAccepted; this.isMessageRequestAccepted = isMessageRequestAccepted;
this.groupAddedBy = groupAddedBy; this.groupAddedBy = groupAddedBy;
} }
public static @NonNull Extra forRevealable() { public static @NonNull Extra forViewOnce() {
return new Extra(true, false, false, true, null); return new Extra(true, false, false, false, true, null);
} }
public static @NonNull Extra forSticker() { 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() { 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() { 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) { 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; return isRevealable;
} }
@ -947,6 +956,10 @@ public class ThreadDatabase extends Database {
return isAlbum; return isAlbum;
} }
public boolean isRemoteDelete() {
return isRemoteDelete;
}
public boolean isMessageRequestAccepted() { public boolean isMessageRequestAccepted() {
return 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 ATTACHMENT_CDN_NUMBER = 57;
private static final int JOB_INPUT_DATA = 58; private static final int JOB_INPUT_DATA = 58;
private static final int SERVER_TIMESTAMP = 59; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; 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)"); 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

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

View File

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

View File

@ -33,9 +33,9 @@ public abstract class MmsMessageRecord extends MessageRecord {
@NonNull SlideDeck slideDeck, int readReceiptCount, @NonNull SlideDeck slideDeck, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts, @Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified, @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.slideDeck = slideDeck;
this.quote = quote; this.quote = quote;

View File

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

View File

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

View File

@ -127,8 +127,10 @@ public class ThreadRecord extends DisplayRecord {
if (TextUtils.isEmpty(getBody())) { if (TextUtils.isEmpty(getBody())) {
if (extra != null && extra.isSticker()) { if (extra != null && extra.isSticker()) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker))); 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))); 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 { } else {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message))); 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(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory()); put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory());
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory()); put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.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.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -115,6 +116,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
public final class PushProcessMessageJob extends BaseJob { public final class PushProcessMessageJob extends BaseJob {
@ -273,13 +275,14 @@ public final class PushProcessMessageJob extends BaseJob {
return; return;
} }
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId); if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId); else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId); else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
else if (message.getReaction().isPresent()) handleReaction(content, message); else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId); else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId); else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
handleUnknownGroupMessage(content, message.getGroupContext().get()); handleUnknownGroupMessage(content, message.getGroupContext().get());
@ -606,7 +609,7 @@ public final class PushProcessMessageJob extends BaseJob {
Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor()); Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor());
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId()); 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()); Recipient reactionAuthor = Recipient.externalPush(context, content.getSender());
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); 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); db.addReaction(targetMessage.getId(), reactionRecord);
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false); 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 { } else {
Log.w(TAG, "[handleReaction] Could not find matching message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); 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) { private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
IdentityUtil.processVerifiedMessage(context, verifiedMessage); IdentityUtil.processVerifiedMessage(context, verifiedMessage);
} }
@ -751,6 +773,8 @@ public final class PushProcessMessageJob extends BaseJob {
handleReaction(content, message.getMessage()); handleReaction(content, message.getMessage());
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message)); threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
threadId = threadId != -1 ? threadId : null; 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()) { } else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce()) {
threadId = handleSynchronizeSentMediaMessage(message); threadId = handleSynchronizeSentMediaMessage(message);
} else { } else {
@ -1392,7 +1416,7 @@ public final class PushProcessMessageJob extends BaseJob {
RecipientId author = Recipient.externalPush(context, quote.get().getAuthor()).getId(); RecipientId author = Recipient.externalPush(context, quote.get().getAuthor()).getId();
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quote.get().getId(), author); 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..."); Log.i(TAG, "Found matching message record...");
List<Attachment> attachments = new LinkedList<>(); 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)); 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..."); 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(); slideDeck = ((MmsMessageRecord) record).getSlideDeck();
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) { } else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record))); 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()) { } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message)); body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck(); 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.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob; import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.ReactionSendJob; import org.thoughtcrime.securesms.jobs.ReactionSendJob;
import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException; 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) { public static void resendGroupMessage(Context context, MessageRecord messageRecord, RecipientId filterRecipientId) {
if (!messageRecord.isMms()) throw new AssertionError("Not Group"); if (!messageRecord.isMms()) throw new AssertionError("Not Group");
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientId, Collections.emptyList()); 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 PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone"; private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
private static final String ATTACHMENTS_V3 = "android.attachmentsV3"; 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 * 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, PINS_MEGAPHONE_KILL_SWITCH,
PROFILE_NAMES_MEGAPHONE, PROFILE_NAMES_MEGAPHONE,
MESSAGE_REQUESTS, MESSAGE_REQUESTS,
ATTACHMENTS_V3 ATTACHMENTS_V3,
REMOTE_DELETE
); );
/** /**
@ -218,6 +220,11 @@ public final class FeatureFlags {
return getValue(ATTACHMENTS_V3, false); 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. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() { public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES); 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_read_more">&#160; Read More</string>
<string name="ConversationItem_download_more">&#160; Download More</string> <string name="ConversationItem_download_more">&#160; Download More</string>
<string name="ConversationItem_pending">&#160; Pending</string> <string name="ConversationItem_pending">&#160; Pending</string>
<string name="ConversationItem_this_message_was_deleted">This message was deleted.</string>
<!-- ConversationActivity --> <!-- ConversationActivity -->
<string name="ConversationActivity_reset_secure_session_question">Reset secure session?</string> <string name="ConversationActivity_reset_secure_session_question">Reset secure session?</string>
@ -298,6 +299,8 @@
<string name="ConversationFragment_sms">SMS</string> <string name="ConversationFragment_sms">SMS</string>
<string name="ConversationFragment_deleting">Deleting</string> <string name="ConversationFragment_deleting">Deleting</string>
<string name="ConversationFragment_deleting_messages">Deleting messages…</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_not_found">Original message not found</string>
<string name="ConversationFragment_quoted_message_no_longer_available">Original message no longer available</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> <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_secure_session_reset_s">%s reset the secure session.</string>
<string name="SmsMessageRecord_duplicate_message">Duplicate message.</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_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 --> <!-- StickerManagementActivity -->
<string name="StickerManagementActivity_stickers">Stickers</string> <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_photo">View-once photo</string>
<string name="ThreadRecord_view_once_video">View-once video</string> <string name="ThreadRecord_view_once_video">View-once video</string>
<string name="ThreadRecord_view_once_media">View-once media</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_s_is_on_signal">%s is on Signal!</string>
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</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> <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_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_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_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 --> <!-- Notification Channels -->
<string name="NotificationChannel_messages">Default</string> <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())); 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()); builder.setTimestamp(message.getTimestamp());
return container.setDataMessage(builder).build().toByteArray(); return container.setDataMessage(builder).build().toByteArray();

View File

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

View File

@ -35,6 +35,7 @@ public class SignalServiceDataMessage {
private final Optional<Sticker> sticker; private final Optional<Sticker> sticker;
private final boolean viewOnce; private final boolean viewOnce;
private final Optional<Reaction> reaction; private final Optional<Reaction> reaction;
private final Optional<RemoteDelete> remoteDelete;
/** /**
* Construct a SignalServiceDataMessage. * Construct a SignalServiceDataMessage.
@ -53,7 +54,7 @@ public class SignalServiceDataMessage {
String body, boolean endSession, int expiresInSeconds, String body, boolean endSession, int expiresInSeconds,
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate, boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews, Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker, boolean viewOnce, Reaction reaction) Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete)
{ {
try { try {
this.group = SignalServiceGroupContext.createOptional(group, groupV2); this.group = SignalServiceGroupContext.createOptional(group, groupV2);
@ -72,6 +73,7 @@ public class SignalServiceDataMessage {
this.sticker = Optional.fromNullable(sticker); this.sticker = Optional.fromNullable(sticker);
this.viewOnce = viewOnce; this.viewOnce = viewOnce;
this.reaction = Optional.fromNullable(reaction); this.reaction = Optional.fromNullable(reaction);
this.remoteDelete = Optional.fromNullable(remoteDelete);
if (attachments != null && !attachments.isEmpty()) { if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments); this.attachments = Optional.of(attachments);
@ -174,6 +176,10 @@ public class SignalServiceDataMessage {
return reaction; return reaction;
} }
public Optional<RemoteDelete> getRemoteDelete() {
return remoteDelete;
}
public static class Builder { public static class Builder {
private List<SignalServiceAttachment> attachments = new LinkedList<>(); private List<SignalServiceAttachment> attachments = new LinkedList<>();
@ -193,6 +199,7 @@ public class SignalServiceDataMessage {
private Sticker sticker; private Sticker sticker;
private boolean viewOnce; private boolean viewOnce;
private Reaction reaction; private Reaction reaction;
private RemoteDelete remoteDelete;
private Builder() {} private Builder() {}
@ -300,12 +307,17 @@ public class SignalServiceDataMessage {
return this; return this;
} }
public Builder withRemoteDelete(RemoteDelete remoteDelete) {
this.remoteDelete = remoteDelete;
return this;
}
public SignalServiceDataMessage build() { public SignalServiceDataMessage build() {
if (timestamp == 0) timestamp = System.currentTimeMillis(); if (timestamp == 0) timestamp = System.currentTimeMillis();
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession, return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
expiresInSeconds, expirationUpdate, profileKey, expiresInSeconds, expirationUpdate, profileKey,
profileKeyUpdate, quote, sharedContacts, previews, profileKeyUpdate, quote, sharedContacts, previews,
sticker, viewOnce, reaction); sticker, viewOnce, reaction, remoteDelete);
} }
} }
@ -446,4 +458,16 @@ public class SignalServiceDataMessage {
return targetSentTimestamp; 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; optional uint64 targetSentTimestamp = 5;
} }
message Delete {
optional uint64 targetSentTimestamp = 1;
}
enum ProtocolVersion { enum ProtocolVersion {
option allow_alias = true; option allow_alias = true;
@ -211,6 +215,7 @@ message DataMessage {
optional uint32 requiredProtocolVersion = 12; optional uint32 requiredProtocolVersion = 12;
optional bool isViewOnce = 14; optional bool isViewOnce = 14;
optional Reaction reaction = 16; optional Reaction reaction = 16;
optional Delete delete = 17;
} }
message NullMessage { message NullMessage {