diff --git a/build.gradle b/build.gradle
index 92b3f51acb..dbf4e912ec 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,7 +61,7 @@ dependencies {
compile 'org.whispersystems:jobmanager:1.0.2'
compile 'org.whispersystems:libpastelog:1.0.7'
- compile 'org.whispersystems:signal-service-android:2.5.10'
+ compile 'org.whispersystems:signal-service-android:2.5.11'
compile 'org.whispersystems:webrtc-android:M57-S2'
compile "me.leolin:ShortcutBadger:1.1.16"
@@ -135,7 +135,7 @@ dependencyVerification {
'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718',
'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181',
'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88',
- 'org.whispersystems:signal-service-android:0c6937decce77b94807d100a16bcffc9e69489057b4430ca07a5bce618637b6e',
+ 'org.whispersystems:signal-service-android:355c2b139a7587bbde899261a0344bc6a32c1cf0baa5ac748f6c86190c07374c',
'org.whispersystems:webrtc-android:9d11e39d4b3823713e5b1486226e0ce09f989d6f47f52da1815e406c186701d5',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
@@ -169,7 +169,7 @@ dependencyVerification {
'com.google.android.gms:play-services-base:0ca636a8fc9a5af45e607cdcd61783bf5d561cbbb0f862021ce69606eee5ad49',
'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d',
'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70',
- 'org.whispersystems:signal-service-java:1de66a4068523098951529a363b4f02436e654d3d3fcf9228154b8c2cf94945b',
+ 'org.whispersystems:signal-service-java:02956dc0c22d8a6c7cf613b2e40cf54aec15a5e1cc5acab4d0f8b82bc22f2e0d',
'org.whispersystems:signal-protocol-android:b05cd9570d2e262afeb6610b70f473a936c54dd23a7c967d76e8f288766731fd',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
diff --git a/res/drawable-hdpi/ic_check_circle_white_18dp.png b/res/drawable-hdpi/ic_check_circle_white_18dp.png
new file mode 100644
index 0000000000..81c6ee054f
Binary files /dev/null and b/res/drawable-hdpi/ic_check_circle_white_18dp.png differ
diff --git a/res/drawable-mdpi/ic_check_circle_white_18dp.png b/res/drawable-mdpi/ic_check_circle_white_18dp.png
new file mode 100644
index 0000000000..0533ac82ea
Binary files /dev/null and b/res/drawable-mdpi/ic_check_circle_white_18dp.png differ
diff --git a/res/drawable-xhdpi/ic_check_circle_white_18dp.png b/res/drawable-xhdpi/ic_check_circle_white_18dp.png
new file mode 100644
index 0000000000..e190a5c537
Binary files /dev/null and b/res/drawable-xhdpi/ic_check_circle_white_18dp.png differ
diff --git a/res/drawable-xxhdpi/ic_check_circle_white_18dp.png b/res/drawable-xxhdpi/ic_check_circle_white_18dp.png
new file mode 100644
index 0000000000..8b16b1c2e1
Binary files /dev/null and b/res/drawable-xxhdpi/ic_check_circle_white_18dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_check_circle_white_18dp.png b/res/drawable-xxxhdpi/ic_check_circle_white_18dp.png
new file mode 100644
index 0000000000..4f967453e4
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_check_circle_white_18dp.png differ
diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml
index 26492329c5..5e5f06aef8 100644
--- a/res/layout/conversation_activity.xml
+++ b/res/layout/conversation_activity.xml
@@ -23,6 +23,12 @@
android:clipToPadding="false"
android:clipChildren="false">
+
+
+
diff --git a/res/layout/conversation_title_view.xml b/res/layout/conversation_title_view.xml
index 116833d20c..1c5478874b 100644
--- a/res/layout/conversation_title_view.xml
+++ b/res/layout/conversation_title_view.xml
@@ -21,14 +21,31 @@
style="@style/TextSecure.TitleTextStyle"
tools:ignore="UnusedAttribute"/>
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/unverified_banner_view.xml b/res/layout/unverified_banner_view.xml
new file mode 100644
index 0000000000..d6a6ab318d
--- /dev/null
+++ b/res/layout/unverified_banner_view.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/verify_display_fragment.xml b/res/layout/verify_display_fragment.xml
index 2c7603c239..2c94a73efc 100644
--- a/res/layout/verify_display_fragment.xml
+++ b/res/layout/verify_display_fragment.xml
@@ -12,17 +12,25 @@
android:background="?verification_background"
android:orientation="vertical">
-
+
+
+
+ android:visibility="invisible"
+ tools:src="@drawable/splash_logo"
+ tools:visibility="invisible"/>
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3fab75e9f8..2468a5ff9c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -421,6 +421,10 @@
You set disappearing message time to %1$s.
%1$s set disappearing message time to %2$s.
Your safety number with %s has changed.
+ You marked your safety number with %s verified
+ You marked your safety number with %s verified from another device
+ You marked your safety number with %s unverified
+ You marked your safety number with %s unverified from another device
@@ -594,6 +598,8 @@
Disappearing message time set to %s
Safety number changed
Your safety number with %s has changed.
+ You marked verified
+ You marked unverified
Signal update
@@ -851,6 +857,24 @@
%dw
+
+
+ Your safety number with %s has changed and is no longer verified
+ Your safety numbers with %1$s and %2$s are no longer verified
+ Your safety numbers with %1$s, %2$s, and %3$s are no longer verified
+
+ Your safety number with %1$s has changed and is no longer verified. This could either mean that someone is trying to intercept your communication, or that %1$s simply reinstalled Signal.
+ Your safety numbers with %1$s and %2$s are no longer verified. This could either mean that someone is trying to intercept your communication, or that they simply reinstalled Signal.
+ Your safety numbers with %1$s, %2$s, and %3$s are no longer verified. This could either mean that someone is trying to intercept your communication, or that they simply reinstalled Signal.
+
+ Your safety number with %s just changed.
+ Your safety number with %1$s and %2$s just changed.
+ Your safety number with %1$s, %2$s, and %3$s just changed.
+
+
+ - %d other
+ - %d others
+
Search GIFs and stickers
@@ -1022,8 +1046,10 @@
Add members
- Learn more about verifying safety numbers]]>
+ Learn more.]]>
Tap to scan
+ Loading...
+ Verified
Share safety number
@@ -1375,6 +1401,10 @@
Transport icon
+ Send message?
+ Send
+ Send message?
+ Send
diff --git a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java
index 334ce1fca7..a926447723 100644
--- a/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java
+++ b/src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java
@@ -110,7 +110,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(number, 1);
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
- identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true, true);
+ identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
}
processMessageRecord(messageRecord);
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index e92015b7fe..6216b4d122 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -85,11 +85,15 @@ import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
+import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog;
+import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
+import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.reminder.InviteReminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
+import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
@@ -98,12 +102,16 @@ import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientPreferenceEvent;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.mms.AttachmentManager;
@@ -141,6 +149,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
+import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -155,11 +164,13 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
+import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
/**
@@ -199,19 +210,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int PICK_GIF = 9;
private static final int SMS_DEFAULT = 10;
- private MasterSecret masterSecret;
- protected ComposeText composeText;
- private AnimatingToggle buttonToggle;
- private SendButton sendButton;
- private ImageButton attachButton;
- protected ConversationTitleView titleView;
- private TextView charactersLeft;
- private ConversationFragment fragment;
- private Button unblockButton;
- private Button makeDefaultSmsButton;
- private InputAwareLayout container;
- private View composePanel;
- protected Stub reminderView;
+ private MasterSecret masterSecret;
+ protected ComposeText composeText;
+ private AnimatingToggle buttonToggle;
+ private SendButton sendButton;
+ private ImageButton attachButton;
+ protected ConversationTitleView titleView;
+ private TextView charactersLeft;
+ private ConversationFragment fragment;
+ private Button unblockButton;
+ private Button makeDefaultSmsButton;
+ private InputAwareLayout container;
+ private View composePanel;
+ protected Stub reminderView;
+ private Stub unverifiedBannerView;
private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager;
@@ -231,8 +243,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private boolean isDefaultSms = true;
private boolean isMmsEnabled = true;
- private DynamicTheme dynamicTheme = new DynamicTheme();
- private DynamicLanguage dynamicLanguage = new DynamicLanguage();
+ private final IdentityRecordList identityRecords = new IdentityRecordList();
+ private final DynamicTheme dynamicTheme = new DynamicTheme();
+ private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@Override
protected void onPreCreate() {
@@ -308,6 +321,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeEnabledCheck();
initializeMmsEnabledCheck();
+ initializeIdentityRecords();
composeText.setTransport(sendButton.getSelectedTransport());
titleView.setTitle(recipients);
@@ -317,7 +331,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(threadId);
markThreadAsRead();
- markIdentitySeen();
Log.w(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
}
@@ -854,6 +867,56 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
startActivity(intent);
}
+ private void handleUnverifiedRecipients() {
+ List unverifiedRecipients = identityRecords.getUnverifiedRecipients(this);
+ List unverifiedRecords = identityRecords.getUnverifiedRecords();
+ String message = IdentityUtil.getUnverifiedSendDialogDescription(this, unverifiedRecipients);
+
+ if (message == null) return;
+
+ new UnverifiedSendDialog(this, message, unverifiedRecords, new UnverifiedSendDialog.ResendListener() {
+ @Override
+ public void onResendMessage() {
+ initializeIdentityRecords().addListener(new ListenableFuture.Listener() {
+ @Override
+ public void onSuccess(Boolean result) {
+ sendMessage();
+ }
+
+ @Override
+ public void onFailure(ExecutionException e) {
+ throw new AssertionError(e);
+ }
+ });
+ }
+ }).show();
+ }
+
+ private void handleUntrustedRecipients() {
+ List untrustedRecipients = identityRecords.getUntrustedRecipients(this);
+ List untrustedRecords = identityRecords.getUntrustedRecords();
+ String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients);
+
+ if (untrustedMessage == null) return;
+
+ new UntrustedSendDialog(this, untrustedMessage, untrustedRecords, new UntrustedSendDialog.ResendListener() {
+ @Override
+ public void onResendMessage() {
+ initializeIdentityRecords().addListener(new ListenableFuture.Listener() {
+ @Override
+ public void onSuccess(Boolean result) {
+ sendMessage();
+ }
+
+ @Override
+ public void onFailure(ExecutionException e) {
+ throw new AssertionError(e);
+ }
+ });
+ }
+ }).show();
+ }
+
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
this.isSecureText = isSecureText;
this.isDefaultSms = isDefaultSms;
@@ -1027,6 +1090,64 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute();
}
+ private ListenableFuture initializeIdentityRecords() {
+ final SettableFuture future = new SettableFuture<>();
+
+ new AsyncTask>() {
+ @Override
+ protected @NonNull Pair doInBackground(Recipients... params) {
+ try {
+ IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
+ IdentityRecordList identityRecordList = new IdentityRecordList();
+ Recipients recipients = params[0];
+
+ if (recipients.isGroupRecipient()) {
+ recipients = DatabaseFactory.getGroupDatabase(ConversationActivity.this)
+ .getGroupMembers(GroupUtil.getDecodedId(recipients.getPrimaryRecipient().getNumber()), false);
+ }
+
+ for (long recipientId : recipients.getIds()) {
+ Log.w(TAG, "Loading identity for: " + recipientId);
+ identityRecordList.add(identityDatabase.getIdentity(recipientId));
+ }
+
+ String message = null;
+
+ if (identityRecordList.isUnverified()) {
+ message = IdentityUtil.getUnverifiedBannerDescription(ConversationActivity.this, identityRecordList.getUnverifiedRecipients(ConversationActivity.this));
+ }
+
+ return new Pair<>(identityRecordList, message);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(@NonNull Pair result) {
+ Log.w(TAG, "Got identity records: " + result.first.isUnverified());
+ identityRecords.replaceWith(result.first);
+
+ if (result.second != null) {
+ Log.w(TAG, "Replacing banner...");
+ unverifiedBannerView.get().display(result.second, result.first.getUnverifiedRecords(),
+ new UnverifiedClickedListener(),
+ new UnverifiedDismissedListener());
+ } else if (unverifiedBannerView.resolved()) {
+ Log.w(TAG, "Clearing banner...");
+ unverifiedBannerView.get().hide();
+ }
+
+ titleView.setVerified(identityRecords.isVerified());
+
+ future.set(true);
+ }
+
+ }.execute(recipients);
+
+ return future;
+ }
+
private void initializeViews() {
titleView = (ConversationTitleView) getSupportActionBar().getCustomView();
buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
@@ -1040,6 +1161,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composePanel = ViewUtil.findById(this, R.id.bottom_panel);
container = ViewUtil.findById(this, R.id.layout_container);
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
+ unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
quickAttachmentDrawer = ViewUtil.findById(this, R.id.quick_attachment_drawer);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
@@ -1157,6 +1279,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void run() {
titleView.setTitle(recipients);
+ titleView.setVerified(identityRecords.isVerified());
setBlockedUserState(recipients, isSecureText, isDefaultSms);
setActionBarColor(recipients.getColor());
invalidateOptionsMenu();
@@ -1172,6 +1295,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onIdentityRecordUpdate(final IdentityRecord event) {
+ initializeIdentityRecords();
+ }
+
private void initializeReceivers() {
securityUpdateReceiver = new BroadcastReceiver() {
@Override
@@ -1443,17 +1571,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId);
}
- private void markIdentitySeen() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Recipient... params) {
- DatabaseFactory.getIdentityDatabase(ConversationActivity.this)
- .setSeen(params[0].getRecipientId());
- return null;
- }
- }.execute(recipients.getPrimaryRecipient());
- }
-
protected void sendComplete(long threadId) {
boolean refreshFragment = (threadId != this.threadId);
this.threadId = threadId;
@@ -1490,6 +1607,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if ((!recipients.isSingleRecipient() || recipients.isEmailRecipient()) && !isMmsEnabled) {
handleManualMmsRequired();
+ } else if (!forceSms && identityRecords.isUnverified()) {
+ handleUnverifiedRecipients();
+ } else if (!forceSms && identityRecords.isUntrusted()) {
+ handleUntrustedRecipients();
} else if (attachmentManager.isAttachmentPresent() || !recipients.isSingleRecipient() || recipients.isGroupRecipient() || recipients.isEmailRecipient()) {
sendMediaMessage(forceSms, expiresIn, subscriptionId);
} else {
@@ -1886,4 +2007,68 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
}
+
+ private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
+ @Override
+ public void onDismissed(final List unverifiedIdentities) {
+ final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(ConversationActivity.this);
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (SESSION_LOCK) {
+ for (IdentityRecord identityRecord : unverifiedIdentities) {
+ identityDatabase.setVerified(identityRecord.getRecipientId(),
+ identityRecord.getIdentityKey(),
+ VerifiedStatus.DEFAULT);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ initializeIdentityRecords();
+ }
+ }.execute();
+ }
+ }
+
+ private class UnverifiedClickedListener implements UnverifiedBannerView.ClickListener {
+ @Override
+ public void onClicked(final List unverifiedIdentities) {
+ Log.w(TAG, "onClicked: " + unverifiedIdentities.size());
+ if (unverifiedIdentities.size() == 1) {
+ Intent intent = new Intent(ConversationActivity.this, VerifyIdentityActivity.class);
+ intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, unverifiedIdentities.get(0).getRecipientId());
+ intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(unverifiedIdentities.get(0).getIdentityKey()));
+ intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
+
+ startActivity(intent);
+ } else {
+ String[] unverifiedNames = new String[unverifiedIdentities.size()];
+
+ for (int i=0;i
@Override
public int getItemViewType(@NonNull MessageRecord messageRecord) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() || messageRecord.isJoined() ||
- messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() || messageRecord.isIdentityUpdate()) {
+ messageRecord.isExpirationTimerUpdate() || messageRecord.isEndSession() ||
+ messageRecord.isIdentityUpdate() || messageRecord.isIdentityVerified() ||
+ messageRecord.isIdentityDefault())
+ {
return MESSAGE_TYPE_UPDATE;
} else if (hasAudio(messageRecord)) {
if (messageRecord.isOutgoing()) return MESSAGE_TYPE_AUDIO_OUTGOING;
diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java
index e2ea374297..5f65c94339 100644
--- a/src/org/thoughtcrime/securesms/ConversationFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationFragment.java
@@ -223,7 +223,8 @@ public class ConversationFragment extends Fragment
for (MessageRecord messageRecord : messageRecords) {
if (messageRecord.isGroupAction() || messageRecord.isCallLog() ||
messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() ||
- messageRecord.isEndSession() || messageRecord.isIdentityUpdate())
+ messageRecord.isEndSession() || messageRecord.isIdentityUpdate() ||
+ messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault())
{
actionMessage = true;
break;
diff --git a/src/org/thoughtcrime/securesms/ConversationTitleView.java b/src/org/thoughtcrime/securesms/ConversationTitleView.java
index 64e593b98a..93676a00bd 100644
--- a/src/org/thoughtcrime/securesms/ConversationTitleView.java
+++ b/src/org/thoughtcrime/securesms/ConversationTitleView.java
@@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
+import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -18,6 +19,7 @@ public class ConversationTitleView extends LinearLayout {
private TextView title;
private TextView subtitle;
+ private ImageView verified;
public ConversationTitleView(Context context) {
this(context, null);
@@ -32,8 +34,9 @@ public class ConversationTitleView extends LinearLayout {
public void onFinishInflate() {
super.onFinishInflate();
- this.title = (TextView) findViewById(R.id.title);
- this.subtitle = (TextView) findViewById(R.id.subtitle);
+ this.title = (TextView) findViewById(R.id.title);
+ this.subtitle = (TextView) findViewById(R.id.subtitle);
+ this.verified = (ImageView) findViewById(R.id.verified_indicator);
ViewUtil.setTextViewGravityStart(this.title, getContext());
ViewUtil.setTextViewGravityStart(this.subtitle, getContext());
@@ -53,6 +56,10 @@ public class ConversationTitleView extends LinearLayout {
}
}
+ public void setVerified(boolean verified) {
+ this.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
+ }
+
private void setComposeTitle() {
this.title.setText(R.string.ConversationActivity_compose_message);
this.subtitle.setText(null);
diff --git a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
index a7232f3d08..cbf321f3cc 100644
--- a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
+++ b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java
@@ -16,6 +16,8 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -24,7 +26,6 @@ import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
-import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
@@ -96,6 +97,8 @@ public class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
+ else if (messageRecord.isIdentityVerified() ||
+ messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord);
else throw new AssertionError("Neither group nor log nor joined.");
if (batchSelected.contains(messageRecord)) setSelected(true);
@@ -132,6 +135,15 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(View.GONE);
}
+ private void setIdentityVerifyUpdate(final MessageRecord messageRecord) {
+ if (messageRecord.isIdentityVerified()) icon.setImageResource(R.drawable.ic_check_white_24dp);
+ else icon.setImageResource(R.drawable.ic_info_outline_white_24dp);
+
+ icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
+ body.setText(messageRecord.getDisplayBody());
+ date.setVisibility(View.GONE);
+ }
+
private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp);
icon.clearColorFilter();
@@ -193,20 +205,25 @@ public class ConversationUpdateItem extends LinearLayout
@Override
public void onClick(View v) {
- if (!messageRecord.isIdentityUpdate() || !batchSelected.isEmpty()) {
+ if ((!messageRecord.isIdentityUpdate() &&
+ !messageRecord.isIdentityDefault() &&
+ !messageRecord.isIdentityVerified()) ||
+ !batchSelected.isEmpty())
+ {
if (parent != null) parent.onClick(v);
return;
}
final Recipient sender = ConversationUpdateItem.this.sender;
- IdentityUtil.getRemoteIdentityKey(getContext(), masterSecret, sender).addListener(new ListenableFuture.Listener>() {
+ IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener>() {
@Override
- public void onSuccess(Optional result) {
+ public void onSuccess(Optional result) {
if (result.isPresent()) {
Intent intent = new Intent(getContext(), VerifyIdentityActivity.class);
- intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, sender.getRecipientId());
- intent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(result.get()));
+ intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, sender.getRecipientId());
+ intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(result.get().getIdentityKey()));
+ intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, result.get().getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
getContext().startActivity(intent);
}
diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
index 4e4d3e0afa..7b844b0f0b 100644
--- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
+++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java
@@ -21,6 +21,7 @@ import android.support.v4.app.Fragment;
import android.support.v4.preference.PreferenceFragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
+import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
@@ -32,6 +33,8 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
@@ -302,9 +305,9 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
if (recipients.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
- IdentityUtil.getRemoteIdentityKey(getActivity(), masterSecret, recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener>() {
+ IdentityUtil.getRemoteIdentityKey(getActivity(), recipients.getPrimaryRecipient()).addListener(new ListenableFuture.Listener>() {
@Override
- public void onSuccess(Optional result) {
+ public void onSuccess(Optional result) {
if (result.isPresent()) {
if (identityPreference != null) identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
if (identityPreference != null) identityPreference.setEnabled(true);
@@ -458,17 +461,19 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
- private final IdentityKey identityKey;
+ private final IdentityRecord identityKey;
- private IdentityClickedListener(IdentityKey identityKey) {
+ private IdentityClickedListener(IdentityRecord identityKey) {
+ Log.w(TAG, "Identity record: " + identityKey);
this.identityKey = identityKey;
}
@Override
public boolean onPreferenceClick(Preference preference) {
Intent verifyIdentityIntent = new Intent(getActivity(), VerifyIdentityActivity.class);
- verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, recipients.getPrimaryRecipient().getRecipientId());
- verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(identityKey));
+ verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, recipients.getPrimaryRecipient().getRecipientId());
+ verifyIdentityIntent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey.getIdentityKey()));
+ verifyIdentityIntent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, identityKey.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
startActivity(verifyIdentityIntent);
return true;
diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
index b17ed6280a..8d6e060240 100644
--- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
+++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java
@@ -16,6 +16,8 @@
*/
package org.thoughtcrime.securesms;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@@ -26,13 +28,18 @@ import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.v4.animation.AnimatorCompatHelper;
+import android.support.v4.animation.ValueAnimatorCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
+import android.support.v7.widget.SwitchCompat;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
@@ -49,7 +56,7 @@ import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
-import android.widget.AdapterView;
+import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -59,14 +66,18 @@ import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
+import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
-import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -80,6 +91,8 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
+import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
+
/**
* Activity for verifying identity keys.
*
@@ -89,8 +102,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
- public static final String RECIPIENT_ID = "recipient_id";
- public static final String RECIPIENT_IDENTITY = "recipient_identity";
+ public static final String RECIPIENT_ID_EXTRA = "recipient_id";
+ public static final String IDENTITY_EXTRA = "recipient_identity";
+ public static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@@ -110,16 +124,18 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
- Recipient recipient = RecipientFactory.getRecipientForId(this, getIntent().getLongExtra(RECIPIENT_ID, -1), true);
+ Recipient recipient = RecipientFactory.getRecipientForId(this, getIntent().getLongExtra(RECIPIENT_ID_EXTRA, -1), true);
recipient.addListener(this);
setActionBarNotificationBarColor(recipient.getColor());
Bundle extras = new Bundle();
- extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(RECIPIENT_IDENTITY));
+ extras.putLong(VerifyDisplayFragment.REMOTE_RECIPIENT_ID, getIntent().getLongExtra(RECIPIENT_ID_EXTRA, -1));
+ extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
extras.putString(VerifyDisplayFragment.REMOTE_NUMBER, Util.canonicalizeNumber(this, recipient.getNumber()));
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
+ extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
scanFragment.setScanListener(this);
displayFragment.setClickListener(this);
@@ -212,16 +228,19 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
}
- public static class VerifyDisplayFragment extends Fragment implements Recipients.RecipientsModifiedListener {
+ public static class VerifyDisplayFragment extends Fragment implements Recipient.RecipientModifiedListener, CompoundButton.OnCheckedChangeListener {
- public static final String REMOTE_NUMBER = "remote_number";
- public static final String REMOTE_IDENTITY = "remote_identity";
- public static final String LOCAL_IDENTITY = "local_identity";
- public static final String LOCAL_NUMBER = "local_number";
+ public static final String REMOTE_RECIPIENT_ID = "remote_recipient_id";
+ public static final String REMOTE_NUMBER = "remote_number";
+ public static final String REMOTE_IDENTITY = "remote_identity";
+ public static final String LOCAL_IDENTITY = "local_identity";
+ public static final String LOCAL_NUMBER = "local_number";
+ public static final String VERIFIED_STATE = "verified_state";
- private Recipients recipient;
- private String localNumber;
- private String remoteNumber;
+ private MasterSecret masterSecret;
+ private Recipient recipient;
+ private String localNumber;
+ private String remoteNumber;
private IdentityKey localIdentity;
private IdentityKey remoteIdentity;
@@ -232,8 +251,10 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
private View numbersContainer;
private ImageView qrCode;
private ImageView qrVerified;
+ private TextView tapLabel;
private TextView description;
private View.OnClickListener clickListener;
+ private SwitchCompat verified;
private TextView[] codes = new TextView[12];
private boolean animateSuccessOnDraw = false;
@@ -244,8 +265,10 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.numbersContainer = ViewUtil.findById(container, R.id.number_table);
this.qrCode = ViewUtil.findById(container, R.id.qr_code);
+ this.verified = ViewUtil.findById(container, R.id.verified_switch);
this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
this.description = ViewUtil.findById(container, R.id.description);
+ this.tapLabel = ViewUtil.findById(container, R.id.tap_label);
this.codes[0] = ViewUtil.findById(container, R.id.code_first);
this.codes[1] = ViewUtil.findById(container, R.id.code_second);
this.codes[2] = ViewUtil.findById(container, R.id.code_third);
@@ -262,6 +285,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
this.qrCode.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
+ this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
+ this.verified.setOnCheckedChangeListener(this);
+
return container;
}
@@ -269,23 +295,37 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
+ this.masterSecret = getArguments().getParcelable("master_secret");
this.localNumber = getArguments().getString(LOCAL_NUMBER);
this.localIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(LOCAL_IDENTITY)).get();
this.remoteNumber = getArguments().getString(REMOTE_NUMBER);
- this.recipient = RecipientFactory.getRecipientsFromString(getActivity(), this.remoteNumber, true);
+ this.recipient = RecipientFactory.getRecipientForId(getActivity(), getArguments().getLong(REMOTE_RECIPIENT_ID), true);
this.remoteIdentity = ((IdentityKeyParcelable)getArguments().getParcelable(REMOTE_IDENTITY)).get();
- this.fingerprint = new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity,
- remoteNumber, remoteIdentity);
this.recipient.addListener(this);
+
+ new AsyncTask() {
+ @Override
+ protected Fingerprint doInBackground(Void... params) {
+ return new NumericFingerprintGenerator(5200).createFor(localNumber, localIdentity,
+ remoteNumber, remoteIdentity);
+ }
+
+ @Override
+ protected void onPostExecute(Fingerprint fingerprint) {
+ VerifyDisplayFragment.this.fingerprint = fingerprint;
+
+ setFingerprintViews(fingerprint, true);
+ }
+ }.execute();
}
@Override
- public void onModified(Recipients recipients) {
+ public void onModified(final Recipient recipient) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
- setFingerprintViews(fingerprint);
+ setRecipientText(recipient);
}
});
}
@@ -294,7 +334,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
public void onResume() {
super.onResume();
- setFingerprintViews(fingerprint);
+ setRecipientText(recipient);
+
+ if (fingerprint != null) {
+ setFingerprintViews(fingerprint, false);
+ }
if (animateSuccessOnDraw) {
animateSuccessOnDraw = false;
@@ -398,12 +442,19 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
}
}
- private void setFingerprintViews(Fingerprint fingerprint) {
+ private void setRecipientText(Recipient recipient) {
+ description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.toShortString())));
+ description.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+
+ private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
int partSize = digits.length() / codes.length;
for (int i=0;i= 11) {
+ ValueAnimator valueAnimator = new ValueAnimator();
+ valueAnimator.setObjectValues(0, Integer.parseInt(segment));
+
+ valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int value = (int) animation.getAnimatedValue();
+ codeView.setText(String.format("%05d", value));
+ }
+ });
+
+ valueAnimator.setEvaluator(new TypeEvaluator() {
+ public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
+ return Math.round(startValue + (endValue - startValue) * fraction);
+ }
+ });
+
+ valueAnimator.setDuration(1000);
+ valueAnimator.start();
+ } else {
+ codeView.setText(segment);
+ }
}
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
@@ -478,6 +562,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
ViewUtil.animateIn(qrVerified, scaleAnimation);
}
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Recipient... params) {
+ synchronized (SESSION_LOCK) {
+ if (isChecked) {
+ Log.w(TAG, "Saving identity: " + params[0].getRecipientId());
+ DatabaseFactory.getIdentityDatabase(getActivity())
+ .saveIdentity(params[0].getRecipientId(),
+ remoteIdentity,
+ VerifiedStatus.VERIFIED, false,
+ System.currentTimeMillis(), true);
+ } else {
+ DatabaseFactory.getIdentityDatabase(getActivity())
+ .setVerified(params[0].getRecipientId(),
+ remoteIdentity,
+ VerifiedStatus.DEFAULT);
+ }
+
+ ApplicationContext.getInstance(getActivity())
+ .getJobManager()
+ .add(new MultiDeviceVerifiedUpdateJob(getActivity(),
+ recipient.getNumber(),
+ remoteIdentity,
+ isChecked ? VerifiedStatus.VERIFIED :
+ VerifiedStatus.DEFAULT));
+
+ IdentityUtil.markIdentityVerified(getActivity(), new MasterSecretUnion(masterSecret), recipient, isChecked, false);
+ }
+ return null;
+ }
+ }.execute(recipient);
+ }
}
public static class VerifyScanFragment extends Fragment {
diff --git a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java
index 79058b9e2d..55d0bb7189 100644
--- a/src/org/thoughtcrime/securesms/WebRtcCallActivity.java
+++ b/src/org/thoughtcrime/securesms/WebRtcCallActivity.java
@@ -258,7 +258,7 @@ public class WebRtcCallActivity extends Activity {
public void onClick(View v) {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
- identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), theirIdentity, true, true);
+ identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.getNumber(), 1), theirIdentity, true);
}
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
diff --git a/src/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java b/src/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java
new file mode 100644
index 0000000000..0bd6268a4b
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/identity/UntrustedSendDialog.java
@@ -0,0 +1,66 @@
+package org.thoughtcrime.securesms.components.identity;
+
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+
+import java.util.List;
+
+import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
+
+public class UntrustedSendDialog extends AlertDialog.Builder implements DialogInterface.OnClickListener {
+
+ private final List untrustedRecords;
+ private final ResendListener resendListener;
+
+ public UntrustedSendDialog(@NonNull Context context,
+ @NonNull String message,
+ @NonNull List untrustedRecords,
+ @NonNull ResendListener resendListener)
+ {
+ super(context);
+ this.untrustedRecords = untrustedRecords;
+ this.resendListener = resendListener;
+
+ setTitle(R.string.UntrustedSendDialog_send_message);
+ setIconAttribute(R.attr.dialog_alert_icon);
+ setMessage(message);
+ setPositiveButton(R.string.UntrustedSendDialog_send, this);
+ setNegativeButton(android.R.string.cancel, null);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (SESSION_LOCK) {
+ for (IdentityRecord identityRecord : untrustedRecords) {
+ identityDatabase.setApproval(identityRecord.getRecipientId(), true);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ resendListener.onResendMessage();
+ }
+ }.execute();
+ }
+
+ public interface ResendListener {
+ public void onResendMessage();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/identity/UnverifiedBannerView.java b/src/org/thoughtcrime/securesms/components/identity/UnverifiedBannerView.java
new file mode 100644
index 0000000000..753d8df71c
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/identity/UnverifiedBannerView.java
@@ -0,0 +1,98 @@
+package org.thoughtcrime.securesms.components.identity;
+
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+import org.thoughtcrime.securesms.util.ViewUtil;
+
+import java.util.List;
+
+public class UnverifiedBannerView extends LinearLayout {
+
+ private static final String TAG = UnverifiedBannerView.class.getSimpleName();
+
+ private View container;
+ private TextView text;
+ private ImageView closeButton;
+
+ public UnverifiedBannerView(Context context) {
+ super(context);
+ initialize();
+ }
+
+ public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
+ public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public UnverifiedBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize();
+ }
+
+ private void initialize() {
+ LayoutInflater.from(getContext()).inflate(R.layout.unverified_banner_view, this, true);
+ this.container = ViewUtil.findById(this, R.id.container);
+ this.text = ViewUtil.findById(this, R.id.unverified_text);
+ this.closeButton = ViewUtil.findById(this, R.id.cancel);
+ }
+
+ public void display(@NonNull final String text,
+ @NonNull final List unverifiedIdentities,
+ @NonNull final ClickListener clickListener,
+ @NonNull final DismissListener dismissListener)
+ {
+ this.text.setText(text);
+ setVisibility(View.VISIBLE);
+
+ this.container.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Log.w(TAG, "onClick()");
+ clickListener.onClicked(unverifiedIdentities);
+ }
+ });
+
+ this.closeButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ hide();
+ dismissListener.onDismissed(unverifiedIdentities);
+ }
+ });
+ }
+
+ public void hide() {
+ setVisibility(View.GONE);
+ }
+
+ public interface DismissListener {
+ public void onDismissed(List unverifiedIdentities);
+ }
+
+ public interface ClickListener {
+ public void onClicked(List unverifiedIdentities);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/components/identity/UnverifiedSendDialog.java b/src/org/thoughtcrime/securesms/components/identity/UnverifiedSendDialog.java
new file mode 100644
index 0000000000..bf1e0cf300
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/identity/UnverifiedSendDialog.java
@@ -0,0 +1,67 @@
+package org.thoughtcrime.securesms.components.identity;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AlertDialog;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+
+import java.util.List;
+
+import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
+
+public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogInterface.OnClickListener {
+
+ private final List untrustedRecords;
+ private final ResendListener resendListener;
+
+ public UnverifiedSendDialog(@NonNull Context context,
+ @NonNull String message,
+ @NonNull List untrustedRecords,
+ @NonNull ResendListener resendListener)
+ {
+ super(context);
+ this.untrustedRecords = untrustedRecords;
+ this.resendListener = resendListener;
+
+ setTitle(R.string.UnverifiedSendDialog_send_message);
+ setIconAttribute(R.attr.dialog_alert_icon);
+ setMessage(message);
+ setPositiveButton(R.string.UnverifiedSendDialog_send, this);
+ setNegativeButton(android.R.string.cancel, null);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (SESSION_LOCK) {
+ for (IdentityRecord identityRecord : untrustedRecords) {
+ identityDatabase.setVerified(identityRecord.getRecipientId(),
+ identityRecord.getIdentityKey(),
+ IdentityDatabase.VerifiedStatus.DEFAULT);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ resendListener.onResendMessage();
+ }
+ }.execute();
+ }
+
+ public interface ResendListener {
+ public void onResendMessage();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/crypto/SessionUtil.java b/src/org/thoughtcrime/securesms/crypto/SessionUtil.java
index d1974face3..32d9c203f0 100644
--- a/src/org/thoughtcrime/securesms/crypto/SessionUtil.java
+++ b/src/org/thoughtcrime/securesms/crypto/SessionUtil.java
@@ -6,9 +6,12 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import java.util.List;
+
public class SessionUtil {
public static boolean hasSession(Context context, MasterSecret masterSecret, Recipient recipient) {
@@ -21,4 +24,23 @@ public class SessionUtil {
return sessionStore.containsSession(axolotlAddress);
}
+
+ public static void archiveSiblingSessions(Context context, SignalProtocolAddress address) {
+ SessionStore sessionStore = new TextSecureSessionStore(context);
+ List devices = sessionStore.getSubDeviceSessions(address.getName());
+ devices.add(1);
+
+ for (int device : devices) {
+ if (device != address.getDeviceId()) {
+ SignalProtocolAddress sibling = new SignalProtocolAddress(address.getName(), device);
+
+ if (sessionStore.containsSession(sibling)) {
+ SessionRecord sessionRecord = sessionStore.loadSession(sibling);
+ sessionRecord.archiveCurrentState();
+ sessionStore.storeSession(sibling, sessionRecord);
+ }
+ }
+ }
+
+ }
}
diff --git a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java
index 1e0524585e..5ecb7d5bd1 100644
--- a/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java
+++ b/src/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java
@@ -4,9 +4,11 @@ import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
+import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -42,9 +44,7 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return TextSecurePreferences.getLocalRegistrationId(context);
}
- public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey,
- boolean blockingApproval, boolean nonBlockingApproval)
- {
+ public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address.getName(), true);
@@ -53,20 +53,29 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
if (!identityRecord.isPresent()) {
Log.w(TAG, "Saving new identity...");
- identityDatabase.saveIdentity(recipientId, identityKey, true, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
+ identityDatabase.saveIdentity(recipientId, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
return false;
}
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
Log.w(TAG, "Replacing existing identity...");
- identityDatabase.saveIdentity(recipientId, identityKey, false, System.currentTimeMillis(), blockingApproval, nonBlockingApproval);
+ VerifiedStatus verifiedStatus;
+
+ if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) {
+ verifiedStatus = VerifiedStatus.UNVERIFIED;
+ } else {
+ verifiedStatus = VerifiedStatus.DEFAULT;
+ }
+
+ identityDatabase.saveIdentity(recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
IdentityUtil.markIdentityUpdate(context, recipients.getPrimaryRecipient());
+ SessionUtil.archiveSiblingSessions(context, address);
return true;
}
- if (isBlockingApprovalRequired(identityRecord.get()) || isNonBlockingApprovalRequired(identityRecord.get())) {
+ if (isNonBlockingApprovalRequired(identityRecord.get())) {
Log.w(TAG, "Setting approval status...");
- identityDatabase.setApproval(recipientId, blockingApproval, nonBlockingApproval);
+ identityDatabase.setApproval(recipientId, nonBlockingApproval);
return false;
}
@@ -76,7 +85,7 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
- return saveIdentity(address, identityKey, !TextSecurePreferences.isSendingIdentityApprovalRequired(context), false);
+ return saveIdentity(address, identityKey, false);
}
@Override
@@ -110,8 +119,8 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return false;
}
- if (isBlockingApprovalRequired(identityRecord.get())) {
- Log.w(TAG, "Needs blocking approval!");
+ if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
+ Log.w(TAG, "Needs unverified approval!");
return false;
}
@@ -123,12 +132,6 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
return true;
}
- private boolean isBlockingApprovalRequired(IdentityRecord identityRecord) {
- return !identityRecord.isFirstUse() &&
- TextSecurePreferences.isSendingIdentityApprovalRequired(context) &&
- !identityRecord.isApprovedBlocking();
- }
-
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() &&
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
index b7dc02514b..d752698607 100644
--- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
+++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java
@@ -871,14 +871,11 @@ public class DatabaseFactory {
if (oldVersion < INTRODUCED_IDENTITY_TIMESTAMP) {
db.execSQL("ALTER TABLE identities ADD COLUMN timestamp INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN first_use INTEGER DEFAULT 0");
- db.execSQL("ALTER TABLE identities ADD COLUMN seen INTEGER DEFAULT 0");
- db.execSQL("ALTER TABLE identities ADD COLUMN blocking_approval INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE identities ADD COLUMN nonblocking_approval INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE identities ADD COLuMN verified INTEGER DEFAULT 0");
db.execSQL("DROP INDEX archived_index");
db.execSQL("CREATE INDEX IF NOT EXISTS archived_count_index ON thread (archived, message_count)");
-
- db.execSQL("UPDATE identities SET blocking_approval = '1'");
}
db.setTransactionSuccessful();
diff --git a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java
index 60d9223569..e477e0ac0f 100644
--- a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java
@@ -21,9 +21,10 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.util.Log;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -35,17 +36,14 @@ public class IdentityDatabase extends Database {
private static final String TAG = IdentityDatabase.class.getSimpleName();
- private static final Uri CHANGE_URI = Uri.parse("content://textsecure/identities");
-
private static final String TABLE_NAME = "identities";
private static final String ID = "_id";
private static final String RECIPIENT = "recipient";
private static final String IDENTITY_KEY = "key";
private static final String TIMESTAMP = "timestamp";
private static final String FIRST_USE = "first_use";
- private static final String SEEN = "seen";
- private static final String BLOCKING_APPROVAL = "blocking_approval";
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
+ private static final String VERIFIED = "verified";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " +
@@ -53,14 +51,41 @@ public class IdentityDatabase extends Database {
IDENTITY_KEY + " TEXT, " +
FIRST_USE + " INTEGER DEFAULT 0, " +
TIMESTAMP + " INTEGER DEFAULT 0, " +
- SEEN + " INTEGER DEFAULT 0, " +
- BLOCKING_APPROVAL + " INTEGER DEFAULT 0, " +
+ VERIFIED + " INTEGER DEFAULT 0, " +
NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
+ public enum VerifiedStatus {
+ DEFAULT, VERIFIED, UNVERIFIED;
+
+ public int toInt() {
+ if (this == DEFAULT) return 0;
+ else if (this == VERIFIED) return 1;
+ else if (this == UNVERIFIED) return 2;
+ else throw new AssertionError();
+ }
+
+ public static VerifiedStatus forState(int state) {
+ if (state == 0) return DEFAULT;
+ else if (state == 1) return VERIFIED;
+ else if (state == 2) return UNVERIFIED;
+ else throw new AssertionError("No such state: " + state);
+ }
+ }
+
IdentityDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
+ public Cursor getIdentities() {
+ SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ return database.query(TABLE_NAME, null, null, null, null, null, null);
+ }
+
+ public @Nullable IdentityReader readerFor(@Nullable Cursor cursor) {
+ if (cursor == null) return null;
+ return new IdentityReader(cursor);
+ }
+
public Optional getIdentity(long recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@@ -70,15 +95,7 @@ public class IdentityDatabase extends Database {
new String[] {recipientId + ""}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
- String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
- long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
- long seen = cursor.getLong(cursor.getColumnIndexOrThrow(SEEN));
- boolean blockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKING_APPROVAL)) == 1;
- boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
- boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
- IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
-
- return Optional.of(new IdentityRecord(identity, firstUse, timestamp, seen, blockingApproval, nonblockingApproval));
+ return Optional.of(getIdentityRecord(cursor));
}
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
@@ -89,8 +106,8 @@ public class IdentityDatabase extends Database {
return Optional.absent();
}
- public void saveIdentity(long recipientId, IdentityKey identityKey, boolean firstUse,
- long timestamp, boolean blockingApproval, boolean nonBlockingApproval)
+ public void saveIdentity(long recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus,
+ boolean firstUse, long timestamp, boolean nonBlockingApproval)
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
@@ -99,60 +116,74 @@ public class IdentityDatabase extends Database {
contentValues.put(RECIPIENT, recipientId);
contentValues.put(IDENTITY_KEY, identityKeyString);
contentValues.put(TIMESTAMP, timestamp);
- contentValues.put(BLOCKING_APPROVAL, blockingApproval ? 1 : 0);
+ contentValues.put(VERIFIED, verifiedStatus.toInt());
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0);
contentValues.put(FIRST_USE, firstUse ? 1 : 0);
- contentValues.put(SEEN, 0);
database.replace(TABLE_NAME, null, contentValues);
- context.getContentResolver().notifyChange(CHANGE_URI, null);
+ EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus,
+ firstUse, timestamp, nonBlockingApproval));
}
- public void setApproval(long recipientId, boolean blockingApproval, boolean nonBlockingApproval) {
+ public void setApproval(long recipientId, boolean nonBlockingApproval) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(2);
- contentValues.put(BLOCKING_APPROVAL, blockingApproval);
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
database.update(TABLE_NAME, contentValues, RECIPIENT + " = ?",
new String[] {String.valueOf(recipientId)});
-
- context.getContentResolver().notifyChange(CHANGE_URI, null);
}
- public void setSeen(long recipientId) {
- Log.w(TAG, "Setting seen to current time: " + recipientId);
+ public void setVerified(long recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
- contentValues.put(SEEN, System.currentTimeMillis());
+ contentValues.put(VERIFIED, verifiedStatus.toInt());
- database.update(TABLE_NAME, contentValues, RECIPIENT + " = ? AND " + SEEN + " = 0",
- new String[] {String.valueOf(recipientId)});
+ database.update(TABLE_NAME, contentValues, RECIPIENT + " = ? AND " + IDENTITY_KEY + " = ?",
+ new String[] {String.valueOf(recipientId),
+ Base64.encodeBytes(identityKey.serialize())});
+ }
+
+ private IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException {
+ long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT));
+ String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
+ long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
+ int verifiedStatus = cursor.getInt(cursor.getColumnIndexOrThrow(VERIFIED));
+ boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
+ boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
+ IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
+
+ return new IdentityRecord(recipientId, identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval);
}
public static class IdentityRecord {
- private final IdentityKey identitykey;
- private final boolean firstUse;
- private final long timestamp;
- private final long seen;
- private final boolean blockingApproval;
- private final boolean nonblockingApproval;
+ private final long recipientId;
+ private final IdentityKey identitykey;
+ private final VerifiedStatus verifiedStatus;
+ private final boolean firstUse;
+ private final long timestamp;
+ private final boolean nonblockingApproval;
- private IdentityRecord(IdentityKey identitykey, boolean firstUse, long timestamp,
- long seen, boolean blockingApproval, boolean nonblockingApproval)
+ private IdentityRecord(long recipientId,
+ IdentityKey identitykey, VerifiedStatus verifiedStatus,
+ boolean firstUse, long timestamp, boolean nonblockingApproval)
{
+ this.recipientId = recipientId;
this.identitykey = identitykey;
+ this.verifiedStatus = verifiedStatus;
this.firstUse = firstUse;
this.timestamp = timestamp;
- this.seen = seen;
- this.blockingApproval = blockingApproval;
this.nonblockingApproval = nonblockingApproval;
}
+ public long getRecipientId() {
+ return recipientId;
+ }
+
public IdentityKey getIdentityKey() {
return identitykey;
}
@@ -161,12 +192,8 @@ public class IdentityDatabase extends Database {
return timestamp;
}
- public long getSeen() {
- return seen;
- }
-
- public boolean isApprovedBlocking() {
- return blockingApproval;
+ public VerifiedStatus getVerifiedStatus() {
+ return verifiedStatus;
}
public boolean isApprovedNonBlocking() {
@@ -177,6 +204,35 @@ public class IdentityDatabase extends Database {
return firstUse;
}
+ @Override
+ public String toString() {
+ return "{recipientId: " + recipientId + ", identityKey: " + identitykey + ", verifiedStatus: " + verifiedStatus + ", firstUse: " + firstUse + "}";
+ }
+
+ }
+
+ public class IdentityReader {
+ private final Cursor cursor;
+
+ public IdentityReader(@NonNull Cursor cursor) {
+ this.cursor = cursor;
+ }
+
+ public @Nullable IdentityRecord getNext() {
+ if (cursor.moveToNext()) {
+ try {
+ return getIdentityRecord(cursor);
+ } catch (IOException | InvalidKeyException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ return null;
+ }
+
+ public void close() {
+ cursor.close();
+ }
}
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index 79a904a680..59849c96ff 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -50,15 +50,15 @@ public interface MmsSmsColumns {
protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
// Key Exchange Information
- protected static final long KEY_EXCHANGE_MASK = 0xFF00;
- protected static final long KEY_EXCHANGE_BIT = 0x8000;
- protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000;
- protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000;
- protected static final long KEY_EXCHANGE_CORRUPTED_BIT = 0x1000;
- protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800;
- protected static final long KEY_EXCHANGE_BUNDLE_BIT = 0x400;
- protected static final long KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200;
- protected static final long KEY_EXCHANGE_CONTENT_FORMAT = 0x100;
+ protected static final long KEY_EXCHANGE_MASK = 0xFF00;
+ protected static final long KEY_EXCHANGE_BIT = 0x8000;
+ protected static final long KEY_EXCHANGE_IDENTITY_VERIFIED_BIT = 0x4000;
+ protected static final long KEY_EXCHANGE_IDENTITY_DEFAULT_BIT = 0x2000;
+ protected static final long KEY_EXCHANGE_CORRUPTED_BIT = 0x1000;
+ protected static final long KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800;
+ protected static final long KEY_EXCHANGE_BUNDLE_BIT = 0x400;
+ protected static final long KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200;
+ protected static final long KEY_EXCHANGE_CONTENT_FORMAT = 0x100;
// Secure Message Information
protected static final long SECURE_MESSAGE_BIT = 0x800000;
@@ -112,7 +112,7 @@ public interface MmsSmsColumns {
public static boolean isPendingMessageType(long type) {
return
(type & BASE_TYPE_MASK) == BASE_OUTBOX_TYPE ||
- (type & BASE_TYPE_MASK) == BASE_SENDING_TYPE;
+ (type & BASE_TYPE_MASK) == BASE_SENDING_TYPE;
}
public static boolean isPendingSmsFallbackType(long type) {
@@ -152,12 +152,12 @@ public interface MmsSmsColumns {
return (type & KEY_EXCHANGE_BIT) != 0;
}
- public static boolean isStaleKeyExchange(long type) {
- return (type & KEY_EXCHANGE_STALE_BIT) != 0;
+ public static boolean isIdentityVerified(long type) {
+ return (type & KEY_EXCHANGE_IDENTITY_VERIFIED_BIT) != 0;
}
- public static boolean isProcessedKeyExchange(long type) {
- return (type & KEY_EXCHANGE_PROCESSED_BIT) != 0;
+ public static boolean isIdentityDefault(long type) {
+ return (type & KEY_EXCHANGE_IDENTITY_DEFAULT_BIT) != 0;
}
public static boolean isCorruptedKeyExchange(long type) {
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index ec400b249e..d830bd2b32 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -521,6 +521,9 @@ public class SmsDatabase extends MessagingDatabase {
if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
if (message.isContentPreKeyBundle()) type |= Types.KEY_EXCHANGE_CONTENT_FORMAT;
+ if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
+ else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT;
+
Recipients recipients;
if (message.getSender() != null) {
@@ -540,7 +543,7 @@ public class SmsDatabase extends MessagingDatabase {
boolean unread = (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) ||
message.isSecureMessage() || message.isGroup() || message.isPreKeyBundle()) &&
- !message.isIdentityUpdate();
+ !message.isIdentityUpdate() && !message.isIdentityDefault() && !message.isIdentityVerified();
long threadId;
@@ -577,7 +580,7 @@ public class SmsDatabase extends MessagingDatabase {
DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
}
- if (!message.isIdentityUpdate()) {
+ if (!message.isIdentityUpdate() && !message.isIdentityVerified() && !message.isIdentityDefault()) {
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
}
@@ -605,6 +608,9 @@ public class SmsDatabase extends MessagingDatabase {
else if (message.isEndSession()) type |= Types.END_SESSION_BIT;
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
+ if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
+ else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT;
+
String address = message.getRecipients().getPrimaryRecipient().getNumber();
ContentValues contentValues = new ContentValues(6);
diff --git a/src/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java b/src/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java
new file mode 100644
index 0000000000..ad7679c350
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java
@@ -0,0 +1,116 @@
+package org.thoughtcrime.securesms.database.identity;
+
+
+import android.content.Context;
+
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientFactory;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class IdentityRecordList {
+
+ private static final String TAG = IdentityRecordList.class.getSimpleName();
+
+ private final List identityRecords = new LinkedList<>();
+
+ public void add(Optional identityRecord) {
+ if (identityRecord.isPresent()) {
+ identityRecords.add(identityRecord.get());
+ }
+ }
+
+ public void replaceWith(IdentityRecordList identityRecordList) {
+ identityRecords.clear();
+ identityRecords.addAll(identityRecordList.identityRecords);
+ }
+
+ public boolean isVerified() {
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {
+ return false;
+ }
+ }
+
+ return identityRecords.size() > 0;
+ }
+
+ public boolean isUnverified() {
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public boolean isUntrusted() {
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (isUntrusted(identityRecord)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public List getUntrustedRecords() {
+ List results = new LinkedList<>();
+
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (isUntrusted(identityRecord)) {
+ results.add(identityRecord);
+ }
+ }
+
+ return results;
+ }
+
+ public List getUntrustedRecipients(Context context) {
+ List untrusted = new LinkedList<>();
+
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (isUntrusted(identityRecord)) {
+ untrusted.add(RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), false));
+ }
+ }
+
+ return untrusted;
+ }
+
+ public List getUnverifiedRecords() {
+ List results = new LinkedList<>();
+
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
+ results.add(identityRecord);
+ }
+ }
+
+ return results;
+ }
+
+ public List getUnverifiedRecipients(Context context) {
+ List unverified = new LinkedList<>();
+
+ for (IdentityRecord identityRecord : identityRecords) {
+ if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
+ unverified.add(RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), false));
+ }
+ }
+
+ return unverified;
+ }
+
+ private boolean isUntrusted(IdentityRecord identityRecord) {
+ return !identityRecord.isApprovedNonBlocking() &&
+ System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(5);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
index df6f6f80c5..f480033a10 100644
--- a/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/DisplayRecord.java
@@ -70,7 +70,9 @@ public abstract class DisplayRecord {
}
public boolean isPending() {
- return MmsSmsColumns.Types.isPendingMessageType(type);
+ return MmsSmsColumns.Types.isPendingMessageType(type) &&
+ !MmsSmsColumns.Types.isIdentityVerified(type) &&
+ !MmsSmsColumns.Types.isIdentityDefault(type);
}
public boolean isOutgoing() {
diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 7e77e9c5d8..ff269e04d0 100644
--- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -114,6 +114,12 @@ public abstract class MessageRecord extends DisplayRecord {
: emphasisAdded(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(), time));
} else if (isIdentityUpdate()) {
return emphasisAdded(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString()));
+ } else if (isIdentityVerified()) {
+ if (isOutgoing()) return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString()));
+ else return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString()));
+ } else if (isIdentityDefault()) {
+ if (isOutgoing()) return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString()));
+ else return emphasisAdded(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString()));
} else if (getBody().getBody().length() > MAX_DISPLAY_LENGTH) {
return new SpannableString(getBody().getBody().substring(0, MAX_DISPLAY_LENGTH));
}
@@ -140,12 +146,12 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isForcedSms(type);
}
- public boolean isStaleKeyExchange() {
- return SmsDatabase.Types.isStaleKeyExchange(type);
+ public boolean isIdentityVerified() {
+ return SmsDatabase.Types.isIdentityVerified(type);
}
- public boolean isProcessedKeyExchange() {
- return SmsDatabase.Types.isProcessedKeyExchange(type);
+ public boolean isIdentityDefault() {
+ return SmsDatabase.Types.isIdentityDefault(type);
}
public boolean isIdentityMismatchFailure() {
diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
index 796d08d48b..2a8ab4a3b8 100644
--- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
@@ -64,10 +64,6 @@ public class SmsMessageRecord extends MessageRecord {
public SpannableString getDisplayBody() {
if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
- } else if (isProcessedKeyExchange()) {
- return new SpannableString("");
- } else if (isStaleKeyExchange()) {
- return emphasisAdded(context.getString(R.string.ConversationItem_error_received_stale_key_exchange_message));
} else if (isCorruptedKeyExchange()) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message));
} else if (isInvalidVersionKeyExchange()) {
diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
index 18cf7e0aa0..8f21299871 100644
--- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java
@@ -105,6 +105,10 @@ public class ThreadRecord extends DisplayRecord {
} else if (SmsDatabase.Types.isIdentityUpdate(type)) {
if (getRecipients().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipients().getPrimaryRecipient().toShortString()));
+ } else if (SmsDatabase.Types.isIdentityVerified(type)) {
+ return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
+ } else if (SmsDatabase.Types.isIdentityDefault(type)) {
+ return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_unverified));
} else {
if (TextUtils.isEmpty(getBody().getBody())) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java
index f432b0ef90..97f4ec7358 100644
--- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java
+++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
+import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
@@ -61,7 +62,8 @@ import dagger.Provides;
AvatarDownloadJob.class,
RotateSignedPreKeyJob.class,
WebRtcCallService.class,
- RetrieveProfileJob.class})
+ RetrieveProfileJob.class,
+ MultiDeviceVerifiedUpdateJob.class})
public class SignalCommunicationModule {
private final Context context;
diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java
new file mode 100644
index 0000000000..92028384b6
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java
@@ -0,0 +1,153 @@
+package org.thoughtcrime.securesms.jobs;
+
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.Log;
+
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
+import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
+import org.thoughtcrime.securesms.dependencies.InjectableType;
+import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientFactory;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.Util;
+import org.whispersystems.jobqueue.JobParameters;
+import org.whispersystems.jobqueue.requirements.NetworkRequirement;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
+import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+public class MultiDeviceVerifiedUpdateJob extends ContextJob implements InjectableType {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String TAG = MultiDeviceVerifiedUpdateJob.class.getSimpleName();
+
+ @Inject
+ transient SignalCommunicationModule.SignalMessageSenderFactory messageSenderFactory;
+
+ private final String destination;
+ private final byte[] identityKey;
+ private final VerifiedStatus verifiedStatus;
+
+ public MultiDeviceVerifiedUpdateJob(Context context, String destination, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
+ super(context, JobParameters.newBuilder()
+ .withRequirement(new NetworkRequirement(context))
+ .withPersistence()
+ .withGroupId("__MULTI_DEVICE_VERIFIED_UPDATE__")
+ .create());
+
+ this.destination = destination;
+ this.identityKey = identityKey.serialize();
+ this.verifiedStatus = verifiedStatus;
+ }
+
+ public MultiDeviceVerifiedUpdateJob(Context context) {
+ super(context, JobParameters.newBuilder()
+ .withRequirement(new NetworkRequirement(context))
+ .withPersistence()
+ .withGroupId("__MULTI_DEVICE_VERIFIED_UPDATE__")
+ .create());
+ this.destination = null;
+ this.identityKey = null;
+ this.verifiedStatus = null;
+ }
+
+
+ @Override
+ public void onRun() throws IOException, UntrustedIdentityException {
+ try {
+ if (!TextSecurePreferences.isMultiDevice(context)) {
+ Log.w(TAG, "Not multi device...");
+ return;
+ }
+
+ if (destination != null) sendSpecificUpdate(destination, identityKey, verifiedStatus);
+ else sendFullUpdate();
+
+ } catch (InvalidNumberException | InvalidKeyException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private void sendSpecificUpdate(String destination, byte[] identityKey, VerifiedStatus verifiedStatus)
+ throws IOException, UntrustedIdentityException, InvalidNumberException, InvalidKeyException
+ {
+ String canonicalDestination = Util.canonicalizeNumber(context, destination);
+ VerifiedMessage.VerifiedState verifiedState = getVerifiedState(verifiedStatus);
+ SignalServiceMessageSender messageSender = messageSenderFactory.create();
+ VerifiedMessage verifiedMessage = new VerifiedMessage(canonicalDestination, new IdentityKey(identityKey, 0), verifiedState);
+
+ messageSender.sendMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
+ }
+
+ private void sendFullUpdate() throws IOException, UntrustedIdentityException, InvalidNumberException {
+ IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
+ IdentityDatabase.IdentityReader reader = identityDatabase.readerFor(identityDatabase.getIdentities());
+ List verifiedMessages = new LinkedList<>();
+
+ try {
+ IdentityRecord identityRecord;
+
+ while (reader != null && (identityRecord = reader.getNext()) != null) {
+ if (identityRecord.getVerifiedStatus() != VerifiedStatus.DEFAULT) {
+ Recipient recipient = RecipientFactory.getRecipientForId(context, identityRecord.getRecipientId(), true);
+ String destination = Util.canonicalizeNumber(context, recipient.getNumber());
+ VerifiedMessage.VerifiedState verifiedState = getVerifiedState(identityRecord.getVerifiedStatus());
+
+ verifiedMessages.add(new VerifiedMessage(destination, identityRecord.getIdentityKey(), verifiedState));
+ }
+ }
+ } finally {
+ if (reader != null) reader.close();
+ }
+
+ if (!verifiedMessages.isEmpty()) {
+ SignalServiceMessageSender messageSender = messageSenderFactory.create();
+ messageSender.sendMessage(SignalServiceSyncMessage.forVerified(verifiedMessages));
+ }
+ }
+
+ private VerifiedMessage.VerifiedState getVerifiedState(VerifiedStatus status) {
+ VerifiedMessage.VerifiedState verifiedState;
+
+ switch (status) {
+ case DEFAULT: verifiedState = VerifiedMessage.VerifiedState.DEFAULT; break;
+ case VERIFIED: verifiedState = VerifiedMessage.VerifiedState.VERIFIED; break;
+ case UNVERIFIED: verifiedState = VerifiedMessage.VerifiedState.UNVERIFIED; break;
+ default: throw new AssertionError("Unknown status: " + verifiedStatus);
+ }
+
+ return verifiedState;
+ }
+
+ @Override
+ public boolean onShouldRetry(Exception exception) {
+ return exception instanceof PushNetworkException;
+ }
+
+ @Override
+ public void onAdded() {
+
+ }
+
+ @Override
+ public void onCanceled() {
+
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 9b2f18aa42..5c09002c10 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
+import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
@@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
+import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.DuplicateMessageException;
@@ -75,13 +77,12 @@ import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import org.thoughtcrime.securesms.mms.MmsException;
-
public class PushDecryptJob extends ContextJob {
private static final long serialVersionUID = 2L;
@@ -165,9 +166,10 @@ public class PushDecryptJob extends ContextJob {
} else if (content.getSyncMessage().isPresent()) {
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
- if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, envelope, syncMessage.getSent().get(), smsMessageId);
- else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get());
- else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(masterSecret, syncMessage.getRead().get(), envelope.getTimestamp());
+ if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, envelope, syncMessage.getSent().get(), smsMessageId);
+ else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get());
+ else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(masterSecret, syncMessage.getRead().get(), envelope.getTimestamp());
+ else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(masterSecret, syncMessage.getVerified().get());
else Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
Log.w(TAG, "Got call message...");
@@ -389,6 +391,14 @@ public class PushDecryptJob extends ContextJob {
}
}
+ private void handleSynchronizeVerifiedMessage(@NonNull MasterSecretUnion masterSecret,
+ @NonNull List verifiedMessages)
+ {
+ for (VerifiedMessage verifiedMessage : verifiedMessages) {
+ IdentityUtil.processVerifiedMessage(context, masterSecret, verifiedMessage);
+ }
+ }
+
private void handleSynchronizeSentMessage(@NonNull MasterSecretUnion masterSecret,
@NonNull SignalServiceEnvelope envelope,
@NonNull SentTranscriptMessage message,
@@ -430,6 +440,10 @@ public class PushDecryptJob extends ContextJob {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new MultiDeviceContactUpdateJob(getContext()));
+
+ ApplicationContext.getInstance(context)
+ .getJobManager()
+ .add(new MultiDeviceVerifiedUpdateJob(getContext()));
}
if (message.isGroupsRequest()) {
diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
index ad98e2d2ea..6f8f214281 100644
--- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
+++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java
@@ -219,11 +219,6 @@ public class MessageNotifier {
if (isVisible) {
List messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds);
-
- if (recipients != null && recipients.getPrimaryRecipient() != null) {
- DatabaseFactory.getIdentityDatabase(context)
- .setSeen(recipients.getPrimaryRecipient().getRecipientId());
- }
}
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
diff --git a/src/org/thoughtcrime/securesms/service/RegistrationService.java b/src/org/thoughtcrime/securesms/service/RegistrationService.java
index 5309daf444..d1e1ac4c2b 100644
--- a/src/org/thoughtcrime/securesms/service/RegistrationService.java
+++ b/src/org/thoughtcrime/securesms/service/RegistrationService.java
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -260,7 +261,7 @@ public class RegistrationService extends Service {
TextSecurePreferences.setWebsocketRegistered(this, true);
- DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey(), true, System.currentTimeMillis(), true, true);
+ DatabaseFactory.getIdentityDatabase(this).saveIdentity(self.getRecipientId(), identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true);
DirectoryHelper.refreshDirectory(this, accountManager, number);
DirectoryRefreshListener.schedule(this);
diff --git a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
index 7e952ea5db..27db22f284 100644
--- a/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
+++ b/src/org/thoughtcrime/securesms/service/WebRtcCallService.java
@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule.SignalMessageSenderFactory;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -69,7 +68,6 @@ import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoTrack;
import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@@ -339,12 +337,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
return;
}
- if (isUnseenIdentity(this.recipient)) {
- insertMissedCall(this.recipient, true);
- terminate();
- return;
- }
-
timeoutExecutor.schedule(new TimeoutRunnable(this.callId), 2, TimeUnit.MINUTES);
initializeVideo();
@@ -370,6 +362,7 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
@Override
public void onFailureContinue(Throwable error) {
Log.w(TAG, error);
+ insertMissedCall(recipient, true);
terminate();
}
});
@@ -955,28 +948,6 @@ public class WebRtcCallService extends Service implements InjectableType, PeerCo
else return result;
}
- private boolean isUnseenIdentity(@NonNull Recipient recipient) {
- Log.w(TAG, "Checking for unseen identity: " + recipient.getRecipientId());
-
- Optional identityRecord = DatabaseFactory.getIdentityDatabase(this).getIdentity(recipient.getRecipientId());
-
- if (!identityRecord.isPresent()) {
- throw new AssertionError("Should have an identity record at this point.");
- }
-
- if (identityRecord.get().isFirstUse()) {
- Log.w(TAG, "Identity is first use...");
- return false;
- }
-
- Log.w(TAG, "Last seen: " + identityRecord.get().getSeen() + " vs timestamp: " + identityRecord.get().getTimestamp());
- if (identityRecord.get().getSeen() >= identityRecord.get().getTimestamp()) {
- return false;
- }
-
- return true;
- }
-
private long getCallId(Intent intent) {
return intent.getLongExtra(EXTRA_CALL_ID, -1);
}
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingIdentityDefaultMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingIdentityDefaultMessage.java
new file mode 100644
index 0000000000..fc9c75e752
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/sms/IncomingIdentityDefaultMessage.java
@@ -0,0 +1,15 @@
+package org.thoughtcrime.securesms.sms;
+
+
+public class IncomingIdentityDefaultMessage extends IncomingTextMessage {
+
+ public IncomingIdentityDefaultMessage(IncomingTextMessage base) {
+ super(base, "");
+ }
+
+ @Override
+ public boolean isIdentityDefault() {
+ return true;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingIdentityVerifiedMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingIdentityVerifiedMessage.java
new file mode 100644
index 0000000000..23a5d51900
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/sms/IncomingIdentityVerifiedMessage.java
@@ -0,0 +1,15 @@
+package org.thoughtcrime.securesms.sms;
+
+
+public class IncomingIdentityVerifiedMessage extends IncomingTextMessage {
+
+ public IncomingIdentityVerifiedMessage(IncomingTextMessage base) {
+ super(base, "");
+ }
+
+ @Override
+ public boolean isIdentityVerified() {
+ return true;
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
index 2029122e5d..befd029f15 100644
--- a/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/IncomingTextMessage.java
@@ -227,6 +227,14 @@ public class IncomingTextMessage implements Parcelable {
return false;
}
+ public boolean isIdentityVerified() {
+ return false;
+ }
+
+ public boolean isIdentityDefault() {
+ return false;
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingIdentityDefaultMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityDefaultMessage.java
new file mode 100644
index 0000000000..df728de264
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityDefaultMessage.java
@@ -0,0 +1,20 @@
+package org.thoughtcrime.securesms.sms;
+
+
+import org.thoughtcrime.securesms.recipients.Recipients;
+
+public class OutgoingIdentityDefaultMessage extends OutgoingTextMessage {
+
+ public OutgoingIdentityDefaultMessage(Recipients recipients) {
+ super(recipients, "", -1);
+ }
+
+ @Override
+ public boolean isIdentityDefault() {
+ return true;
+ }
+
+ public OutgoingTextMessage withBody(String body) {
+ return new OutgoingIdentityDefaultMessage(getRecipients());
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingIdentityVerifiedMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityVerifiedMessage.java
new file mode 100644
index 0000000000..28683eb4f1
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/sms/OutgoingIdentityVerifiedMessage.java
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.sms;
+
+
+import org.thoughtcrime.securesms.recipients.Recipients;
+
+public class OutgoingIdentityVerifiedMessage extends OutgoingTextMessage {
+
+ public OutgoingIdentityVerifiedMessage(Recipients recipients) {
+ super(recipients, "", -1);
+ }
+
+ @Override
+ public boolean isIdentityVerified() {
+ return true;
+ }
+
+ @Override
+ public OutgoingTextMessage withBody(String body) {
+ return new OutgoingIdentityVerifiedMessage(getRecipients());
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
index 2a90dd0f42..760e1d9710 100644
--- a/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
+++ b/src/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
@@ -60,6 +60,14 @@ public class OutgoingTextMessage {
return false;
}
+ public boolean isIdentityVerified() {
+ return false;
+ }
+
+ public boolean isIdentityDefault() {
+ return false;
+ }
+
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody(), record.getExpiresIn());
diff --git a/src/org/thoughtcrime/securesms/util/IdentityUtil.java b/src/org/thoughtcrime/securesms/util/IdentityUtil.java
index 6dd750281d..69f8863f9c 100644
--- a/src/org/thoughtcrime/securesms/util/IdentityUtil.java
+++ b/src/org/thoughtcrime/securesms/util/IdentityUtil.java
@@ -2,61 +2,59 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.util.Log;
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.crypto.MasterSecretUnion;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
-import org.thoughtcrime.securesms.database.MessagingDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.SmsDatabase;
-import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
+import org.thoughtcrime.securesms.sms.IncomingIdentityDefaultMessage;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
+import org.thoughtcrime.securesms.sms.IncomingIdentityVerifiedMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
+import org.thoughtcrime.securesms.sms.OutgoingIdentityDefaultMessage;
+import org.thoughtcrime.securesms.sms.OutgoingIdentityVerifiedMessage;
+import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
-import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.SignalProtocolAddress;
-import org.whispersystems.libsignal.state.SessionRecord;
-import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
+import java.util.List;
+
+import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
+
public class IdentityUtil {
private static final String TAG = IdentityUtil.class.getSimpleName();
@UiThread
- public static ListenableFuture> getRemoteIdentityKey(final Context context,
- final MasterSecret masterSecret,
- final Recipient recipient)
- {
- final SettableFuture> future = new SettableFuture<>();
+ public static ListenableFuture> getRemoteIdentityKey(final Context context, final Recipient recipient) {
+ final SettableFuture> future = new SettableFuture<>();
- new AsyncTask>() {
+ new AsyncTask>() {
@Override
- protected Optional doInBackground(Recipient... recipient) {
- SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
- SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(recipient[0].getNumber(), SignalServiceAddress.DEFAULT_DEVICE_ID);
- SessionRecord record = sessionStore.loadSession(axolotlAddress);
-
- if (record == null) {
- return Optional.absent();
- }
-
- return Optional.fromNullable(record.getSessionState().getRemoteIdentityKey());
+ protected Optional doInBackground(Recipient... recipient) {
+ return DatabaseFactory.getIdentityDatabase(context)
+ .getIdentity(recipient[0].getRecipientId());
}
@Override
- protected void onPostExecute(Optional result) {
+ protected void onPostExecute(Optional result) {
future.set(result);
}
}.execute(recipient);
@@ -64,6 +62,102 @@ public class IdentityUtil {
return future;
}
+ public static void markIdentityVerified(Context context, MasterSecretUnion masterSecret,
+ Recipient recipient, boolean verified, boolean remote)
+ {
+ long time = System.currentTimeMillis();
+ SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ Recipients recipients = RecipientFactory.getRecipientsFor(context, recipient, true);
+ GroupDatabase.Reader reader = groupDatabase.getGroups();
+
+ String number = recipient.getNumber();
+
+ try {
+ number = Util.canonicalizeNumber(context, number);
+ } catch (InvalidNumberException e) {
+ Log.w(TAG, e);
+ }
+
+ GroupDatabase.GroupRecord groupRecord;
+
+ while ((groupRecord = reader.getNext()) != null) {
+ if (groupRecord.getMembers().contains(number) && groupRecord.isActive()) {
+ SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
+
+ if (remote) {
+ IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.of(group), 0);
+
+ if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
+ else incoming = new IncomingIdentityDefaultMessage(incoming);
+
+ smsDatabase.insertMessageInbox(incoming);
+ } else {
+ Recipients groupRecipients = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(group.getGroupId()), true);
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
+ OutgoingTextMessage outgoing ;
+
+ if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipients);
+ else outgoing = new OutgoingIdentityDefaultMessage(recipients);
+
+ DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageOutbox(masterSecret, threadId, outgoing, false, time, null);
+ }
+ }
+ }
+
+ if (remote) {
+ IncomingTextMessage incoming = new IncomingTextMessage(number, 1, time, null, Optional.absent(), 0);
+
+ if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming);
+ else incoming = new IncomingIdentityDefaultMessage(incoming);
+
+ smsDatabase.insertMessageInbox(incoming);
+ } else {
+ OutgoingTextMessage outgoing;
+
+ if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipients);
+ else outgoing = new OutgoingIdentityDefaultMessage(recipients);
+
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
+
+ Log.w(TAG, "Inserting verified outbox...");
+ DatabaseFactory.getEncryptingSmsDatabase(context)
+ .insertMessageOutbox(masterSecret, threadId, outgoing, false, time, null);
+ }
+ }
+
+ public static void processVerifiedMessage(Context context, MasterSecretUnion masterSecret, VerifiedMessage verifiedMessage) {
+ synchronized (SESSION_LOCK) {
+ IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
+ Recipient recipient = RecipientFactory.getRecipientsFromString(context, verifiedMessage.getDestination(), true).getPrimaryRecipient();
+ Optional identityRecord = identityDatabase.getIdentity(recipient.getRecipientId());
+
+ if (!identityRecord.isPresent() && verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT) {
+ Log.w(TAG, "No existing record for default status");
+ return;
+ }
+
+ if (identityRecord.isPresent() &&
+ identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey()) &&
+ identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.DEFAULT &&
+ verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.DEFAULT)
+ {
+ identityDatabase.setVerified(recipient.getRecipientId(), identityRecord.get().getIdentityKey(), IdentityDatabase.VerifiedStatus.DEFAULT);
+ markIdentityVerified(context, masterSecret, recipient, false, true);
+ }
+
+ if (verifiedMessage.getVerified() == VerifiedMessage.VerifiedState.VERIFIED &&
+ (!identityRecord.isPresent() ||
+ (identityRecord.isPresent() && !identityRecord.get().getIdentityKey().equals(verifiedMessage.getIdentityKey())) ||
+ (identityRecord.isPresent() && identityRecord.get().getVerifiedStatus() != IdentityDatabase.VerifiedStatus.VERIFIED)))
+ {
+ identityDatabase.saveIdentity(recipient.getRecipientId(), verifiedMessage.getIdentityKey(),
+ IdentityDatabase.VerifiedStatus.VERIFIED, false, System.currentTimeMillis(), true);
+ markIdentityVerified(context, masterSecret, recipient, true, true);
+ }
+ }
+ }
+
public static void markIdentityUpdate(Context context, Recipient recipient) {
long time = System.currentTimeMillis();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
@@ -98,4 +192,62 @@ public class IdentityUtil {
MessageNotifier.updateNotification(context, null, insertResult.get().getThreadId());
}
}
+
+ public static @Nullable String getUnverifiedBannerDescription(@NonNull Context context,
+ @NonNull List unverified)
+ {
+ return getPluralizedIdentityDescription(context, unverified,
+ R.string.IdentityUtil_unverified_banner_one,
+ R.string.IdentityUtil_unverified_banner_two,
+ R.string.IdentityUtil_unverified_banner_many);
+ }
+
+ public static @Nullable String getUnverifiedSendDialogDescription(@NonNull Context context,
+ @NonNull List unverified)
+ {
+ return getPluralizedIdentityDescription(context, unverified,
+ R.string.IdentityUtil_unverified_dialog_one,
+ R.string.IdentityUtil_unverified_dialog_two,
+ R.string.IdentityUtil_unverified_dialog_many);
+ }
+
+ public static @Nullable String getUntrustedSendDialogDescription(@NonNull Context context,
+ @NonNull List untrusted)
+ {
+ return getPluralizedIdentityDescription(context, untrusted,
+ R.string.IdentityUtil_untrusted_dialog_one,
+ R.string.IdentityUtil_untrusted_dialog_two,
+ R.string.IdentityUtil_untrusted_dialog_many);
+ }
+
+ private static @Nullable String getPluralizedIdentityDescription(@NonNull Context context,
+ @NonNull List recipients,
+ @StringRes int resourceOne,
+ @StringRes int resourceTwo,
+ @StringRes int resourceMany)
+ {
+ if (recipients.isEmpty()) return null;
+
+ if (recipients.size() == 1) {
+ String name = recipients.get(0).toShortString();
+ return context.getString(resourceOne, name);
+ } else {
+ String firstName = recipients.get(0).toShortString();
+ String secondName = recipients.get(1).toShortString();
+
+ if (recipients.size() == 2) {
+ return context.getString(resourceTwo, firstName, secondName);
+ } else {
+ String nMore;
+
+ if (recipients.size() == 3) {
+ nMore = context.getResources().getQuantityString(R.plurals.identity_others, 1);
+ } else {
+ nMore = context.getResources().getQuantityString(R.plurals.identity_others, recipients.size() - 2);
+ }
+
+ return context.getString(resourceMany, firstName, secondName, nMore);
+ }
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/util/VerifySpan.java b/src/org/thoughtcrime/securesms/util/VerifySpan.java
index 4e750ac9a1..d450f6688d 100644
--- a/src/org/thoughtcrime/securesms/util/VerifySpan.java
+++ b/src/org/thoughtcrime/securesms/util/VerifySpan.java
@@ -32,8 +32,9 @@ public class VerifySpan extends ClickableSpan {
@Override
public void onClick(View widget) {
Intent intent = new Intent(context, VerifyIdentityActivity.class);
- intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID, recipientId);
- intent.putExtra(VerifyIdentityActivity.RECIPIENT_IDENTITY, new IdentityKeyParcelable(identityKey));
+ intent.putExtra(VerifyIdentityActivity.RECIPIENT_ID_EXTRA, recipientId);
+ intent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
+ intent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, false);
context.startActivity(intent);
}
}