Handle session restoration.

This commit is contained in:
Mikunj 2019-12-06 11:35:10 +11:00
parent fd2dc678ea
commit 0caeb3a109
6 changed files with 110 additions and 32 deletions

View File

@ -3086,8 +3086,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void updateItemButtonPressed(@NonNull MessageRecord messageRecord) { public void updateItemButtonPressed(@NonNull MessageRecord messageRecord) {
// Loki - User clicked restore session // Loki - User clicked restore session
if (messageRecord.isNoRemoteSession() && !messageRecord.isLokiSessionRestoreSent()) { Recipient recipient = messageRecord.getRecipient();
// TODO: Send a message with `SESSION_RESTORE` flag if (!recipient.isGroupRecipient() && messageRecord.isNoRemoteSession() && !messageRecord.isLokiSessionRestoreSent()) {
MessageSender.sendRestoreSessionMessage(this, recipient.getAddress().serialize());
DatabaseFactory.getSmsDatabase(this).markAsLokiSessionRestoreSent(messageRecord.id); DatabaseFactory.getSmsDatabase(this).markAsLokiSessionRestoreSent(messageRecord.id);
TextSecurePreferences.setShowingSessionRestorePrompt(this, messageRecord.getIndividualRecipient().getAddress().serialize(), false); TextSecurePreferences.setShowingSessionRestorePrompt(this, messageRecord.getIndividualRecipient().getAddress().serialize(), false);
} }

View File

@ -74,6 +74,7 @@ import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities; import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.loki.DebouncerCache;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
@ -94,6 +95,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
@ -337,6 +339,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MultiDeviceUtilities.checkForRevocation(context); MultiDeviceUtilities.checkForRevocation(context);
} }
} else { } else {
// Loki - We shouldn't process session restore message any further
if (message.isSessionRestore()) { return; }
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
else if (message.isExpirationUpdate()) else if (message.isExpirationUpdate())
@ -1187,7 +1191,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
private void storePreKeyBundleIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) { private void storePreKeyBundleIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
if (content.lokiServiceMessage.isPresent()) { Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
if (!sender.isGroupRecipient() && content.lokiServiceMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get(); LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
if (lokiMessage.getPreKeyBundleMessage() != null) { if (lokiMessage.getPreKeyBundleMessage() != null) {
int registrationID = TextSecurePreferences.getLocalRegistrationId(context); int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
@ -1203,10 +1208,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// If we got a friend request and we were friends with this user then we need to reset our session // If we got a friend request and we were friends with this user then we need to reset our session
if (envelope.isFriendRequest()) { if (envelope.isFriendRequest()) {
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
long threadID = threadDatabase.getThreadIdIfExistsFor(sender); long threadID = threadDatabase.getThreadIdIfExistsFor(sender);
if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) { if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) {
resetSession(content.getSender(), threadID); resetSession(content.getSender(), threadID);
// Let our other devices know that we have reset session
MessageSender.syncContact(context, sender.getAddress());
} }
} }
} }
@ -1398,23 +1404,43 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleNoSessionMessage(@NonNull String sender, int senderDevice, long timestamp, private void handleNoSessionMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
{ {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); Recipient recipient = Recipient.from(context, Address.fromSerialized(sender), false);
if (recipient.isGroupRecipient()) { return; }
if (!smsMessageId.isPresent()) { long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
if (!TextSecurePreferences.isShowingSessionRestorePrompt(context, sender)) { LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp); /*
If we are friends with the user or we sent a friend request to them and we got a message back with no session then we want to try and restore the session automatically.
otherwise if we're not friends or our friend request expired then we need to prompt the user for action
*/
if (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
autoRestoreSession(sender);
} else if (friendRequestStatus == LokiThreadFriendRequestStatus.NONE || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (insertResult.isPresent()) { if (!smsMessageId.isPresent()) {
smsDatabase.markAsNoSession(insertResult.get().getMessageId()); if (!TextSecurePreferences.isShowingSessionRestorePrompt(context, sender)) {
TextSecurePreferences.setShowingSessionRestorePrompt(context, sender, true); Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
//MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
if (insertResult.isPresent()) {
smsDatabase.markAsNoSession(insertResult.get().getMessageId());
TextSecurePreferences.setShowingSessionRestorePrompt(context, sender, true);
//MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
} }
} else {
smsDatabase.markAsNoSession(smsMessageId.get());
} }
} else {
smsDatabase.markAsNoSession(smsMessageId.get());
} }
} }
private void autoRestoreSession(@NonNull String sender) {
// We don't want to keep spamming the user for an auto restore
String key = "restore_session_" + sender;
Debouncer debouncer = DebouncerCache.getDebouncer(key, 10000);
debouncer.publish(() -> MessageSender.sendRestoreSessionMessage(context, sender));
}
private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp, private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
{ {

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.loki
import org.thoughtcrime.securesms.util.Debouncer
object DebouncerCache {
private val cache: HashMap<String, Debouncer> = hashMapOf()
@JvmStatic
fun getDebouncer(key: String, threshold: Long): Debouncer {
val throttler = cache[key] ?: Debouncer(threshold)
cache[key] = throttler
return throttler
}
@JvmStatic
fun remove(key: String) {
cache.remove(key)
}
}

View File

@ -27,6 +27,15 @@ object FriendRequestHandler {
ActionType.Sent -> LokiThreadFriendRequestStatus.REQUEST_SENT ActionType.Sent -> LokiThreadFriendRequestStatus.REQUEST_SENT
} }
DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(threadId, threadFriendStatus) DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(threadId, threadFriendStatus)
// If we sent a friend request then we need to hide the session restore prompt
if (type == ActionType.Sent) {
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
smsDatabase.getMessages(threadId)
.filter { it.isNoRemoteSession && !it.isLokiSessionRestoreSent }
.forEach {
smsDatabase.markAsLokiSessionRestoreSent(it.id)
}
}
} }
// Update message status // Update message status

