mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 20:18:34 +00:00
Add pre-alpha receive support for remote delete.
This commit is contained in:
parent
456bcf3d57
commit
6ecd3b59fd
@ -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() &&
|
||||||
|
@ -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());
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(*)"};
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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...");
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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());
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -186,6 +186,7 @@
|
|||||||
<string name="ConversationItem_read_more">  Read More</string>
|
<string name="ConversationItem_read_more">  Read More</string>
|
||||||
<string name="ConversationItem_download_more">  Download More</string>
|
<string name="ConversationItem_download_more">  Download More</string>
|
||||||
<string name="ConversationItem_pending">  Pending</string>
|
<string name="ConversationItem_pending">  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>
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user