Add support for sending and syncing viewed receipts behind a feature flag.

fork-5.53.8
Alex Hart 2021-04-28 16:21:34 -03:00
rodzic cdc7f1565e
commit ab44d608d2
16 zmienionych plików z 425 dodań i 44 usunięć

Wyświetl plik

@ -99,7 +99,7 @@ public class ConversationItemFooter extends LinearLayout {
presentTimer(messageRecord);
presentInsecureIndicator(messageRecord);
presentDeliveryStatus(messageRecord);
hideAudioDurationViews();
presentAudioDuration(messageRecord);
}
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
@ -259,6 +259,12 @@ public class ConversationItemFooter extends LinearLayout {
moveAudioViewsForIncoming();
}
showAudioDurationViews();
if (messageRecord.getViewedReceiptCount() > 0) {
revealDot.setProgress(1f);
} else {
revealDot.setProgress(0f);
}
} else {
hideAudioDurationViews();
}
@ -295,7 +301,7 @@ public class ConversationItemFooter extends LinearLayout {
private void showAudioDurationViews() {
audioSpace.setVisibility(View.VISIBLE);
audioDuration.setVisibility(View.VISIBLE);
audioDuration.setVisibility(View.GONE);
if (FeatureFlags.viewedReceipts()) {
revealDot.setVisibility(View.VISIBLE);

Wyświetl plik

@ -33,7 +33,17 @@ import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import java.util.Collections;
@ -48,10 +58,10 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
private static final String EMPTY_ROOT_ID = "empty-root-id";
private static final int LOAD_MORE_THRESHOLD = 2;
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_SEEK_TO |
PlaybackStateCompat.ACTION_STOP |
private static final long SUPPORTED_ACTIONS = PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_SEEK_TO |
PlaybackStateCompat.ACTION_STOP |
PlaybackStateCompat.ACTION_PLAY_PAUSE;
private MediaSessionCompat mediaSession;
@ -152,6 +162,7 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
becomingNoisyReceiver.unregister();
voiceNoteProximityManager.onPlayerEnded();
} else {
sendViewedReceiptForCurrentWindowIndex();
becomingNoisyReceiver.register();
voiceNoteProximityManager.onPlayerReady();
}
@ -172,11 +183,12 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
MediaDescriptionCompat mediaDescriptionCompat = queueDataAdapter.getMediaDescription(currentWindowIndex);
sendViewedReceiptForCurrentWindowIndex();
Log.d(TAG, "onPositionDiscontinuity: current window uri: " + mediaDescriptionCompat.getMediaUri());
}
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
boolean isWithinThreshold = currentWindowIndex < LOAD_MORE_THRESHOLD ||
currentWindowIndex + LOAD_MORE_THRESHOLD >= queueDataAdapter.size();
if (isWithinThreshold && currentWindowIndex % 2 == 0) {
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
@ -190,6 +202,36 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
}
}
private void sendViewedReceiptForCurrentWindowIndex() {
if (player.getPlaybackState() == Player.STATE_READY &&
player.getPlayWhenReady() &&
player.getCurrentWindowIndex() != C.INDEX_UNSET &&
FeatureFlags.sendViewedReceipts()) {
final MediaDescriptionCompat descriptionCompat = queueDataAdapter.getMediaDescription(player.getCurrentWindowIndex());
if (!descriptionCompat.getMediaUri().getScheme().equals("content")) {
return;
}
SignalExecutors.BOUNDED.execute(() -> {
Bundle extras = descriptionCompat.getExtras();
long messageId = extras.getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
RecipientId recipientId = RecipientId.from(extras.getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_RECIPIENT_ID));
MessageDatabase messageDatabase = DatabaseFactory.getMmsDatabase(this);
MessageDatabase.MarkedMessageInfo markedMessageInfo = messageDatabase.setIncomingMessageViewed(messageId);
if (markedMessageInfo != null) {
ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(markedMessageInfo.getThreadId(),
recipientId,
markedMessageInfo.getSyncMessageId().getTimetamp()));
MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(markedMessageInfo.getSyncMessageId()));
}
});
}
}
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
@Override

Wyświetl plik