View File

@ -13,30 +13,43 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.util.JsonUtil import org.whispersystems.signalservice.internal.util.JsonUtil
import java.io.IOException import java.io.IOException
import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
data class BackgroundMessage private constructor(val recipient: String, val body: String?, val friendRequest: Boolean, val unpairingRequest: Boolean) { data class BackgroundMessage private constructor(val data: Map<String, Any>) {
companion object { companion object {
@JvmStatic @JvmStatic
fun create(recipient: String) = BackgroundMessage(recipient, null, false, false) fun create(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient))
@JvmStatic @JvmStatic
fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(recipient, messageBody, true, false) fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(mapOf(
"recipient" to recipient,
"body" to messageBody,
"friendRequest" to true
))
@JvmStatic @JvmStatic
fun createUnpairingRequest(recipient: String) = BackgroundMessage(recipient, null, false, true) fun createUnpairingRequest(recipient: String) = BackgroundMessage(mapOf(
"recipient" to recipient,
"unpairingRequest" to true
))
@JvmStatic
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf(
"recipient" to recipient,
"friendRequest" to true,
"sessionRestore" to true
))
internal fun parse(serialized: String): BackgroundMessage { internal fun parse(serialized: String): BackgroundMessage {
val node = JsonUtil.fromJson(serialized) val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map<String, Any> ?: throw AssertionError("JSON parsing failed")
val recipient = node.get("recipient").asText() return BackgroundMessage(data)
val body = if (node.hasNonNull("body")) node.get("body").asText() else null
val friendRequest = node.get("friendRequest").asBoolean(false)
val unpairingRequest = node.get("unpairingRequest").asBoolean(false)
return BackgroundMessage(recipient, body, friendRequest, unpairingRequest)
} }
} }
fun <T> get(key: String, defaultValue: T): T {
return data[key] as? T ?: defaultValue
}
fun serialize(): String { fun serialize(): String {
val map = mapOf("recipient" to recipient, "body" to body, "friendRequest" to friendRequest, "unpairingRequest" to unpairingRequest) return JsonUtil.toJson(data)
return JsonUtil.toJson(map)
} }
} }
@ -71,24 +84,31 @@ class PushBackgroundMessageSendJob private constructor(
} }
public override fun onRun() { public override fun onRun() {
val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException()
val dataMessage = SignalServiceDataMessage.newBuilder() val dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis()) .withTimestamp(System.currentTimeMillis())
.withBody(message.body) .withBody(message.get<String?>("body", null))
if (message.friendRequest) { if (message.get("friendRequest", false)) {
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(message.recipient) val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
dataMessage.withPreKeyBundle(bundle) dataMessage.withPreKeyBundle(bundle)
.asFriendRequest(true) .asFriendRequest(true)
} else if (message.unpairingRequest) { }
if (message.get("unpairingRequest", false)) {
dataMessage.asUnpairingRequest(true) dataMessage.asUnpairingRequest(true)
} }
if (message.get("sessionRestore", false)) {
dataMessage.asSessionRestore(true)
}
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(message.recipient) val address = SignalServiceAddress(recipient)
try { try {
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), dataMessage.build()) // The message ID doesn't matter messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), dataMessage.build()) // The message ID doesn't matter
} catch (e: Exception) { } catch (e: Exception) {
Log.d("Loki", "Failed to send background message to: ${message.recipient}.") Log.d("Loki", "Failed to send background message to: ${recipient}.")
throw e throw e
} }
} }

View File

@ -139,6 +139,10 @@ public class MessageSender {
public static void sendUnpairRequest(Context context, String contactHexEncodedPublicKey) { public static void sendUnpairRequest(Context context, String contactHexEncodedPublicKey) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createUnpairingRequest(contactHexEncodedPublicKey))); ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createUnpairingRequest(contactHexEncodedPublicKey)));
} }
public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(contactHexEncodedPublicKey)));
}
// endregion // endregion
public static long send(final Context context, public static long send(final Context context,