kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add support for sending and syncing viewed receipts behind a feature flag.
rodzic
cdc7f1565e
commit
ab44d608d2
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 + " = ?, " +
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ public class SendViewedReceiptJob extends BaseJob {
|
|||
.build(),
|
||||
threadId,
|
||||
recipientId,
|
||||
syncTimestamps,
|
||||
SendReadReceiptJob.ensureSize(syncTimestamps, SendReadReceiptJob.MAX_TIMESTAMPS),
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Ładowanie…
Reference in New Issue