@ -117,6 +117,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil;
@ -1164,7 +1165,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp());
if (current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() ||
if (forceFooter(messageRecord) || current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() ||
current.isFailed() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread))
{
ConversationItemFooter activeFooter = getActiveFooter(current);
@ -1189,6 +1190,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
return FeatureFlags.viewedReceipts() && hasAudio(messageRecord) && messageRecord.getViewedReceiptCount() == 0;
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasNoBubble(messageRecord)) {
return stickerFooter;

Wyświetl plik

@ -128,6 +128,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract Pair<Long, Long> updateBundleMessageBody(long messageId, String body);
public abstract @NonNull List<MarkedMessageInfo> getViewedIncomingMessages(long threadId);
public abstract @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId);
public abstract @NonNull List<MarkedMessageInfo> setIncomingMessagesViewed(@NonNull List<Long> messageIds);
public abstract void addFailures(long messageId, List<NetworkFailure> failure);
public abstract void removeFailure(long messageId, NetworkFailure failure);

Wyświetl plik

@ -412,39 +412,56 @@ public class MmsDatabase extends MessageDatabase {
@Override
public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID};
String where = ID_WHERE + " AND " + VIEWED_RECEIPT_COUNT + " = 0";
String[] args = SqlUtil.buildArgs(messageId);
List<MarkedMessageInfo> results = setIncomingMessagesViewed(Collections.singletonList(messageId));
if (results.isEmpty()) {
return null;
} else {
return results.get(0);
}
}
@Override
public @NonNull List<MarkedMessageInfo> setIncomingMessagesViewed(@NonNull List<Long> messageIds) {
if (messageIds.isEmpty()) {
return Collections.emptyList();
}
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID};
String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND " + VIEWED_RECEIPT_COUNT + " = 0";
List<MarkedMessageInfo> results = new LinkedList<>();
database.beginTransaction();
try (Cursor cursor = database.query(TABLE_NAME, columns, where, args, null, null, null)) {
if (cursor == null || !cursor.moveToFirst()) {
return null;
}
long type = CursorUtil.requireLong(cursor, MESSAGE_BOX);
if (Types.isSecureType(type) && Types.isInboxType(type)) {
long threadId = CursorUtil.requireLong(cursor, THREAD_ID);
RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID));
long dateSent = CursorUtil.requireLong(cursor, DATE_SENT);
SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent);
MarkedMessageInfo result = new MarkedMessageInfo(threadId, syncMessageId, null);
ContentValues contentValues = new ContentValues();
contentValues.put(VIEWED_RECEIPT_COUNT, 1);
database.update(TABLE_NAME, contentValues, where, args);
database.setTransactionSuccessful();
return result;
} else {
return null;
try (Cursor cursor = database.query(TABLE_NAME, columns, where, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
long type = CursorUtil.requireLong(cursor, MESSAGE_BOX);
if (Types.isSecureType(type) && Types.isInboxType(type)) {
long threadId = CursorUtil.requireLong(cursor, THREAD_ID);
RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID));
long dateSent = CursorUtil.requireLong(cursor, DATE_SENT);
SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent);
results.add(new MarkedMessageInfo(threadId, syncMessageId, null));
ContentValues contentValues = new ContentValues();
contentValues.put(VIEWED_RECEIPT_COUNT, 1);
database.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(CursorUtil.requireLong(cursor, ID)));
}
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
Set<Long> threadsUpdated = Stream.of(results)
.map(MarkedMessageInfo::getThreadId)
.collect(Collectors.toSet());
notifyConversationListeners(threadsUpdated);
return results;
}
@Override

Wyświetl plik

@ -635,6 +635,11 @@ public class SmsDatabase extends MessageDatabase {
return null;
}
@Override
public @NonNull List<MarkedMessageInfo> setIncomingMessagesViewed(@NonNull List<Long> messageIds) {
return Collections.emptyList();
}
private Pair<Long, Long> updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +

Wyświetl plik

@ -104,6 +104,7 @@ public final class JobManagerFactories {
put(MultiDeviceStorageSyncRequestJob.KEY, new MultiDeviceStorageSyncRequestJob.Factory());
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory());
put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory());
put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory());
put(PushDecryptMessageJob.KEY, new PushDecryptMessageJob.Factory());
put(PushDecryptDrainedJob.KEY, new PushDecryptDrainedJob.Factory());

Wyświetl plik

