mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge pull request #55 from loki-project/session-restore
Session restore
This commit is contained in:
commit
2781c30d7a
@ -104,6 +104,11 @@
|
||||
android:indeterminate="false"
|
||||
android:progress="80" />
|
||||
|
||||
<org.thoughtcrime.securesms.loki.SessionRestoreBannerView
|
||||
android:id="@+id/sessionRestoreBannerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/group_share_profile_view_stub"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -53,7 +53,7 @@
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:autoLink="none"
|
||||
android:gravity="center"
|
||||
android:linksClickable="true"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
|
73
res/layout/session_restore_banner.xml
Normal file
73
res/layout/session_restore_banner.xml
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/sessionRestoreBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/dialog_background"
|
||||
android:orientation="vertical"
|
||||
android:elevation="10dp">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="@color/separator" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/medium_spacing"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Session Out of Sync"
|
||||
android:textColor="@color/text"
|
||||
android:textStyle="bold"
|
||||
android:textSize="@dimen/medium_font_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:text="@string/session_restore_banner_message"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/UnimportantDialogButton"
|
||||
android:id="@+id/dismissButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/session_restore_banner_dismiss_button_title" />
|
||||
|
||||
<Button
|
||||
style="@style/ProminentDialogButton"
|
||||
android:id="@+id/restoreButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/small_button_height"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginLeft="@dimen/medium_spacing"
|
||||
android:text="@string/session_restore_banner_restore_button_title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="@color/separator" />
|
||||
|
||||
</LinearLayout>
|
@ -735,6 +735,7 @@
|
||||
<!-- MessageDisplayHelper -->
|
||||
<string name="MessageDisplayHelper_bad_encrypted_message">Bad encrypted message</string>
|
||||
<string name="MessageDisplayHelper_message_encrypted_for_non_existing_session">Message encrypted for non-existing session</string>
|
||||
<string name="MessageRecord_session_restore_sent">You have sent a session restoration request to %s</string>
|
||||
|
||||
<!-- MmsMessageRecord -->
|
||||
<string name="MmsMessageRecord_bad_encrypted_mms_message">Bad encrypted MMS message</string>
|
||||
@ -1651,6 +1652,10 @@
|
||||
<!-- Device unlink dialog -->
|
||||
<string name="dialog_device_unlink_title">Device unlinked</string>
|
||||
<string name="dialog_device_unlink_message">This device has been successfully unlinked</string>
|
||||
<!-- Session restore banner -->
|
||||
<string name="session_restore_banner_message">Would you like to restore your session with %s?</string>
|
||||
<string name="session_restore_banner_dismiss_button_title">Dismiss</string>
|
||||
<string name="session_restore_banner_restore_button_title">Restore</string>
|
||||
|
||||
<!-- Loki -->
|
||||
|
||||
|
@ -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<String> devices = DatabaseFactory.getLokiThreadDatabase(this).getSessionRestoreDevices(threadId);
|
||||
if (devices.size() > 0) {
|
||||
sessionRestoreBannerView.update(recipient);
|
||||
sessionRestoreBannerView.show();
|
||||
} else {
|
||||
sessionRestoreBannerView.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDefaultSubscriptionId(Optional<Integer> 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<String> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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()) {
|
||||
|
@ -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)) {
|
||||
|
@ -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<Long> smsMessageId)
|
||||
{
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
if (!smsMessageId.isPresent()) {
|
||||
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
||||
SmsMessageRecord lastMessage = getLastMessage(sender);
|
||||
if (lastMessage == null || !SmsDatabase.Types.isFailedDecryptType(lastMessage.getType())) {
|
||||
Optional<InsertResult> 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> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
||||
SmsMessageRecord lastMessage = getLastMessage(sender);
|
||||
if (lastMessage == null || !SmsDatabase.Types.isNoRemoteSessionType(lastMessage.getType())) {
|
||||
Optional<InsertResult> 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) {
|
||||
|
@ -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)
|
||||
|
@ -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<String> {
|
||||
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
|
||||
}
|
@ -3,4 +3,5 @@ package org.thoughtcrime.securesms.loki
|
||||
interface LokiThreadDatabaseDelegate {
|
||||
|
||||
fun handleThreadFriendRequestStatusChanged(threadID: Long)
|
||||
fun handleSessionRestoreDevicesChanged(threadID: Long)
|
||||
}
|
@ -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<String, Any>) {
|
||||
|
||||
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<String, Any> ?: throw AssertionError("JSON parsing failed")
|
||||
return BackgroundMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> 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<String?>("recipient", null) ?: throw IllegalStateException()
|
||||
val dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withBody(message.body)
|
||||
.withBody(message.get<String?>("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<UnidentifiedAccessPair>(), 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
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user