diff --git a/build.gradle b/build.gradle
index 6abbfb661d..49a874bf37 100644
--- a/build.gradle
+++ b/build.gradle
@@ -185,8 +185,8 @@ dependencies {
implementation "com.opencsv:opencsv:4.6"
}
-def canonicalVersionCode = 75
-def canonicalVersionName = "1.4.6"
+def canonicalVersionCode = 81
+def canonicalVersionName = "1.4.7"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
diff --git a/res/layout/activity_create_closed_group.xml b/res/layout/activity_create_closed_group.xml
index 3a108b6f4d..6778a17915 100644
--- a/res/layout/activity_create_closed_group.xml
+++ b/res/layout/activity_create_closed_group.xml
@@ -1,6 +1,7 @@
@@ -58,4 +59,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/conversation_item_thumbnail.xml b/res/layout/conversation_item_thumbnail.xml
index 23544e8e68..bff1a90fa8 100644
--- a/res/layout/conversation_item_thumbnail.xml
+++ b/res/layout/conversation_item_thumbnail.xml
@@ -42,7 +42,7 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
- app:footer_text_color="@color/core_white"
- app:footer_icon_color="@color/core_white"/>
+ app:footer_text_color="@android:color/white"
+ app:footer_icon_color="@android:color/white"/>
diff --git a/res/layout/thumbnail_view.xml b/res/layout/thumbnail_view.xml
index a9f979014c..a22164f376 100644
--- a/res/layout/thumbnail_view.xml
+++ b/res/layout/thumbnail_view.xml
@@ -22,6 +22,17 @@
android:src="@drawable/ic_caption_28"
android:visibility="gone" />
+
+
if (position < 0) return -1;
MessageRecord record = getRecordForPositionOrThrow(position);
-
- calendar.setTime(new Date(record.getDateSent()));
+ if (record.getRecipient().getAddress().isOpenGroup()) {
+ calendar.setTime(new Date(record.getDateReceived()));
+ } else {
+ calendar.setTime(new Date(record.getDateSent()));
+ }
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
}
diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
index 9342c63c71..8f6f663fe6 100644
--- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java
@@ -374,7 +374,7 @@ public class AttachmentDatabase extends Database {
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId));
}
- public void updateAttachmentAfterUpload(@NonNull AttachmentId id, @NonNull Attachment attachment) {
+ public void updateAttachmentAfterUploadSucceeded(@NonNull AttachmentId id, @NonNull Attachment attachment) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
@@ -390,6 +390,15 @@ public class AttachmentDatabase extends Database {
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
}
+ public void updateAttachmentAfterUploadFailed(@NonNull AttachmentId id) {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+
+ values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED);
+
+ database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
+ }
+
@NonNull Map insertAttachmentsForMessage(long mmsId, @NonNull List attachments, @NonNull List quoteAttachment)
throws MmsException
{
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index 8948ff6123..97a59c5007 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -972,6 +972,14 @@ public class MmsDatabase extends MessagingDatabase {
public long insertMessageOutbox(@NonNull OutgoingMediaMessage message,
long threadId, boolean forceSms,
@Nullable SmsDatabase.InsertListener insertListener)
+ throws MmsException {
+ return insertMessageOutbox(message, threadId, forceSms, insertListener, 0);
+ }
+
+ public long insertMessageOutbox(@NonNull OutgoingMediaMessage message,
+ long threadId, boolean forceSms,
+ @Nullable SmsDatabase.InsertListener insertListener,
+ long serverTimestamp)
throws MmsException
{
long type = Types.BASE_SENDING_TYPE;
@@ -998,7 +1006,10 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(MESSAGE_BOX, type);
contentValues.put(THREAD_ID, threadId);
contentValues.put(READ, 1);
- contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
+ // In open groups messages should be sorted by their server timestamp
+ long receivedTimestamp = serverTimestamp;
+ if (serverTimestamp == 0) { receivedTimestamp = System.currentTimeMillis(); }
+ contentValues.put(DATE_RECEIVED, receivedTimestamp);
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
contentValues.put(EXPIRES_IN, message.getExpiresIn());
contentValues.put(ADDRESS, message.getRecipient().getAddress().serialize());
diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
index effea38c60..7e8fe34985 100644
--- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -136,10 +136,10 @@ public abstract class MessageRecord extends DisplayRecord {
}
public long getTimestamp() {
- if (isPush() && getDateSent() < getDateReceived()) {
- return getDateSent();
- }
if (getRecipient().getAddress().isOpenGroup()) {
+ return getDateReceived();
+ }
+ if (isPush() && getDateSent() < getDateReceived()) {
return getDateSent();
}
return getDateReceived();
diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
index 72a83a47a7..94989c231b 100644
--- a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
@@ -88,13 +88,20 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
// Only upload attachment if necessary
if (databaseAttachment.getUrl().isEmpty()) {
- MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
- Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
- SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
- SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker(), new SignalServiceAddress(destination.serialize()));
- Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
+ final Attachment attachment;
+ try {
+ MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
+ Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
+ SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
+ SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker(), new SignalServiceAddress(destination.serialize()));
+ attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
+ } catch (Exception e) {
+ // On any error make sure we mark the related DB record's transfer state as failed.
+ database.updateAttachmentAfterUploadFailed(databaseAttachment.getAttachmentId());
+ throw e;
+ }
- database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
+ database.updateAttachmentAfterUploadSucceeded(databaseAttachment.getAttachmentId(), attachment);
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 28ec14dfdf..4b97762c9b 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -1021,10 +1021,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long messageId;
if (isGroup) {
- OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
+ OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getMessage().getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
- messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null);
+ messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null,message.getTimestamp());
if (message.messageServerID >= 0) { DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageId, message.messageServerID); }
database = DatabaseFactory.getMmsDatabase(context);
diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt
index 306173446e..30d59281d6 100644
--- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt
@@ -15,6 +15,7 @@ import android.widget.Toast
import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import network.loki.messenger.R
+import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.database.Address
@@ -22,6 +23,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
+import org.thoughtcrime.securesms.loki.utilities.fadeIn
+import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -118,13 +121,19 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
if (selectedMembers.count() < 1) {
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
- if (selectedMembers.count() > ClosedGroupsProtocol.groupSizeLimit) { // Minus one because we're going to include self later
+ if (selectedMembers.count() >= ClosedGroupsProtocol.groupSizeLimit) { // Minus one because we're going to include self later
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
- val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey ))
- val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
- openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
+ loader.fadeIn()
+ ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
+ loader.fadeOut()
+ val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
+ if (!isFinishing) {
+ openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
+ finish()
+ }
+ }
}
private fun createLegacyClosedGroup() {
diff --git a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt
index 03762c60bd..c513d08993 100644
--- a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt
@@ -228,14 +228,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else Companion.legacyGroupSizeLimit
- if (members.size > maxGroupMembers) {
+ if (members.size >= maxGroupMembers) {
// TODO: Update copy for SSK based closed groups
return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
if (isSSKBasedClosedGroup) {
- ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() },
- name, admins.map { it.address.serialize() })
+ ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
} else {
GroupManager.updateGroup(this, groupID, members, null, name, admins)
}
diff --git a/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt
index 7d8587c194..e021576840 100644
--- a/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt
@@ -97,7 +97,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// linkedDevicesButton.setOnClickListener { showLinkedDevices() }
seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() }
- versionTextView.text = String.format(getString(R.string.version_s), BuildConfig.VERSION_NAME)
+ versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt
index 9cd363823e..e4eb12df94 100644
--- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt
+++ b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt
@@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit
class BackgroundPollWorker : PersistentAlarmManagerListener() {
companion object {
- private val pollInterval = TimeUnit.MINUTES.toMillis(20)
+ private val pollInterval = TimeUnit.MINUTES.toMillis(15)
@JvmStatic
fun schedule(context: Context) {
diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt
index 28aa5f93f3..6a5ee5dad0 100644
--- a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt
+++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt
@@ -192,7 +192,7 @@ class PublicChatPoller(private val context: Context, private val group: PublicCh
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val dataMessage = getDataMessage(message)
SessionMetaProtocol.dropFromTimestampCacheIfNeeded(dataMessage.timestamp)
- val transcript = SentTranscriptMessage(userHexEncodedPublicKey, dataMessage.timestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(userHexEncodedPublicKey, false))
+ val transcript = SentTranscriptMessage(userHexEncodedPublicKey, message.serverTimestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(userHexEncodedPublicKey, false))
transcript.messageServerID = messageServerID
if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) {
PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript)
diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt
index d41ef58791..18fae93b87 100644
--- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt
@@ -137,7 +137,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
return listOf( listOf( path0Snode0, path0Snode1, path0Snode2 ), listOf( path1Snode0, path1Snode1, path1Snode2 ) )
}
- fun clearOnionRequestPaths() {
+ override fun clearOnionRequestPaths() {
val database = databaseHelper.writableDatabase
fun delete(indexPath: String) {
database.delete(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath))
diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt
index 23afeeaca0..15b415e8e0 100644
--- a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt
@@ -63,8 +63,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) :
override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set {
val database = databaseHelper.readableDatabase
- val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?"
- return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor ->
+ val query = "${Companion.closedGroupPublicKey} = ?"
+ return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey )) { cursor ->
val chainKey = cursor.getString(Companion.chainKey)
val keyIndex = cursor.getInt(Companion.keyIndex)
val senderPublicKey = cursor.getString(Companion.senderPublicKey)
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
index 3df6187a7b..0841aaf2a6 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
@@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
-import android.graphics.Bitmap
import android.util.Log
import com.google.protobuf.ByteString
+import nl.komponents.kovenant.Promise
+import nl.komponents.kovenant.deferred
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
@@ -35,55 +36,78 @@ object ClosedGroupsProtocol {
val isSharedSenderKeysEnabled = false
val groupSizeLimit = 10
- public fun createClosedGroup(context: Context, name: String, members: Collection): String {
- // Prepare
- val userPublicKey = TextSecurePreferences.getLocalNumber(context)
- // Generate a key pair for the group
- val groupKeyPair = Curve.generateKeyPair()
- val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
- val membersAsData = members.map { Hex.fromStringCondensed(it) }
- // Create ratchets for all members
- val senderKeys: List = members.map { publicKey ->
- val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
- ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
- }
- // Create the group
- val groupID = doubleEncodeGroupID(groupPublicKey)
- val admins = setOf( userPublicKey )
- DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
- null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
- DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
- // Establish sessions if needed
- establishSessionsWithMembersIfNeeded(context, members)
- // Send a closed group update message to all members using established channels
- val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
- senderKeys, membersAsData, adminsAsData)
- for (member in members) {
- val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- }
- // TODO: Wait for the messages to finish sending
- // Add the group to the user's set of public keys to poll for
- DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
- // Notify the user
- val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
- insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
+ public fun createClosedGroup(context: Context, name: String, members: Collection): Promise {
+ val deferred = deferred()
+ Thread {
+ // Prepare
+ val userPublicKey = TextSecurePreferences.getLocalNumber(context)
+ // Generate a key pair for the group
+ val groupKeyPair = Curve.generateKeyPair()
+ val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
+ val membersAsData = members.map { Hex.fromStringCondensed(it) }
+ // Create ratchets for all members
+ val senderKeys: List = members.map { publicKey ->
+ val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
+ ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
+ }
+ // Create the group
+ val groupID = doubleEncodeGroupID(groupPublicKey)
+ val admins = setOf( userPublicKey )
+ DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
+ null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
+ DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
+ // Establish sessions if needed
+ establishSessionsWithMembersIfNeeded(context, members)
+ // Send a closed group update message to all members using established channels
+ val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
+ senderKeys, membersAsData, adminsAsData)
+ for (member in members) {
+ if (member == userPublicKey) { continue }
+ val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
+ job.setContext(context)
+ job.onRun() // Run the job immediately
+ }
+ // Add the group to the user's set of public keys to poll for
+ DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
+ // Notify the user
+ val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
+ insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
+ // Fulfill the promise
+ deferred.resolve(groupID)
+ }.start()
// Return
- return groupID
+ return deferred.promise
}
- public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) {
- // Prepare
+ @JvmStatic
+ public fun leave(context: Context, groupPublicKey: String) {
+ val userPublicKey = TextSecurePreferences.getLocalNumber(context)
+ val groupDB = DatabaseFactory.getGroupDatabase(context)
+ val groupID = doubleEncodeGroupID(groupPublicKey)
+ val group = groupDB.getGroup(groupID).orNull()
+ if (group == null) {
+ Log.d("Loki", "Can't leave nonexistent closed group.")
+ return
+ }
+ val name = group.title
+ val oldMembers = group.members.map { it.serialize() }.toSet()
+ val newMembers = oldMembers.minus(userPublicKey)
+ update(context, groupPublicKey, newMembers, name)
+ }
+
+ public fun update(context: Context, groupPublicKey: String, members: Collection, name: String) {
+ val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
- Log.d("Loki", "Can't add users to nonexistent closed group.")
+ Log.d("Loki", "Can't update nonexistent closed group.")
return
}
- val name = group.title
+ val oldMembers = group.members.map { it.serialize() }.toSet()
+ val membersAsData = members.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
@@ -91,114 +115,74 @@ object ClosedGroupsProtocol {
Log.d("Loki", "Couldn't get private key for closed group.")
return
}
- // Add the members to the member list
- val members = group.members.map { it.serialize() }.toMutableSet()
- members.addAll(newMembers)
- val membersAsData = members.map { Hex.fromStringCondensed(it) }
- // Generate ratchets for the new members
- val senderKeys: List = newMembers.map { publicKey ->
- val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
- ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
- }
- // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
- senderKeys, membersAsData, adminsAsData)
- val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- // Establish sessions if needed
- establishSessionsWithMembersIfNeeded(context, newMembers)
- // Send closed group update messages to the new members using established channels
- val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys
- for (member in members) {
- @Suppress("NAME_SHADOWING")
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
- Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
- @Suppress("NAME_SHADOWING")
- val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- }
- // Update the group
- groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
- // Notify the user
- val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
- insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
- }
-
- @JvmStatic
- public fun leave(context: Context, groupPublicKey: String) {
- val userPublicKey = TextSecurePreferences.getLocalNumber(context)
- removeMembers(context, setOf( userPublicKey ), groupPublicKey)
- }
-
- public fun removeMembers(context: Context, membersToRemove: Collection, groupPublicKey: String) {
- val userPublicKey = TextSecurePreferences.getLocalNumber(context)
- val sskDatabase = DatabaseFactory.getSSKDatabase(context)
- val isUserLeaving = membersToRemove.contains(userPublicKey)
- if (isUserLeaving && membersToRemove.count() != 1) {
- Log.d("Loki", "Can't remove self and others simultaneously.")
- return
- }
- val groupDB = DatabaseFactory.getGroupDatabase(context)
- val groupID = doubleEncodeGroupID(groupPublicKey)
- val group = groupDB.getGroup(groupID).orNull()
- if (group == null) {
- Log.d("Loki", "Can't add users to nonexistent closed group.")
- return
- }
- val name = group.title
- val admins = group.admins.map { it.serialize() }
- val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
- // Remove the members from the member list
- val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove)
- val membersAsData = members.map { Hex.fromStringCondensed(it) }
- // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
- name, setOf(), membersAsData, adminsAsData)
- val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
- job.setContext(context)
- job.onRun() // Run the job immediately
- // Delete all ratchets (it's important that this happens after sending out the update)
- sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
- // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
- // send it out to all members (minus the removed ones) using established channels.
- if (isUserLeaving) {
- sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
- groupDB.setActive(groupID, false)
+ val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
+ val removedMembers = oldMembers.minus(members)
+ val isUserLeaving = removedMembers.contains(userPublicKey)
+ if (wasAnyUserRemoved) {
+ if (isUserLeaving && removedMembers.count() != 1) {
+ Log.d("Loki", "Can't remove self and others simultaneously.")
+ return
+ }
+ // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
+ name, setOf(), membersAsData, adminsAsData)
+ val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
+ job.setContext(context)
+ job.onRun() // Run the job immediately
+ // Delete all ratchets (it's important that this happens * after * sending out the update)
+ sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
+ // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
+ // send it out to all members (minus the removed ones) using established channels.
+ if (isUserLeaving) {
+ sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
+ groupDB.setActive(groupID, false)
+ groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
+ } else {
+ // Establish sessions if needed
+ establishSessionsWithMembersIfNeeded(context, members)
+ // Send out the user's new ratchet to all members (minus the removed ones) using established channels
+ val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
+ val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
+ for (member in members) {
+ if (member == userPublicKey) { continue }
+ @Suppress("NAME_SHADOWING")
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
+ @Suppress("NAME_SHADOWING")
+ val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
+ ApplicationContext.getInstance(context).jobManager.add(job)
+ }
+ }
} else {
+ // Generate ratchets for any new members
+ val newMembers = members.minus(oldMembers)
+ val newSenderKeys: List = newMembers.map { publicKey ->
+ val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
+ ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
+ }
+ // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
+ newSenderKeys, membersAsData, adminsAsData)
+ val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
+ ApplicationContext.getInstance(context).jobManager.add(job)
// Establish sessions if needed
- establishSessionsWithMembersIfNeeded(context, members)
- // Send out the user's new ratchet to all members (minus the removed ones) using established channels
- val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
- val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
- for (member in members) {
+ establishSessionsWithMembersIfNeeded(context, newMembers)
+ // Send closed group update messages to the new members using established channels
+ val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + newSenderKeys
+ for (member in newMembers) {
@Suppress("NAME_SHADOWING")
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
+ Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
// Update the group
- groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
- // Notify the user
- val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
- insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID)
- }
-
- public fun update(context: Context, groupPublicKey: String, members: Collection, name: String, admins: Collection) {
- val groupDB = DatabaseFactory.getGroupDatabase(context)
- val groupID = doubleEncodeGroupID(groupPublicKey)
- if (groupDB.getGroup(groupID).orNull() == null) {
- Log.d("Loki", "Can't update nonexistent closed group.")
- return
+ groupDB.updateTitle(groupID, name)
+ if (!isUserLeaving) {
+ // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
+ groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
}
- // Send the update to the group
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
- name, setOf(), members.map { Hex.fromStringCondensed(it) }, admins.map { Hex.fromStringCondensed(it) })
- val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- // Update the group
- groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
// Notify the user
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
@@ -206,6 +190,7 @@ object ClosedGroupsProtocol {
@JvmStatic
public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) {
+ Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.")
// Establish session if needed
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
// Send the request
@@ -317,11 +302,13 @@ object ClosedGroupsProtocol {
if (wasCurrentUserRemoved) {
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
groupDB.setActive(groupID, false)
+ groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
} else {
establishSessionsWithMembersIfNeeded(context, members)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) {
+ if (member == userPublicKey) { continue }
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
@@ -330,7 +317,10 @@ object ClosedGroupsProtocol {
}
// Update the group
groupDB.updateTitle(groupID, name)
- groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
+ if (!wasCurrentUserRemoved) {
+ // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
+ groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
+ }
// Notify the user
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
@@ -354,6 +344,7 @@ object ClosedGroupsProtocol {
return
}
// Respond to the request
+ Log.d("Loki", "Responding to sender key request from: $senderPublicKey.")
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
@@ -389,6 +380,7 @@ object ClosedGroupsProtocol {
return
}
// Store the sender key
+ Log.d("Loki", "Received a sender key from: $senderPublicKey.")
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet)
}