diff --git a/app/src/main/java/org/thoughtcrime/securesms/ConfirmIdentityDialog.java b/app/src/main/java/org/thoughtcrime/securesms/ConfirmIdentityDialog.java index 5144bfeec..d32c4a210 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ConfirmIdentityDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ConfirmIdentityDialog.java @@ -175,7 +175,9 @@ public class ConfirmIdentityDialog extends AlertDialog { messageRecord.getDateSent(), legacy ? Base64.decode(messageRecord.getBody()) : null, !legacy ? Base64.decode(messageRecord.getBody()) : null, - 0, null); + 0, + 0, + null); long pushId = pushDatabase.insert(envelope); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java index e347dc2e6..1a9d1d0f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java @@ -3,10 +3,8 @@ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.text.TextUtils; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import net.sqlcipher.database.SQLiteDatabase; @@ -16,31 +14,38 @@ import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.util.Util; import java.io.IOException; -import java.util.UUID; public class PushDatabase extends Database { private static final String TAG = PushDatabase.class.getSimpleName(); - private static final String TABLE_NAME = "push"; - public static final String ID = "_id"; - public static final String TYPE = "type"; - public static final String SOURCE_E164 = "source"; - public static final String SOURCE_UUID = "source_uuid"; - public static final String DEVICE_ID = "device_id"; - public static final String LEGACY_MSG = "body"; - public static final String CONTENT = "content"; - public static final String TIMESTAMP = "timestamp"; - public static final String SERVER_TIMESTAMP = "server_timestamp"; - public static final String SERVER_GUID = "server_guid"; + private static final String TABLE_NAME = "push"; + public static final String ID = "_id"; + public static final String TYPE = "type"; + public static final String SOURCE_E164 = "source"; + public static final String SOURCE_UUID = "source_uuid"; + public static final String DEVICE_ID = "device_id"; + public static final String LEGACY_MSG = "body"; + public static final String CONTENT = "content"; + public static final String TIMESTAMP = "timestamp"; + public static final String SERVER_RECEIVED_TIMESTAMP = "server_timestamp"; + public static final String SERVER_DELIVERED_TIMESTAMP = "server_delivered_timestamp"; + public static final String SERVER_GUID = "server_guid"; - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + - TYPE + " INTEGER, " + SOURCE_E164 + " TEXT, " + SOURCE_UUID + " TEXT, " + DEVICE_ID + " INTEGER, " + LEGACY_MSG + " TEXT, " + CONTENT + " TEXT, " + TIMESTAMP + " INTEGER, " + - SERVER_TIMESTAMP + " INTEGER DEFAULT 0, " + SERVER_GUID + " TEXT DEFAULT NULL);"; + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + + TYPE + " INTEGER, " + + SOURCE_E164 + " TEXT, " + + SOURCE_UUID + " TEXT, " + + DEVICE_ID + " INTEGER, " + + LEGACY_MSG + " TEXT, " + + CONTENT + " TEXT, " + + TIMESTAMP + " INTEGER, " + + SERVER_RECEIVED_TIMESTAMP + " INTEGER DEFAULT 0, " + + SERVER_DELIVERED_TIMESTAMP + " INTEGER DEFAULT 0, " + + SERVER_GUID + " TEXT DEFAULT NULL);"; public PushDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); @@ -60,7 +65,8 @@ public class PushDatabase extends Database { values.put(LEGACY_MSG, envelope.hasLegacyMessage() ? Base64.encodeBytes(envelope.getLegacyMessage()) : ""); values.put(CONTENT, envelope.hasContent() ? Base64.encodeBytes(envelope.getContent()) : ""); values.put(TIMESTAMP, envelope.getTimestamp()); - values.put(SERVER_TIMESTAMP, envelope.getServerTimestamp()); + values.put(SERVER_RECEIVED_TIMESTAMP, envelope.getServerReceivedTimestamp()); + values.put(SERVER_DELIVERED_TIMESTAMP, envelope.getServerDeliveredTimestamp()); values.put(SERVER_GUID, envelope.getUuid()); return databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values); @@ -87,7 +93,8 @@ public class PushDatabase extends Database { cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)), Util.isEmpty(legacyMessage) ? null : Base64.decode(legacyMessage), Util.isEmpty(content) ? null : Base64.decode(content), - cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_TIMESTAMP)), + cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP)), + cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP)), cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID))); } } catch (IOException e) { @@ -154,15 +161,16 @@ public class PushDatabase extends Database { if (cursor == null || !cursor.moveToNext()) return null; - int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)); - String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID)); - String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164)); - int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID)); - String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG)); - String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT)); - long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); - long serverTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_TIMESTAMP)); - String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)); + int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)); + String sourceUuid = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_UUID)); + String sourceE164 = cursor.getString(cursor.getColumnIndexOrThrow(SOURCE_E164)); + int deviceId = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE_ID)); + String legacyMessage = cursor.getString(cursor.getColumnIndexOrThrow(LEGACY_MSG)); + String content = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT)); + long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); + long serverReceivedTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_RECEIVED_TIMESTAMP)); + long serverDeliveredTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(SERVER_DELIVERED_TIMESTAMP)); + String serverGuid = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_GUID)); return new SignalServiceEnvelope(type, SignalServiceAddress.fromRaw(sourceUuid, sourceE164), @@ -170,7 +178,8 @@ public class PushDatabase extends Database { timestamp, legacyMessage != null ? Base64.decode(legacyMessage) : null, content != null ? Base64.decode(content) : null, - serverTimestamp, + serverReceivedTimestamp, + serverDeliveredTimestamp, serverGuid); } catch (IOException e) { throw new AssertionError(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 1bc7b5201..7a40e38d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -136,8 +136,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int COLOR_MIGRATION = 61; private static final int LAST_SCROLLED = 62; private static final int LAST_PROFILE_FETCH = 63; + private static final int SERVER_DELIVERED_TIMESTAMP = 64; - private static final int DATABASE_VERSION = 63; + private static final int DATABASE_VERSION = 64; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -916,6 +917,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE recipient ADD COLUMN last_profile_fetch INTEGER DEFAULT 0"); } + if (oldVersion < SERVER_DELIVERED_TIMESTAMP) { + db.execSQL("ALTER TABLE push ADD COLUMN server_delivered_timestamp INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index d2a43a524..6366fab55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -245,7 +245,7 @@ public final class GroupV1MessageProcessor { } else { SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); String body = Base64.encodeBytes(storage.toByteArray()); - IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt()); + IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt()); IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, storage, body); Optional insertResult = smsDatabase.insertMessageInbox(groupMessage); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index f12ad3834..559febe43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -501,13 +501,14 @@ public final class PushProcessMessageJob extends BaseJob { RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId()); intent.setAction(WebRtcCallService.ACTION_RECEIVE_OFFER) - .putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId()) - .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer) - .putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice()) - .putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription()) - .putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp()) - .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, message.getType().getCode()) - .putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing()); + .putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId()) + .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer) + .putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice()) + .putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription()) + .putExtra(WebRtcCallService.EXTRA_SERVER_RECEIVED_TIMESTAMP, content.getServerReceivedTimestamp()) + .putExtra(WebRtcCallService.EXTRA_SERVER_DELIVERED_TIMESTAMP, content.getServerDeliveredTimestamp()) + .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, message.getType().getCode()) + .putExtra(WebRtcCallService.EXTRA_MULTI_RING, content.getCallMessage().get().isMultiRing()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent); else context.startService(intent); @@ -601,7 +602,7 @@ public final class PushProcessMessageJob extends BaseJob { IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), - content.getServerTimestamp(), + content.getServerReceivedTimestamp(), "", Optional.absent(), 0, content.isNeedsReceipt()); @@ -710,7 +711,7 @@ public final class PushProcessMessageJob extends BaseJob { Recipient sender = Recipient.externalPush(context, content.getSender()); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(), content.getTimestamp(), - content.getServerTimestamp(), + content.getServerReceivedTimestamp(), -1, expiresInSeconds * 1000L, true, @@ -768,7 +769,7 @@ public final class PushProcessMessageJob extends BaseJob { 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())) { + if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerReceivedTimestamp())) { MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); db.markAsRemoteDelete(targetMessage.getId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false); @@ -777,7 +778,7 @@ public final class PushProcessMessageJob extends BaseJob { ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), delete.getTargetSentTimestamp(), content); } 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())); + content.getServerReceivedTimestamp(), targetMessage.getServerTimestamp(), sender.getId(), targetMessage.getRecipient().getId())); } } @@ -1025,7 +1026,7 @@ public final class PushProcessMessageJob extends BaseJob { Optional sticker = getStickerAttachment(message.getSticker()); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Recipient.externalPush(context, content.getSender()).getId(), message.getTimestamp(), - content.getServerTimestamp(), + content.getServerReceivedTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, @@ -1245,7 +1246,7 @@ public final class PushProcessMessageJob extends BaseJob { IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(), content.getSenderDevice(), message.getTimestamp(), - content.getServerTimestamp(), + content.getServerReceivedTimestamp(), body, groupId, message.getExpiresInSeconds() * 1000L, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index 7970f0a04..47857a0f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -95,26 +95,27 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private static final String TAG = WebRtcCallService.class.getSimpleName(); - public static final String EXTRA_MUTE = "mute_value"; - public static final String EXTRA_AVAILABLE = "enabled_value"; - public static final String EXTRA_TIMESTAMP = "timestamp"; - public static final String EXTRA_CALL_ID = "call_id"; - public static final String EXTRA_RESULT_RECEIVER = "result_receiver"; - public static final String EXTRA_SPEAKER = "audio_speaker"; - public static final String EXTRA_BLUETOOTH = "audio_bluetooth"; - public static final String EXTRA_REMOTE_PEER = "remote_peer"; - public static final String EXTRA_REMOTE_DEVICE = "remote_device"; - public static final String EXTRA_OFFER_DESCRIPTION = "offer_description"; - public static final String EXTRA_OFFER_TYPE = "offer_type"; - public static final String EXTRA_MULTI_RING = "multi_ring"; - public static final String EXTRA_HANGUP_TYPE = "hangup_type"; - public static final String EXTRA_HANGUP_IS_LEGACY = "hangup_is_legacy"; - public static final String EXTRA_HANGUP_DEVICE_ID = "hangup_device_id"; - public static final String EXTRA_ANSWER_DESCRIPTION = "answer_description"; - public static final String EXTRA_ICE_CANDIDATES = "ice_candidates"; - public static final String EXTRA_ENABLE = "enable_value"; - public static final String EXTRA_BROADCAST = "broadcast"; - public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video"; + public static final String EXTRA_MUTE = "mute_value"; + public static final String EXTRA_AVAILABLE = "enabled_value"; + public static final String EXTRA_SERVER_RECEIVED_TIMESTAMP = "server_received_timestamp"; + public static final String EXTRA_SERVER_DELIVERED_TIMESTAMP = "server_delivered_timestamp"; + public static final String EXTRA_CALL_ID = "call_id"; + public static final String EXTRA_RESULT_RECEIVER = "result_receiver"; + public static final String EXTRA_SPEAKER = "audio_speaker"; + public static final String EXTRA_BLUETOOTH = "audio_bluetooth"; + public static final String EXTRA_REMOTE_PEER = "remote_peer"; + public static final String EXTRA_REMOTE_DEVICE = "remote_device"; + public static final String EXTRA_OFFER_DESCRIPTION = "offer_description"; + public static final String EXTRA_OFFER_TYPE = "offer_type"; + public static final String EXTRA_MULTI_RING = "multi_ring"; + public static final String EXTRA_HANGUP_TYPE = "hangup_type"; + public static final String EXTRA_HANGUP_IS_LEGACY = "hangup_is_legacy"; + public static final String EXTRA_HANGUP_DEVICE_ID = "hangup_device_id"; + public static final String EXTRA_ANSWER_DESCRIPTION = "answer_description"; + public static final String EXTRA_ICE_CANDIDATES = "ice_candidates"; + public static final String EXTRA_ENABLE = "enable_value"; + public static final String EXTRA_BROADCAST = "broadcast"; + public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video"; public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING"; public static final String ACTION_DENY_CALL = "DENY_CALL"; @@ -383,13 +384,14 @@ public class WebRtcCallService extends Service implements CallManager.Observer, // Handlers private void handleReceivedOffer(Intent intent) { - CallId callId = getCallId(intent); - RemotePeer remotePeer = getRemotePeer(intent); - Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); - String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION); - Long timeStamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1); - OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); - boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false); + CallId callId = getCallId(intent); + RemotePeer remotePeer = getRemotePeer(intent); + Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); + String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION); + long serverReceivedTimestamp = intent.getLongExtra(EXTRA_SERVER_RECEIVED_TIMESTAMP, -1); + long serverDeliveredTimestamp = intent.getLongExtra(EXTRA_SERVER_DELIVERED_TIMESTAMP, -1); + OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); + boolean isMultiRing = intent.getBooleanExtra(EXTRA_MULTI_RING, false); Log.i(TAG, "handleReceivedOffer(): id: " + callId.format(remoteDevice)); @@ -413,12 +415,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, CallManager.CallMediaType callType = getCallMediaTypeFromOfferType(offerType); - long timeMilli = new Date().getTime(); - long messageAgeSec = 0L; - if (timeMilli > timeStamp) { - messageAgeSec = (timeMilli - timeStamp) / 1000; - } - Log.i(TAG, "handleReceivedOffer(): messageAgeSec: " + messageAgeSec); + long messageAgeSec = Math.max(serverDeliveredTimestamp - serverReceivedTimestamp, 0) / 1000; + Log.i(TAG, "handleReceivedOffer(): messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + serverReceivedTimestamp + ", serverDeliveredTimestamp: " + serverDeliveredTimestamp); try { callManager.receivedOffer(callId, remotePeer, remoteDevice, offer, messageAgeSec, callType, 1, isMultiRing, true); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java index 93f08432f..6c396c30a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java @@ -16,6 +16,7 @@ import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext; import org.signal.zkgroup.profiles.ProfileKeyVersion; import org.whispersystems.libsignal.InvalidVersionException; +import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.util.Hex; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; @@ -61,6 +62,8 @@ public class SignalServiceMessagePipe { private static final String TAG = SignalServiceMessagePipe.class.getName(); + private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; + private final WebSocketConnection websocket; private final Optional credentialsProvider; private final ClientZkProfileOperations clientZkProfile; @@ -146,9 +149,21 @@ public class SignalServiceMessagePipe { try { if (isSignalServiceEnvelope(request)) { + Optional timestampHeader = findHeader(request, SERVER_DELIVERED_TIMESTAMP_HEADER); + long timestamp = 0; + + if (timestampHeader.isPresent()) { + try { + timestamp = Long.parseLong(timestampHeader.get()); + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse " + SERVER_DELIVERED_TIMESTAMP_HEADER); + } + } + SignalServiceEnvelope envelope = new SignalServiceEnvelope(request.getBody().toByteArray(), credentialsProvider.get().getSignalingKey(), - signalKeyEncrypted); + signalKeyEncrypted, + timestamp); callback.onMessage(envelope); return Optional.of(envelope); @@ -345,6 +360,23 @@ public class SignalServiceMessagePipe { } } + private static Optional findHeader(WebSocketRequestMessage message, String targetHeader) { + if (message.getHeadersCount() == 0) { + return Optional.absent(); + } + + for (String header : message.getHeadersList()) { + if (header.startsWith(targetHeader)) { + String[] split = header.split(":"); + if (split.length == 2 && split[0].trim().toLowerCase().equals(targetHeader.toLowerCase())) { + return Optional.of(split[1].trim()); + } + } + } + + return Optional.absent(); + } + /** * For receiving a callback when a new message has been * received. diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index d7a7377a2..c325bf306 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.websocket.ConnectivityListener; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.SignalServiceEnvelopeEntity; +import org.whispersystems.signalservice.internal.push.SignalServiceMessagesResult; import org.whispersystems.signalservice.internal.sticker.StickerProtos; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; import org.whispersystems.signalservice.internal.util.Util; @@ -263,22 +264,31 @@ public class SignalServiceMessageReceiver { public List retrieveMessages(MessageReceivedCallback callback) throws IOException { - List results = new LinkedList<>(); - List entities = socket.getMessages(); + List results = new LinkedList<>(); + SignalServiceMessagesResult messageResult = socket.getMessages(); - for (SignalServiceEnvelopeEntity entity : entities) { + for (SignalServiceEnvelopeEntity entity : messageResult.getEnvelopes()) { SignalServiceEnvelope envelope; if (entity.hasSource() && entity.getSourceDevice() > 0) { SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(entity.getSourceUuid()), entity.getSourceE164()); - envelope = new SignalServiceEnvelope(entity.getType(), Optional.of(address), - entity.getSourceDevice(), entity.getTimestamp(), - entity.getMessage(), entity.getContent(), - entity.getServerTimestamp(), entity.getServerUuid()); + envelope = new SignalServiceEnvelope(entity.getType(), + Optional.of(address), + entity.getSourceDevice(), + entity.getTimestamp(), + entity.getMessage(), + entity.getContent(), + entity.getServerTimestamp(), + messageResult.getServerDeliveredTimestamp(), + entity.getServerUuid()); } else { - envelope = new SignalServiceEnvelope(entity.getType(), entity.getTimestamp(), - entity.getMessage(), entity.getContent(), - entity.getServerTimestamp(), entity.getServerUuid()); + envelope = new SignalServiceEnvelope(entity.getType(), + entity.getTimestamp(), + entity.getMessage(), + entity.getContent(), + entity.getServerTimestamp(), + messageResult.getServerDeliveredTimestamp(), + entity.getServerUuid()); } callback.onMessage(envelope); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index c74a0233d..c4e4ae6f2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -176,23 +176,23 @@ public class SignalServiceCipher { SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress); paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); - metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), false); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isSignalMessage()) { SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice()); SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, sourceAddress); paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext)); - metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerTimestamp(), false); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isUnidentifiedSender()) { SealedSessionCipher sealedSessionCipher = new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1); - DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp()); + DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp()); SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid().orNull()), result.getSenderE164()); SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId()); paddedMessage = result.getPaddedMessage(); - metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerTimestamp(), true); + metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true); sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress); } else { throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index a6ab2e719..adab110f7 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -59,7 +59,8 @@ public final class SignalServiceContent { private final SignalServiceAddress sender; private final int senderDevice; private final long timestamp; - private final long serverTimestamp; + private final long serverReceivedTimestamp; + private final long serverDeliveredTimestamp; private final boolean needsReceipt; private final SignalServiceContentProto serializedState; @@ -69,13 +70,22 @@ public final class SignalServiceContent { private final Optional readMessage; private final Optional typingMessage; - private SignalServiceContent(SignalServiceDataMessage message, SignalServiceAddress sender, int senderDevice, long timestamp, long serverTimestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.serverTimestamp = serverTimestamp; - this.needsReceipt = needsReceipt; - this.serializedState = serializedState; + private SignalServiceContent(SignalServiceDataMessage message, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.fromNullable(message); this.synchronizeMessage = Optional.absent(); @@ -84,13 +94,22 @@ public final class SignalServiceContent { this.typingMessage = Optional.absent(); } - private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, SignalServiceAddress sender, int senderDevice, long timestamp, long serverTimestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.serverTimestamp = serverTimestamp; - this.needsReceipt = needsReceipt; - this.serializedState = serializedState; + private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.fromNullable(synchronizeMessage); @@ -99,13 +118,22 @@ public final class SignalServiceContent { this.typingMessage = Optional.absent(); } - private SignalServiceContent(SignalServiceCallMessage callMessage, SignalServiceAddress sender, int senderDevice, long timestamp, long serverTimestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.serverTimestamp = serverTimestamp; - this.needsReceipt = needsReceipt; - this.serializedState = serializedState; + private SignalServiceContent(SignalServiceCallMessage callMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.absent(); @@ -114,13 +142,22 @@ public final class SignalServiceContent { this.typingMessage = Optional.absent(); } - private SignalServiceContent(SignalServiceReceiptMessage receiptMessage, SignalServiceAddress sender, int senderDevice, long timestamp, long serverTimestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.serverTimestamp = serverTimestamp; - this.needsReceipt = needsReceipt; - this.serializedState = serializedState; + private SignalServiceContent(SignalServiceReceiptMessage receiptMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.absent(); @@ -129,13 +166,22 @@ public final class SignalServiceContent { this.typingMessage = Optional.absent(); } - private SignalServiceContent(SignalServiceTypingMessage typingMessage, SignalServiceAddress sender, int senderDevice, long timestamp, long serverTimestamp, boolean needsReceipt, SignalServiceContentProto serializedState) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.serverTimestamp = serverTimestamp; - this.needsReceipt = needsReceipt; - this.serializedState = serializedState; + private SignalServiceContent(SignalServiceTypingMessage typingMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serializedState = serializedState; this.message = Optional.absent(); this.synchronizeMessage = Optional.absent(); @@ -176,8 +222,12 @@ public final class SignalServiceContent { return timestamp; } - public long getServerTimestamp() { - return serverTimestamp; + public long getServerReceivedTimestamp() { + return serverReceivedTimestamp; + } + + public long getServerDeliveredTimestamp() { + return serverDeliveredTimestamp; } public boolean isNeedsReceipt() { @@ -217,7 +267,8 @@ public final class SignalServiceContent { metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), serviceContentProto); } else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) { @@ -228,7 +279,8 @@ public final class SignalServiceContent { metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), serviceContentProto); } else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) { @@ -236,7 +288,8 @@ public final class SignalServiceContent { metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), serviceContentProto); } else if (message.hasCallMessage()) { @@ -244,7 +297,8 @@ public final class SignalServiceContent { metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), serviceContentProto); } else if (message.hasReceiptMessage()) { @@ -252,7 +306,8 @@ public final class SignalServiceContent { metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), serviceContentProto); } else if (message.hasTypingMessage()) { @@ -260,7 +315,8 @@ public final class SignalServiceContent { metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), false, serviceContentProto); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index 321557aaf..a8bd136a3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -55,6 +55,7 @@ public class SignalServiceEnvelope { private static final int CIPHERTEXT_OFFSET = IV_OFFSET + IV_LENGTH; private final Envelope envelope; + private final long serverDeliveredTimestamp; /** * Construct an envelope from a serialized, Base64 encoded SignalServiceEnvelope, encrypted @@ -65,10 +66,13 @@ public class SignalServiceEnvelope { * @throws IOException * @throws InvalidVersionException */ - public SignalServiceEnvelope(String message, String signalingKey, boolean isSignalingKeyEncrypted) + public SignalServiceEnvelope(String message, + String signalingKey, + boolean isSignalingKeyEncrypted, + long serverDeliveredTimestamp) throws IOException, InvalidVersionException { - this(Base64.decode(message), signalingKey, isSignalingKeyEncrypted); + this(Base64.decode(message), signalingKey, isSignalingKeyEncrypted, serverDeliveredTimestamp); } /** @@ -79,7 +83,10 @@ public class SignalServiceEnvelope { * @throws InvalidVersionException * @throws IOException */ - public SignalServiceEnvelope(byte[] input, String signalingKey, boolean isSignalingKeyEncrypted) + public SignalServiceEnvelope(byte[] input, + String signalingKey, + boolean isSignalingKeyEncrypted, + long serverDeliveredTimestamp) throws InvalidVersionException, IOException { if (!isSignalingKeyEncrypted) { @@ -96,14 +103,25 @@ public class SignalServiceEnvelope { this.envelope = Envelope.parseFrom(getPlaintext(input, cipherKey)); } + + this.serverDeliveredTimestamp = serverDeliveredTimestamp; } - public SignalServiceEnvelope(int type, Optional sender, int senderDevice, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) { + public SignalServiceEnvelope(int type, + Optional sender, + int senderDevice, + long timestamp, + byte[] legacyMessage, + byte[] content, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + String uuid) + { Envelope.Builder builder = Envelope.newBuilder() .setType(Envelope.Type.valueOf(type)) .setSourceDevice(senderDevice) .setTimestamp(timestamp) - .setServerTimestamp(serverTimestamp); + .setServerTimestamp(serverReceivedTimestamp); if (sender.isPresent()) { if (sender.get().getUuid().isPresent()) { @@ -122,14 +140,22 @@ public class SignalServiceEnvelope { if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage)); if (content != null) builder.setContent(ByteString.copyFrom(content)); - this.envelope = builder.build(); + this.envelope = builder.build(); + this.serverDeliveredTimestamp = serverDeliveredTimestamp; } - public SignalServiceEnvelope(int type, long timestamp, byte[] legacyMessage, byte[] content, long serverTimestamp, String uuid) { + public SignalServiceEnvelope(int type, + long timestamp, + byte[] legacyMessage, + byte[] content, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + String uuid) + { Envelope.Builder builder = Envelope.newBuilder() .setType(Envelope.Type.valueOf(type)) .setTimestamp(timestamp) - .setServerTimestamp(serverTimestamp); + .setServerTimestamp(serverReceivedTimestamp); if (uuid != null) { builder.setServerGuid(uuid); @@ -138,7 +164,8 @@ public class SignalServiceEnvelope { if (legacyMessage != null) builder.setLegacyMessage(ByteString.copyFrom(legacyMessage)); if (content != null) builder.setContent(ByteString.copyFrom(content)); - this.envelope = builder.build(); + this.envelope = builder.build(); + this.serverDeliveredTimestamp = serverDeliveredTimestamp; } public String getUuid() { @@ -206,10 +233,20 @@ public class SignalServiceEnvelope { return envelope.getTimestamp(); } - public long getServerTimestamp() { + /** + * @return The server timestamp of when the server received the envelope. + */ + public long getServerReceivedTimestamp() { return envelope.getServerTimestamp(); } + /** + * @return The server timestamp of when the envelope was delivered to us. + */ + public long getServerDeliveredTimestamp() { + return serverDeliveredTimestamp; + } + /** * @return Whether the envelope contains a SignalServiceDataMessage */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java index a2a9e1826..7fa269258 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java @@ -6,15 +6,23 @@ public final class SignalServiceMetadata { private final SignalServiceAddress sender; private final int senderDevice; private final long timestamp; - private final long serverTimestamp; + private final long serverReceivedTimestamp; + private final long serverDeliveredTimestamp; private final boolean needsReceipt; - public SignalServiceMetadata(SignalServiceAddress sender, int senderDevice, long timestamp, long serverTimestamp, boolean needsReceipt) { - this.sender = sender; - this.senderDevice = senderDevice; - this.timestamp = timestamp; - this.serverTimestamp = serverTimestamp; - this.needsReceipt = needsReceipt; + public SignalServiceMetadata(SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; } public SignalServiceAddress getSender() { @@ -29,8 +37,12 @@ public final class SignalServiceMetadata { return timestamp; } - public long getServerTimestamp() { - return serverTimestamp; + public long getServerReceivedTimestamp() { + return serverReceivedTimestamp; + } + + public long getServerDeliveredTimestamp() { + return serverDeliveredTimestamp; } public boolean isNeedsReceipt() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 9c37b8650..e1c935274 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -198,6 +198,8 @@ public class PushServiceSocket { private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s"; private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form"; + private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; + private static final Map NO_HEADERS = Collections.emptyMap(); private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler(); @@ -390,9 +392,28 @@ public class PushServiceSocket { }); } - public List getMessages() throws IOException { - String responseText = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", null); - return JsonUtil.fromJson(responseText, SignalServiceEnvelopeEntityList.class).getMessages(); + public SignalServiceMessagesResult getMessages() throws IOException { + Response response = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", (RequestBody) null, NO_HEADERS, NO_HANDLER, Optional.absent()); + validateServiceResponse(response); + + List envelopes; + try { + envelopes = JsonUtil.fromJson(readBodyString(response.body()), SignalServiceEnvelopeEntityList.class).getMessages(); + } catch (IOException e) { + throw new PushNetworkException(e); + } + + long serverDeliveredTimestamp = 0; + try { + String stringValue = response.header(SERVER_DELIVERED_TIMESTAMP_HEADER); + stringValue = stringValue != null ? stringValue : "0"; + + serverDeliveredTimestamp = Long.parseLong(stringValue); + } catch (NumberFormatException e) { + Log.w(TAG, e); + } + + return new SignalServiceMessagesResult(envelopes, serverDeliveredTimestamp); } public void acknowledgeMessage(String sender, long timestamp) throws IOException { @@ -1368,9 +1389,9 @@ public class PushServiceSocket { call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { - try (ResponseBody body = validateServiceResponse(response)) { + try (ResponseBody body = validateServiceResponse(response).body()) { try { - bodyFuture.set(body.string()); + bodyFuture.set(readBodyString(body)); } catch (IOException e) { throw new PushNetworkException(e); } @@ -1395,6 +1416,17 @@ public class PushServiceSocket { ResponseCodeHandler responseCodeHandler, Optional unidentifiedAccessKey) throws NonSuccessfulResponseCodeException, PushNetworkException + { + return makeServiceRequest(urlFragment, method, body, headers, responseCodeHandler, unidentifiedAccessKey).body(); + } + + private Response makeServiceRequest(String urlFragment, + String method, + RequestBody body, + Map headers, + ResponseCodeHandler responseCodeHandler, + Optional unidentifiedAccessKey) + throws NonSuccessfulResponseCodeException, PushNetworkException { Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey); @@ -1403,7 +1435,7 @@ public class PushServiceSocket { return validateServiceResponse(response); } - private ResponseBody validateServiceResponse(Response response) throws NonSuccessfulResponseCodeException, PushNetworkException { + private Response validateServiceResponse(Response response) throws NonSuccessfulResponseCodeException, PushNetworkException { int responseCode = response.code(); String responseMessage = response.message(); ResponseBody responseBody = response.body(); @@ -1420,7 +1452,7 @@ public class PushServiceSocket { MismatchedDevices mismatchedDevices; try { - mismatchedDevices = JsonUtil.fromJson(responseBody.string(), MismatchedDevices.class); + mismatchedDevices = JsonUtil.fromJson(readBodyString(responseBody), MismatchedDevices.class); } catch (JsonProcessingException e) { Log.w(TAG, e); throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); @@ -1433,7 +1465,7 @@ public class PushServiceSocket { StaleDevices staleDevices; try { - staleDevices = JsonUtil.fromJson(responseBody.string(), StaleDevices.class); + staleDevices = JsonUtil.fromJson(readBodyString(responseBody), StaleDevices.class); } catch (JsonProcessingException e) { throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { @@ -1445,7 +1477,7 @@ public class PushServiceSocket { DeviceLimit deviceLimit; try { - deviceLimit = JsonUtil.fromJson(responseBody.string(), DeviceLimit.class); + deviceLimit = JsonUtil.fromJson(readBodyString(responseBody), DeviceLimit.class); } catch (JsonProcessingException e) { throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { @@ -1459,7 +1491,7 @@ public class PushServiceSocket { RegistrationLockFailure accountLockFailure; try { - accountLockFailure = JsonUtil.fromJson(responseBody.string(), RegistrationLockFailure.class); + accountLockFailure = JsonUtil.fromJson(readBodyString(responseBody), RegistrationLockFailure.class); } catch (JsonProcessingException e) { Log.w(TAG, e); throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); @@ -1479,7 +1511,7 @@ public class PushServiceSocket { throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } - return responseBody; + return response; } private Response getServiceConnection(String urlFragment, String method, RequestBody body, Map headers, Optional unidentifiedAccess) @@ -1807,6 +1839,15 @@ public class PushServiceSocket { } } + private static String readBodyString(ResponseBody body) throws IOException { + if (body != null) { + return body.string(); + } else { + throw new IOException("No body!"); + } + } + + private static class GcmRegistrationId { @JsonProperty diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceMessagesResult.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceMessagesResult.java new file mode 100644 index 000000000..089a08c9a --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SignalServiceMessagesResult.java @@ -0,0 +1,21 @@ +package org.whispersystems.signalservice.internal.push; + +import java.util.List; + +public final class SignalServiceMessagesResult { + private final List envelopes; + private final long serverDeliveredTimestamp; + + SignalServiceMessagesResult(List envelopes, long serverDeliveredTimestamp) { + this.envelopes = envelopes; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + } + + public List getEnvelopes() { + return envelopes; + } + + public long getServerDeliveredTimestamp() { + return serverDeliveredTimestamp; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java index 99f6cff67..a48be5e46 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java @@ -14,7 +14,8 @@ public final class SignalServiceMetadataProtobufSerializer { .setSenderDevice(metadata.getSenderDevice()) .setNeedsReceipt(metadata.isNeedsReceipt()) .setTimestamp(metadata.getTimestamp()) - .setServerTimestamp(metadata.getServerTimestamp()) + .setServerReceivedTimestamp(metadata.getServerReceivedTimestamp()) + .setServerDeliveredTimestamp(metadata.getServerDeliveredTimestamp()) .build(); } @@ -22,7 +23,8 @@ public final class SignalServiceMetadataProtobufSerializer { return new SignalServiceMetadata(SignalServiceAddressProtobufSerializer.fromProtobuf(metadata.getAddress()), metadata.getSenderDevice(), metadata.getTimestamp(), - metadata.getServerTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), metadata.getNeedsReceipt()); } } diff --git a/libsignal/service/src/main/proto/InternalSerialization.proto b/libsignal/service/src/main/proto/InternalSerialization.proto index d045925ed..ef01c908b 100644 --- a/libsignal/service/src/main/proto/InternalSerialization.proto +++ b/libsignal/service/src/main/proto/InternalSerialization.proto @@ -22,11 +22,12 @@ message SignalServiceContentProto { } message MetadataProto { - optional AddressProto address = 1; - optional int32 senderDevice = 2; - optional int64 timestamp = 3; - optional int64 serverTimestamp = 5; - optional bool needsReceipt = 4; + optional AddressProto address = 1; + optional int32 senderDevice = 2; + optional int64 timestamp = 3; + optional int64 serverReceivedTimestamp = 5; + optional int64 serverDeliveredTimestamp = 6; + optional bool needsReceipt = 4; } message AddressProto {