diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml
index 8acd2e172c..eba5f98e34 100644
--- a/res/layout/conversation_activity.xml
+++ b/res/layout/conversation_activity.xml
@@ -104,6 +104,11 @@
android:indeterminate="false"
android:progress="80" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f7dd0c4dc8..71c2c3e4ab 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -735,6 +735,7 @@
Bad encrypted message
Message encrypted for non-existing session
+ You have sent a session restoration request to %s
Bad encrypted MMS message
@@ -1651,6 +1652,10 @@
Device unlinked
This device has been successfully unlinked
+
+ Would you like to restore your session with %s?
+ Dismiss
+ Restore
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 7ba65c0aa2..d57facc332 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -141,6 +141,7 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
+import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -162,6 +163,7 @@ import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
+import org.thoughtcrime.securesms.loki.SessionRestoreBannerView;
import org.thoughtcrime.securesms.loki.redesign.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.redesign.views.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.redesign.views.MentionCandidateSelectionView;
@@ -248,6 +250,7 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -372,6 +375,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Multi Device
private boolean isFriendsWithAnyDevice = false;
+ // Restoration
+ protected SessionRestoreBannerView sessionRestoreBannerView;
+
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -446,6 +452,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
});
+ sessionRestoreBannerView.setOnRestore(() -> {
+ this.restoreSession();
+ return Unit.INSTANCE;
+ });
+ sessionRestoreBannerView.setOnDismiss(() -> {
+ // TODO: Maybe silence for x minutes?
+ DatabaseFactory.getLokiThreadDatabase(ConversationActivity.this).removeAllSessionRestoreDevices(threadId);
+ updateSessionRestoreBanner();
+ return Unit.INSTANCE;
+ });
+
LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, this);
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
@@ -539,6 +556,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DatabaseFactory.getLokiThreadDatabase(this).setDelegate(this);
updateInputPanel();
+ updateSessionRestoreBanner();
+
Log.i(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
}
@@ -1542,6 +1561,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
+ protected void updateSessionRestoreBanner() {
+ Set devices = DatabaseFactory.getLokiThreadDatabase(this).getSessionRestoreDevices(threadId);
+ if (devices.size() > 0) {
+ sessionRestoreBannerView.update(recipient);
+ sessionRestoreBannerView.show();
+ } else {
+ sessionRestoreBannerView.hide();
+ }
+ }
+
private void updateDefaultSubscriptionId(Optional defaultSubscriptionId) {
Log.i(TAG, "updateDefaultSubscriptionId(" + defaultSubscriptionId.orNull() + ")");
sendButton.setDefaultSubscriptionId(defaultSubscriptionId);
@@ -1639,6 +1668,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
mentionCandidateSelectionViewContainer = ViewUtil.findById(this, R.id.mentionCandidateSelectionViewContainer);
mentionCandidateSelectionView = ViewUtil.findById(this, R.id.userSelectionView);
+ sessionRestoreBannerView = ViewUtil.findById(this, R.id.sessionRestoreBannerView);
messageStatusProgressBar = ViewUtil.findById(this, R.id.messageStatusProgressBar);
muteIndicatorImageView = ViewUtil.findById(this, R.id.muteIndicatorImageView);
actionBarSubtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView);
@@ -2257,6 +2287,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
this.updateInputPanel();
}
+ @Override
+ public void handleSessionRestoreDevicesChanged(long threadId) {
+ if (threadId == this.threadId) {
+ runOnUiThread(this::updateSessionRestoreBanner);
+ }
+ }
+
private void updateInputPanel() {
/*
isFriendsWithAnyDevice caches whether we are friends with any of the other users device.
@@ -3262,4 +3299,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize());
}
// endregion
+
+ public void restoreSession() {
+ // Loki - User clicked restore session
+ if (recipient.isGroupRecipient()) { return; }
+ LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(this);
+ SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(this);
+ Set devices = lokiThreadDatabase.getSessionRestoreDevices(threadId);
+ for (String device : devices) { MessageSender.sendRestoreSessionMessage(this, device); }
+ long messageId = smsDatabase.insertMessageOutbox(threadId, new OutgoingTextMessage(recipient,"", 0, 0), false, System.currentTimeMillis(), null);
+ if (messageId > -1) {
+ smsDatabase.markAsLokiSessionRestoreSent(messageId);
+ }
+ lokiThreadDatabase.removeAllSessionRestoreDevices(threadId);
+ updateSessionRestoreBanner();
+ }
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index f6ba00526d..f89f0578de 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -361,7 +361,7 @@ public class ConversationFragment extends Fragment
if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() ||
messageRecord.isEndSession() || messageRecord.isIdentityUpdate() ||
- messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault())
+ messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault() || messageRecord.isLokiSessionRestoreSent())
{
actionMessage = true;
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
index ca2b715c30..1228f85cd3 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java
@@ -113,6 +113,7 @@ public class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
else if (messageRecord.isIdentityVerified() ||
messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
+ else if (messageRecord.isLokiSessionRestoreSent()) setTextMessageRecord(messageRecord);
else throw new AssertionError("Neither group nor log nor joined.");
if (batchSelected.contains(messageRecord)) setSelected(true);
@@ -202,6 +203,15 @@ public class ConversationUpdateItem extends LinearLayout
body.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
+
+ private void setTextMessageRecord(MessageRecord messageRecord) {
+ body.setText(messageRecord.getDisplayBody(getContext()));
+
+ icon.setVisibility(GONE);
+ title.setVisibility(GONE);
+ body.setVisibility(VISIBLE);
+ date.setVisibility(GONE);
+ }
@Override
public void onModified(Recipient recipient) {
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index 3872472a6d..af3e881a29 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -82,6 +82,9 @@ public interface MmsSmsColumns {
protected static final long ENCRYPTION_REMOTE_DUPLICATE_BIT = 0x04000000;
protected static final long ENCRYPTION_REMOTE_LEGACY_BIT = 0x02000000;
+ // Loki
+ protected static final long ENCRYPTION_LOKI_SESSION_RESTORE_SENT_BIT = 0x01000000;
+
public static boolean isDraftMessageType(long type) {
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
}
@@ -230,6 +233,10 @@ public interface MmsSmsColumns {
return (type & ENCRYPTION_REMOTE_NO_SESSION_BIT) != 0;
}
+ public static boolean isLokiSessionRestoreSentType(long type) {
+ return (type & ENCRYPTION_LOKI_SESSION_RESTORE_SENT_BIT) != 0;
+ }
+
public static boolean isLegacyType(long type) {
return (type & ENCRYPTION_REMOTE_LEGACY_BIT) != 0 ||
(type & ENCRYPTION_REMOTE_BIT) != 0;
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index c86922bd6a..2dcc72d5ea 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -47,6 +47,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.SecureRandom;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@@ -251,6 +252,10 @@ public class SmsDatabase extends MessagingDatabase {
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_NO_SESSION_BIT);
}
+ public void markAsLokiSessionRestoreSent(long id) {
+ updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_LOKI_SESSION_RESTORE_SENT_BIT);
+ }
+
public void markAsLegacyVersion(long id) {
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_LEGACY_BIT);
}
diff --git a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
index b0a22c36c0..314e2c1d9d 100644
--- a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
@@ -103,9 +103,9 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isKeyExchangeType(type);
}
- public boolean isEndSession() {
- return SmsDatabase.Types.isEndSessionType(type);
- }
+ public boolean isEndSession() { return SmsDatabase.Types.isEndSessionType(type); }
+
+ public boolean isLokiSessionRestoreSent() { return SmsDatabase.Types.isLokiSessionRestoreSentType(type); }
public boolean isGroupUpdate() {
return SmsDatabase.Types.isGroupUpdate(type);
diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 596c2f4853..0531562b73 100644
--- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -180,7 +180,7 @@ public abstract class MessageRecord extends DisplayRecord {
public boolean isUpdate() {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
- isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault();
+ isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent();
}
public boolean isMediaPending() {
diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
index 237d038148..fb7c4b2311 100644
--- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
@@ -82,6 +82,7 @@ public class SmsMessageRecord extends MessageRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
+ Recipient recipient = getRecipient();
if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (isCorruptedKeyExchange()) {
@@ -100,6 +101,8 @@ public class SmsMessageRecord extends MessageRecord {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
+ } else if (isLokiSessionRestoreSent()) {
+ return emphasisAdded(context.getString(R.string.MessageRecord_session_restore_sent, recipient.toShortString()));
} else if (isEndSession() && isOutgoing()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset));
} else if (isEndSession()) {
diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
index 7fae1482c3..6aba9e9165 100644
--- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
@@ -71,6 +71,7 @@ public class ThreadRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
+ Recipient recipient = getRecipient();
if (isGroupUpdate()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
} else if (isGroupQuit()) {
@@ -81,6 +82,8 @@ public class ThreadRecord extends DisplayRecord {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session));
+ } else if (isLokiSessionRestoreSent()) {
+ return emphasisAdded(context.getString(R.string.MessageRecord_session_restore_sent, recipient.toShortString()));
} else if (SmsDatabase.Types.isEndSessionType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_secure_session_reset));
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 29bef63756..9ec5d229a1 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
+import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
@@ -278,6 +279,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
cipher.validateBackgroundMessage(envelope, envelope.getContent());
}
+ // Loki - Ignore any friend requests that we got before restoration
+ if (envelope.isFriendRequest() && envelope.getTimestamp() < TextSecurePreferences.getRestorationTime(context)) {
+ Log.d("Loki", "Ignoring friend request received before restoration.");
+ return;
+ }
+
SignalServiceContent content = cipher.decrypt(envelope);
if (shouldIgnore(content)) {
@@ -331,6 +338,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MultiDeviceUtilities.checkForRevocation(context);
}
} else {
+ // Loki - Don't process session restore message any further
+ if (message.isSessionRestore()) { return; }
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
else if (message.isExpirationUpdate())
@@ -530,21 +539,24 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
if (threadId != null) {
+ resetSession(content.getSender(), threadId);
+ MessageNotifier.updateNotification(context, threadId);
+ }
+ }
+
+ private void resetSession(String hexEncodedPublicKey, long threadId) {
TextSecureSessionStore sessionStore = new TextSecureSessionStore(context);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
- Log.d("Loki", "Received a session reset request from: " + content.getSender() + "; archiving the session.");
+ Log.d("Loki", "Received a session reset request from: " + hexEncodedPublicKey + "; archiving the session.");
- sessionStore.archiveAllSessions(content.getSender());
+ sessionStore.archiveAllSessions(hexEncodedPublicKey);
lokiThreadDatabase.setSessionResetStatus(threadId, LokiThreadSessionResetStatus.REQUEST_RECEIVED);
- Log.d("Loki", "Sending a ping back to " + content.getSender() + ".");
- String contactID = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId).getAddress().toString();
- MessageSender.sendBackgroundMessage(context, contactID);
+ Log.d("Loki", "Sending a ping back to " + hexEncodedPublicKey + ".");
+ MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey);
SecurityEvent.broadcastSecurityUpdateEvent(context);
- MessageNotifier.updateNotification(context, threadId);
- }
}
private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message)
@@ -1177,17 +1189,30 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
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();
if (lokiMessage.getPreKeyBundleMessage() != null) {
int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
- // Store the latest PreKeyBundle
+ // Loki - Store the latest pre key bundle
if (registrationID > 0) {
- Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
+ Log.d("Loki", "Received a pre key bundle from: " + content.getSender() + ".");
PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
- lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
+ lokiPreKeyBundleDatabase.setPreKeyBundle(content.getSender(), preKeyBundle);
+
+ // Loki - If we received a friend request, but we were already friends with this user, then reset the session
+ if (envelope.isFriendRequest()) {
+ long threadID = threadDatabase.getThreadIdIfExistsFor(sender);
+ if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) {
+ resetSession(content.getSender(), threadID);
+ // Let our other devices know that we have reset the session
+ MessageSender.syncContact(context, sender.getAddress());
+ }
+ }
}
}
}
@@ -1357,21 +1382,40 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
+ private SmsMessageRecord getLastMessage(String sender) {
+ try {
+ SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
+ Recipient recipient = Recipient.from(context, Address.fromSerialized(sender), false);
+ long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
+ if (threadID < 0) { return null; }
+ int messageCount = smsDatabase.getMessageCountForThread(threadID);
+ if (messageCount <= 0) { return null; }
+ long lastMessageID = smsDatabase.getIDForMessageAtIndex(threadID, messageCount - 1);
+ return smsDatabase.getMessage(lastMessageID);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional smsMessageId)
{
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (!smsMessageId.isPresent()) {
- Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp);
+ SmsMessageRecord lastMessage = getLastMessage(sender);
+ if (lastMessage == null || !SmsDatabase.Types.isFailedDecryptType(lastMessage.getType())) {
+ Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp);
- if (insertResult.isPresent()) {
- smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId());
- MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+ if (insertResult.isPresent()) {
+ smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId());
+ MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+ }
}
} else {
smsDatabase.markAsDecryptFailed(smsMessageId.get());
}
+ triggerSessionRestorePrompt(sender);
}
private void handleNoSessionMessage(@NonNull String sender, int senderDevice, long timestamp,
@@ -1380,15 +1424,27 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (!smsMessageId.isPresent()) {
- Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp);
+ SmsMessageRecord lastMessage = getLastMessage(sender);
+ if (lastMessage == null || !SmsDatabase.Types.isNoRemoteSessionType(lastMessage.getType())) {
+ Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp);
- if (insertResult.isPresent()) {
- smsDatabase.markAsNoSession(insertResult.get().getMessageId());
- MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+ if (insertResult.isPresent()) {
+ smsDatabase.markAsNoSession(insertResult.get().getMessageId());
+ MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+ }
}
} else {
smsDatabase.markAsNoSession(smsMessageId.get());
}
+ triggerSessionRestorePrompt(sender);
+ }
+
+ private void triggerSessionRestorePrompt(@NonNull String sender) {
+ Recipient primaryRecipient = getPrimaryDeviceRecipient(sender);
+ if (!primaryRecipient.isGroupRecipient()) {
+ long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(primaryRecipient);
+ DatabaseFactory.getLokiThreadDatabase(context).addSessionRestoreDevice(threadID, sender);
+ }
}
private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp,
@@ -1779,7 +1835,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private boolean isGroupChatMessage(SignalServiceContent content) {
- return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupUpdate();
+ return content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupInfo().isPresent();
}
private void resetRecipientToPush(@NonNull Recipient recipient) {
diff --git a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
index 050f9a1bb6..a8d9cae2eb 100644
--- a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
+++ b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
@@ -26,7 +26,12 @@ object FriendRequestHandler {
ActionType.Failed -> LokiThreadFriendRequestStatus.NONE
ActionType.Sent -> LokiThreadFriendRequestStatus.REQUEST_SENT
}
- DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(threadId, threadFriendStatus)
+ val database = DatabaseFactory.getLokiThreadDatabase(context)
+ database.setFriendRequestStatus(threadId, threadFriendStatus)
+ // If we sent a friend request then we need to hide the session restore prompt
+ if (type == ActionType.Sent) {
+ database.removeAllSessionRestoreDevices(threadId)
+ }
}
// Update message status
@@ -56,6 +61,8 @@ object FriendRequestHandler {
@JvmStatic
fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) {
if (threadId < 0) { return }
+ val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
+ if (!recipient.address.isPhone) { return }
val messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId)
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
index 3b66359ea9..3dca0abaaa 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
@@ -8,11 +8,13 @@ import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.loki.api.LokiPublicChat
import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus
+import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol {
var delegate: LokiThreadDatabaseDelegate? = null
@@ -140,4 +142,26 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
override fun removePublicChat(threadID: Long) {
databaseHelper.writableDatabase.delete(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
}
+
+ // region Session Restore
+ fun addSessionRestoreDevice(threadID: Long, hexEncodedPublicKey: String) {
+ val devices = getSessionRestoreDevices(threadID).toMutableSet()
+ if (devices.add(hexEncodedPublicKey)) {
+ TextSecurePreferences.setStringPreference(context, "session_restore_devices_$threadID", devices.joinToString(","))
+ delegate?.handleSessionRestoreDevicesChanged(threadID)
+ }
+ }
+
+ fun getSessionRestoreDevices(threadID: Long): Set {
+ return TextSecurePreferences.getStringPreference(context, "session_restore_devices_$threadID", "")
+ .split(",")
+ .filter { PublicKeyValidation.isValid(it) }
+ .toSet()
+ }
+
+ fun removeAllSessionRestoreDevices(threadID: Long) {
+ TextSecurePreferences.setStringPreference(context, "session_restore_devices_$threadID", "")
+ delegate?.handleSessionRestoreDevicesChanged(threadID)
+ }
+ // endregion
}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabaseDelegate.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabaseDelegate.kt
index 9ef17f6366..ac50afe238 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabaseDelegate.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabaseDelegate.kt
@@ -3,4 +3,5 @@ package org.thoughtcrime.securesms.loki
interface LokiThreadDatabaseDelegate {
fun handleThreadFriendRequestStatusChanged(threadID: Long)
+ fun handleSessionRestoreDevicesChanged(threadID: Long)
}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
index d1a81005b8..a7b1d081ad 100644
--- a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
+++ b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
@@ -15,28 +15,34 @@ import org.whispersystems.signalservice.internal.util.JsonUtil
import java.io.IOException
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) {
+
companion object {
+
@JvmStatic
- fun create(recipient: String) = BackgroundMessage(recipient, null, false, false)
+ fun create(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient))
+
@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
- 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 {
- val node = JsonUtil.fromJson(serialized)
- val recipient = node.get("recipient").asText()
- 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)
+ val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map ?: throw AssertionError("JSON parsing failed")
+ return BackgroundMessage(data)
}
}
+ fun get(key: String, defaultValue: T): T {
+ return data[key] as? T ?: defaultValue
+ }
+
fun serialize(): String {
- val map = mapOf("recipient" to recipient, "body" to body, "friendRequest" to friendRequest, "unpairingRequest" to unpairingRequest)
- return JsonUtil.toJson(map)
+ return JsonUtil.toJson(data)
}
}
@@ -71,24 +77,31 @@ class PushBackgroundMessageSendJob private constructor(
}
public override fun onRun() {
+ val recipient = message.get("recipient", null) ?: throw IllegalStateException()
val dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
- .withBody(message.body)
+ .withBody(message.get("body", null))
- if (message.friendRequest) {
- val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(message.recipient)
+ if (message.get("friendRequest", false)) {
+ val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
dataMessage.withPreKeyBundle(bundle)
.asFriendRequest(true)
- } else if (message.unpairingRequest) {
+ }
+
+ if (message.get("unpairingRequest", false)) {
dataMessage.asUnpairingRequest(true)
}
+ if (message.get("sessionRestore", false)) {
+ dataMessage.asSessionRestore(true)
+ }
+
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
- val address = SignalServiceAddress(message.recipient)
+ val address = SignalServiceAddress(recipient)
try {
messageSender.sendMessage(-1, address, Optional.absent(), dataMessage.build()) // The message ID doesn't matter
} 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
}
}
diff --git a/src/org/thoughtcrime/securesms/loki/SeedActivity.kt b/src/org/thoughtcrime/securesms/loki/SeedActivity.kt
index 61d2741883..7d5f5d7785 100644
--- a/src/org/thoughtcrime/securesms/loki/SeedActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/SeedActivity.kt
@@ -206,6 +206,9 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDelegate, ScanListene
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey), keyPair.publicKey,
IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true)
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
+ if (mode == Mode.Restore) {
+ TextSecurePreferences.setRestorationTime(this, System.currentTimeMillis())
+ }
when (mode) {
Mode.Register -> Analytics.shared.track("Seed Created")
Mode.Restore -> Analytics.shared.track("Seed Restored")
diff --git a/src/org/thoughtcrime/securesms/loki/SessionRestoreBannerView.kt b/src/org/thoughtcrime/securesms/loki/SessionRestoreBannerView.kt
new file mode 100644
index 0000000000..0b8991077f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/SessionRestoreBannerView.kt
@@ -0,0 +1,40 @@
+package org.thoughtcrime.securesms.loki
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import kotlinx.android.synthetic.main.session_restore_banner.view.*
+
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.recipients.Recipient
+
+class SessionRestoreBannerView : LinearLayout {
+ lateinit var recipient: Recipient
+ var onDismiss: (() -> Unit)? = null
+ var onRestore: (() -> Unit)? = null
+
+ constructor(context: Context) : super(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.session_restore_banner, this, true)
+ restoreButton.setOnClickListener { onRestore?.invoke() }
+ dismissButton.setOnClickListener { onDismiss?.invoke() }
+ }
+
+ fun update(recipient: Recipient) {
+ this.recipient = recipient
+ messageTextView.text = context.getString(R.string.session_restore_banner_message, recipient.toShortString())
+ }
+
+ fun show() {
+ sessionRestoreBanner.visibility = View.VISIBLE
+ }
+
+ fun hide() {
+ sessionRestoreBanner.visibility = View.GONE
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt
index ed1f1951c8..50936cf3f9 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/RegisterActivity.kt
@@ -129,6 +129,7 @@ class RegisterActivity : BaseActionBarActivity() {
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true)
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
+ TextSecurePreferences.setRestorationTime(this, 0)
TextSecurePreferences.setHasViewedSeed(this, false)
val intent = Intent(this, DisplayNameActivity::class.java)
push(intent)
diff --git a/src/org/thoughtcrime/securesms/loki/redesign/activities/RestoreActivity.kt b/src/org/thoughtcrime/securesms/loki/redesign/activities/RestoreActivity.kt
index 87972cbd0b..7bc10e7253 100644
--- a/src/org/thoughtcrime/securesms/loki/redesign/activities/RestoreActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/redesign/activities/RestoreActivity.kt
@@ -87,6 +87,7 @@ class RestoreActivity : BaseActionBarActivity() {
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true)
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
+ TextSecurePreferences.setRestorationTime(this, System.currentTimeMillis())
TextSecurePreferences.setHasViewedSeed(this, true)
val intent = Intent(this, DisplayNameActivity::class.java)
push(intent)
diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java
index c60bb40231..9bb30f7dc6 100644
--- a/src/org/thoughtcrime/securesms/sms/MessageSender.java
+++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java
@@ -139,6 +139,10 @@ public class MessageSender {
public static void sendUnpairRequest(Context context, String 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
public static long send(final Context context,
diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index aef385c197..e6729ff233 100644
--- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -1236,6 +1236,14 @@ public class TextSecurePreferences {
public static boolean needsRevocationCheck(Context context) {
return getBooleanPreference(context, "needs_revocation", false);
}
+
+ public static void setRestorationTime(Context context, long time) {
+ setLongPreference(context, "restoration_time", time);
+ }
+
+ public static long getRestorationTime(Context context) {
+ return getLongPreference(context, "restoration_time", 0);
+ }
// endregion
public static void clearAll(Context context) {