@ -0,0 +1,163 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class MultiDeviceViewedUpdateJob extends BaseJob {
public static final String KEY = "MultiDeviceViewedUpdateJob";
private static final String TAG = Log.tag(MultiDeviceViewedUpdateJob.class);
private static final String KEY_MESSAGE_IDS = "message_ids";
private List<SerializableSyncMessageId> messageIds;
private MultiDeviceViewedUpdateJob(List<SyncMessageId> messageIds) {
this(new Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
SendReadReceiptJob.ensureSize(messageIds, SendReadReceiptJob.MAX_TIMESTAMPS));
}
private MultiDeviceViewedUpdateJob(@NonNull Parameters parameters, @NonNull List<SyncMessageId> messageIds) {
super(parameters);
this.messageIds = new LinkedList<>();
for (SyncMessageId messageId : messageIds) {
this.messageIds.add(new SerializableSyncMessageId(messageId.getRecipientId().serialize(), messageId.getTimetamp()));
}
}
/**
* Enqueues all the necessary jobs for read receipts, ensuring that they're all within the
* maximum size.
*/
public static void enqueue(@NonNull List<SyncMessageId> messageIds) {
JobManager jobManager = ApplicationDependencies.getJobManager();
List<List<SyncMessageId>> messageIdChunks = Util.chunk(messageIds, SendReadReceiptJob.MAX_TIMESTAMPS);
if (messageIdChunks.size() > 1) {
Log.w(TAG, "Large receipt count! Had to break into multiple chunks. Total count: " + messageIds.size());
}
for (List<SyncMessageId> chunk : messageIdChunks) {
jobManager.add(new MultiDeviceViewedUpdateJob(chunk));
}
}
@Override
public @NonNull Data serialize() {
String[] ids = new String[messageIds.size()];
for (int i = 0; i < ids.length; i++) {
try {
ids[i] = JsonUtils.toJson(messageIds.get(i));
} catch (IOException e) {
throw new AssertionError(e);
}
}
return new Data.Builder().putStringArray(KEY_MESSAGE_IDS, ids).build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException, UntrustedIdentityException {
if (!TextSecurePreferences.isMultiDevice(context)) {
Log.i(TAG, "Not multi device...");
return;
}
List<ViewedMessage> viewedMessages = new LinkedList<>();
for (SerializableSyncMessageId messageId : messageIds) {
Recipient recipient = Recipient.resolved(RecipientId.from(messageId.recipientId));
if (!recipient.isGroup()) {
viewedMessages.add(new ViewedMessage(RecipientUtil.toSignalServiceAddress(context, recipient), messageId.timestamp));
}
}
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
messageSender.sendMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context));
}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
return exception instanceof PushNetworkException;
}
@Override
public void onFailure() {
}
private static class SerializableSyncMessageId implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty
private final String recipientId;
@JsonProperty
private final long timestamp;
private SerializableSyncMessageId(@JsonProperty("recipientId") String recipientId, @JsonProperty("timestamp") long timestamp) {
this.recipientId = recipientId;
this.timestamp = timestamp;
}
}
public static final class Factory implements Job.Factory<MultiDeviceViewedUpdateJob> {
@Override
public @NonNull MultiDeviceViewedUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) {
List<SyncMessageId> ids = Stream.of(data.getStringArray(KEY_MESSAGE_IDS))
.map(id -> {
try {
return JsonUtils.fromJson(id, SerializableSyncMessageId.class);
} catch (IOException e) {
throw new AssertionError(e);
}
})
.map(id -> new SyncMessageId(RecipientId.from(id.recipientId), id.timestamp))
.toList();
return new MultiDeviceViewedUpdateJob(parameters, ids);
}
}
}

Wyświetl plik

@ -57,7 +57,7 @@ public class SendViewedReceiptJob extends BaseJob {
.build(),
threadId,
recipientId,
syncTimestamps,
SendReadReceiptJob.ensureSize(syncTimestamps, SendReadReceiptJob.MAX_TIMESTAMPS),
System.currentTimeMillis());
}

Wyświetl plik

@ -101,6 +101,7 @@ import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@ -133,6 +134,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.payments.Money;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -147,6 +149,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
@ -268,6 +271,7 @@ public final class MessageContentProcessor {
if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(content, syncMessage.getSent().get());
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(syncMessage.getRequest().get());
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
else if (syncMessage.getViewed().isPresent()) handleSynchronizeViewedMessage(syncMessage.getViewed().get(), content.getTimestamp());
else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(syncMessage.getViewOnceOpen().get(), content.getTimestamp());
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
@ -1019,6 +1023,24 @@ public final class MessageContentProcessor {
messageNotifier.updateNotification(context);
}
private void handleSynchronizeViewedMessage(@NonNull List<ViewedMessage> viewedMessages, long envelopeTimestamp) {
List<Long> toMarkViewed = Stream.of(viewedMessages)
.map(message -> {
RecipientId author = Recipient.externalPush(context, message.getSender()).getId();
return DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), author);
})
.filter(message -> message != null && message.isMms())
.map(MessageRecord::getId)
.toList();
DatabaseFactory.getMmsDatabase(context).setIncomingMessagesViewed(toMarkViewed);
MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier();
messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
messageNotifier.cancelDelayedNotifications();
messageNotifier.updateNotification(context);
}
private void handleSynchronizeViewOnceOpenMessage(@NonNull ViewOnceOpenMessage openMessage, long envelopeTimestamp) {
log(String.valueOf(envelopeTimestamp), "Handling a view-once open for message: " + openMessage.getTimestamp());

Wyświetl plik

@ -11,9 +11,13 @@ import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
class ViewOnceMessageRepository {
private static final String TAG = Log.tag(ViewOnceMessageRepository.class);
@ -28,12 +32,17 @@ class ViewOnceMessageRepository {
SignalExecutors.BOUNDED.execute(() -> {
try (MmsDatabase.Reader reader = MmsDatabase.readerFor(mmsDatabase.getMessageCursor(messageId))) {
MmsMessageRecord record = (MmsMessageRecord) reader.getNext();
MessageDatabase.MarkedMessageInfo info = mmsDatabase.setIncomingMessageViewed(record.getId());
if (info != null) {
ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(record.getThreadId(),
info.getSyncMessageId().getRecipientId(),
info.getSyncMessageId().getTimetamp()));
if (FeatureFlags.sendViewedReceipts()) {
MessageDatabase.MarkedMessageInfo info = mmsDatabase.setIncomingMessageViewed(record.getId());
if (info != null) {
ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(record.getThreadId(),
info.getSyncMessageId().getRecipientId(),
info.getSyncMessageId().getTimetamp()));
MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(info.getSyncMessageId()));
}
}
callback.onComplete(Optional.fromNullable(record));
}
});

Wyświetl plik

@ -49,6 +49,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
@ -354,6 +355,8 @@ public class SignalServiceMessageSender {
content = createMultiDeviceGroupsContent(message.getGroups().get().asStream());
} else if (message.getRead().isPresent()) {
content = createMultiDeviceReadContent(message.getRead().get());
} else if (message.getViewed().isPresent()) {
content = createMultiDeviceViewedContent(message.getViewed().get());
} else if (message.getViewOnceOpen().isPresent()) {
content = createMultiDeviceViewOnceOpenContent(message.getViewOnceOpen().get());
} else if (message.getBlockedList().isPresent()) {
@ -964,6 +967,27 @@ public class SignalServiceMessageSender {
return container.setSyncMessage(builder).build().toByteArray();
}
private byte[] createMultiDeviceViewedContent(List<ViewedMessage> readMessages) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();
for (ViewedMessage readMessage : readMessages) {
SyncMessage.Viewed.Builder viewedBuilder = SyncMessage.Viewed.newBuilder().setTimestamp(readMessage.getTimestamp());
if (readMessage.getSender().getUuid().isPresent()) {
viewedBuilder.setSenderUuid(readMessage.getSender().getUuid().get().toString());
}
if (readMessage.getSender().getNumber().isPresent()) {
viewedBuilder.setSenderE164(readMessage.getSender().getNumber().get());
}
builder.addViewed(viewedBuilder.build());
}
return container.setSyncMessage(builder).build().toByteArray();
}
private byte[] createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();

Wyświetl plik

@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.payments.Money;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -465,6 +466,21 @@ public final class SignalServiceContent {
return SignalServiceSyncMessage.forRead(readMessages);
}
if (content.getViewedList().size() > 0) {
List<ViewedMessage> viewedMessages = new LinkedList<>();
for (SignalServiceProtos.SyncMessage.Viewed viewed : content.getViewedList()) {
if (SignalServiceAddress.isValidAddress(viewed.getSenderUuid(), viewed.getSenderE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(viewed.getSenderUuid()), viewed.getSenderE164());
viewedMessages.add(new ViewedMessage(address, viewed.getTimestamp()));
} else {
Log.w(TAG, "Encountered an invalid ReadMessage! Ignoring.");
}
}
return SignalServiceSyncMessage.forViewed(viewedMessages);
}
if (content.hasViewOnceOpen()) {
if (SignalServiceAddress.isValidAddress(content.getViewOnceOpen().getSenderUuid(), content.getViewOnceOpen().getSenderE164())) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(content.getViewOnceOpen().getSenderUuid()), content.getViewOnceOpen().getSenderE164());

Wyświetl plik

@ -28,6 +28,7 @@ public class SignalServiceSyncMessage {
private final Optional<KeysMessage> keys;
private final Optional<MessageRequestResponseMessage> messageRequestResponse;
private final Optional<OutgoingPaymentMessage> outgoingPaymentMessage;
private final Optional<List<ViewedMessage>> views;
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
Optional<ContactsMessage> contacts,
@ -42,7 +43,8 @@ public class SignalServiceSyncMessage {
Optional<FetchType> fetchType,
Optional<KeysMessage> keys,
Optional<MessageRequestResponseMessage> messageRequestResponse,
Optional<OutgoingPaymentMessage> outgoingPaymentMessage)
Optional<OutgoingPaymentMessage> outgoingPaymentMessage,
Optional<List<ViewedMessage>> views)
{
this.sent = sent;
this.contacts = contacts;
@ -58,6 +60,7 @@ public class SignalServiceSyncMessage {
this.keys = keys;
this.messageRequestResponse = messageRequestResponse;
this.outgoingPaymentMessage = outgoingPaymentMessage;
this.views = views;
}
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
@ -74,6 +77,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -91,6 +95,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -108,6 +113,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -125,6 +131,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -142,9 +149,28 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
public static SignalServiceSyncMessage forViewed(List<ViewedMessage> views) {
return new SignalServiceSyncMessage(Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(views));
}
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
return new SignalServiceSyncMessage(Optional.absent(),
Optional.absent(),
@ -159,6 +185,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -179,6 +206,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -196,6 +224,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -213,6 +242,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -230,6 +260,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -247,6 +278,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -264,6 +296,7 @@ public class SignalServiceSyncMessage {
Optional.of(fetchType),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -281,6 +314,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.of(keys),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -298,6 +332,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.of(messageRequestResponse),
Optional.absent(),
Optional.absent());
}
@ -315,7 +350,8 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(outgoingPaymentMessage));
Optional.of(outgoingPaymentMessage),
Optional.absent());
}
public static SignalServiceSyncMessage empty() {
@ -332,6 +368,7 @@ public class SignalServiceSyncMessage {
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
}
@ -391,6 +428,10 @@ public class SignalServiceSyncMessage {
return outgoingPaymentMessage;
}
public Optional<List<ViewedMessage>> getViewed() {
return views;
}
public enum FetchType {
LOCAL_PROFILE,
STORAGE_MANIFEST

Wyświetl plik

@ -0,0 +1,22 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class ViewedMessage {
private final SignalServiceAddress sender;
private final long timestamp;
public ViewedMessage(SignalServiceAddress sender, long timestamp) {
this.sender = sender;
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public SignalServiceAddress getSender() {
return sender;
}
}

Wyświetl plik

@ -403,6 +403,12 @@ message SyncMessage {
optional uint64 timestamp = 2;
}
message Viewed {
optional string senderE164 = 1;
optional string senderUuid = 3;
optional uint64 timestamp = 2;
}
message Configuration {
optional bool readReceipts = 1;
optional bool unidentifiedDeliveryIndicators = 2;
@ -495,6 +501,7 @@ message SyncMessage {
optional Keys keys = 13;
optional MessageRequestResponse messageRequestResponse = 14;
optional OutgoingPayment outgoingPayment = 15;
repeated Viewed viewed = 16;
}
message AttachmentPointer {