Merge branch 'dev' into light-theme

# Conflicts:
#	res/layout/activity_create_closed_group.xml
This commit is contained in:
Anton Chekulaev 2020-09-02 13:33:16 +10:00
commit 2e31900957
21 changed files with 259 additions and 192 deletions

View File

@ -185,8 +185,8 @@ dependencies {
implementation "com.opencsv:opencsv:4.6" implementation "com.opencsv:opencsv:4.6"
} }
def canonicalVersionCode = 75 def canonicalVersionCode = 81
def canonicalVersionName = "1.4.6" def canonicalVersionName = "1.4.7"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -58,4 +59,22 @@
</LinearLayout> </LinearLayout>
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:visibility="gone"
android:alpha="0">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_centerInParent="true"
app:SpinKit_Color="?android:colorControlNormal" />
</RelativeLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -42,7 +42,7 @@
android:layout_marginStart="@dimen/message_bubble_horizontal_padding" android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding" android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding" android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
app:footer_text_color="@color/core_white" app:footer_text_color="@android:color/white"
app:footer_icon_color="@color/core_white"/> app:footer_icon_color="@android:color/white"/>
</merge> </merge>

View File

@ -22,6 +22,17 @@
android:src="@drawable/ic_caption_28" android:src="@drawable/ic_caption_28"
android:visibility="gone" /> android:visibility="gone" />
<ProgressBar
android:id="@+id/thumbnail_load_indicator"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
android:indeterminateTint="@android:color/white"
android:indeterminateTintMode="src_in"
tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/play_overlay" android:id="@+id/play_overlay"
android:layout_width="48dp" android:layout_width="48dp"

View File

@ -73,7 +73,7 @@ public class AlbumThumbnailView extends FrameLayout {
} }
} }
int sizeClass = sizeClass(slides.size()); int sizeClass = Math.min(slides.size(), 6);
if (sizeClass != currentSizeClass) { if (sizeClass != currentSizeClass) {
inflateLayout(sizeClass); inflateLayout(sizeClass);
@ -148,11 +148,8 @@ public class AlbumThumbnailView extends FrameLayout {
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) { private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
ThumbnailView cell = findViewById(id); ThumbnailView cell = findViewById(id);
cell.setImageResource(glideRequests, slide, false, false); cell.setImageResource(glideRequests, slide, false, false);
cell.setLoadIndicatorVisibile(slide.isInProgress());
cell.setThumbnailClickListener(defaultThumbnailClickListener); cell.setThumbnailClickListener(defaultThumbnailClickListener);
cell.setOnLongClickListener(defaultLongClickListener); cell.setOnLongClickListener(defaultLongClickListener);
} }
private int sizeClass(int size) {
return Math.min(size, 6);
}
} }

View File

@ -3,15 +3,15 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Canvas; import android.graphics.Canvas;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import network.loki.messenger.R; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.List; import java.util.List;
import network.loki.messenger.R;
public class ConversationItemThumbnail extends FrameLayout { public class ConversationItemThumbnail extends FrameLayout {
private ThumbnailView thumbnail; private ThumbnailView thumbnail;
@ -49,12 +51,12 @@ public class ConversationItemThumbnail extends FrameLayout {
private void init(@Nullable AttributeSet attrs) { private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.conversation_item_thumbnail, this); inflate(getContext(), R.layout.conversation_item_thumbnail, this);
this.thumbnail = findViewById(R.id.conversation_thumbnail_image); this.thumbnail = findViewById(R.id.conversation_thumbnail_image);
this.album = findViewById(R.id.conversation_thumbnail_album); this.album = findViewById(R.id.conversation_thumbnail_album);
this.shade = findViewById(R.id.conversation_thumbnail_shade); this.shade = findViewById(R.id.conversation_thumbnail_shade);
this.footer = findViewById(R.id.conversation_thumbnail_footer); this.footer = findViewById(R.id.conversation_thumbnail_footer);
this.cornerMask = new CornerMask(this); this.cornerMask = new CornerMask(this);
this.outliner = new Outliner(); this.outliner = new Outliner();
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
@ -126,8 +128,10 @@ public class ConversationItemThumbnail extends FrameLayout {
thumbnail.setVisibility(VISIBLE); thumbnail.setVisibility(VISIBLE);
album.setVisibility(GONE); album.setVisibility(GONE);
Attachment attachment = slides.get(0).asAttachment(); Slide slide = slides.get(0);
thumbnail.setImageResource(glideRequests, slides.get(0), showControls, isPreview, attachment.getWidth(), attachment.getHeight()); Attachment attachment = slide.asAttachment();
thumbnail.setImageResource(glideRequests, slide, showControls, isPreview, attachment.getWidth(), attachment.getHeight());
thumbnail.setLoadIndicatorVisibile(slide.isInProgress());
setTouchDelegate(thumbnail.getTouchDelegate()); setTouchDelegate(thumbnail.getTouchDelegate());
} else { } else {
thumbnail.setVisibility(GONE); thumbnail.setVisibility(GONE);

View File

@ -52,6 +52,7 @@ public class ThumbnailView extends FrameLayout {
private ImageView image; private ImageView image;
private View playOverlay; private View playOverlay;
private View captionIcon; private View captionIcon;
private View loadIndicator;
private OnClickListener parentClickListener; private OnClickListener parentClickListener;
private final int[] dimens = new int[2]; private final int[] dimens = new int[2];
@ -78,9 +79,10 @@ public class ThumbnailView extends FrameLayout {
inflate(context, R.layout.thumbnail_view, this); inflate(context, R.layout.thumbnail_view, this);
this.image = findViewById(R.id.thumbnail_image); this.image = findViewById(R.id.thumbnail_image);
this.playOverlay = findViewById(R.id.play_overlay); this.playOverlay = findViewById(R.id.play_overlay);
this.captionIcon = findViewById(R.id.thumbnail_caption_icon); this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
super.setOnClickListener(new ThumbnailClickDispatcher()); super.setOnClickListener(new ThumbnailClickDispatcher());
if (attrs != null) { if (attrs != null) {
@ -336,6 +338,10 @@ public class ThumbnailView extends FrameLayout {
getTransferControls().showProgressSpinner(); getTransferControls().showProgressSpinner();
} }
public void setLoadIndicatorVisibile(boolean visible) {
this.loadIndicator.setVisibility(visible ? VISIBLE : GONE);
}
protected void setRadius(int radius) { protected void setRadius(int radius) {
this.radius = radius; this.radius = radius;
} }

View File

@ -396,8 +396,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
if (position < 0) return -1; if (position < 0) return -1;
MessageRecord record = getRecordForPositionOrThrow(position); MessageRecord record = getRecordForPositionOrThrow(position);
if (record.getRecipient().getAddress().isOpenGroup()) {
calendar.setTime(new Date(record.getDateSent())); 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)); return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
} }

View File

@ -374,7 +374,7 @@ public class AttachmentDatabase extends Database {
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId)); 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(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
@ -390,6 +390,15 @@ public class AttachmentDatabase extends Database {
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); 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<Attachment, AttachmentId> insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment) @NonNull Map<Attachment, AttachmentId> insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
throws MmsException throws MmsException
{ {

View File

@ -972,6 +972,14 @@ public class MmsDatabase extends MessagingDatabase {
public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, public long insertMessageOutbox(@NonNull OutgoingMediaMessage message,
long threadId, boolean forceSms, long threadId, boolean forceSms,
@Nullable SmsDatabase.InsertListener insertListener) @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 throws MmsException
{ {
long type = Types.BASE_SENDING_TYPE; long type = Types.BASE_SENDING_TYPE;
@ -998,7 +1006,10 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(MESSAGE_BOX, type); contentValues.put(MESSAGE_BOX, type);
contentValues.put(THREAD_ID, threadId); contentValues.put(THREAD_ID, threadId);
contentValues.put(READ, 1); 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(SUBSCRIPTION_ID, message.getSubscriptionId());
contentValues.put(EXPIRES_IN, message.getExpiresIn()); contentValues.put(EXPIRES_IN, message.getExpiresIn());
contentValues.put(ADDRESS, message.getRecipient().getAddress().serialize()); contentValues.put(ADDRESS, message.getRecipient().getAddress().serialize());

View File

@ -136,10 +136,10 @@ public abstract class MessageRecord extends DisplayRecord {
} }
public long getTimestamp() { public long getTimestamp() {
if (isPush() && getDateSent() < getDateReceived()) {
return getDateSent();
}
if (getRecipient().getAddress().isOpenGroup()) { if (getRecipient().getAddress().isOpenGroup()) {
return getDateReceived();
}
if (isPush() && getDateSent() < getDateReceived()) {
return getDateSent(); return getDateSent();
} }
return getDateReceived(); return getDateReceived();

View File

@ -88,13 +88,20 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
// Only upload attachment if necessary // Only upload attachment if necessary
if (databaseAttachment.getUrl().isEmpty()) { if (databaseAttachment.getUrl().isEmpty()) {
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); final Attachment attachment;
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment); try {
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment); MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker(), new SignalServiceAddress(destination.serialize())); Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get(); 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);
} }
} }

View File

@ -1021,10 +1021,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long messageId; long messageId;
if (isGroup) { 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); 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); } if (message.messageServerID >= 0) { DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageId, message.messageServerID); }
database = DatabaseFactory.getMmsDatabase(context); database = DatabaseFactory.getMmsDatabase(context);

View File

@ -15,6 +15,7 @@ import android.widget.Toast
import kotlinx.android.synthetic.main.activity_create_closed_group.* import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.database.Address 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.database.ThreadDatabase
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol 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.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -118,13 +121,19 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
if (selectedMembers.count() < 1) { if (selectedMembers.count() < 1) {
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() 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() 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 userPublicKey = TextSecurePreferences.getLocalNumber(this)
val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) loader.fadeIn()
val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) 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() { private fun createLegacyClosedGroup() {

View File

@ -228,14 +228,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else Companion.legacyGroupSizeLimit 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 // 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() return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
} }
if (isSSKBasedClosedGroup) { if (isSSKBasedClosedGroup) {
ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
name, admins.map { it.address.serialize() })
} else { } else {
GroupManager.updateGroup(this, groupID, members, null, name, admins) GroupManager.updateGroup(this, groupID, members, null, name, admins)
} }

View File

@ -97,7 +97,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// linkedDevicesButton.setOnClickListener { showLinkedDevices() } // linkedDevicesButton.setOnClickListener { showLinkedDevices() }
seedButton.setOnClickListener { showSeed() } seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() } 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit
class BackgroundPollWorker : PersistentAlarmManagerListener() { class BackgroundPollWorker : PersistentAlarmManagerListener() {
companion object { companion object {
private val pollInterval = TimeUnit.MINUTES.toMillis(20) private val pollInterval = TimeUnit.MINUTES.toMillis(15)
@JvmStatic @JvmStatic
fun schedule(context: Context) { fun schedule(context: Context) {

View File

@ -192,7 +192,7 @@ class PublicChatPoller(private val context: Context, private val group: PublicCh
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val dataMessage = getDataMessage(message) val dataMessage = getDataMessage(message)
SessionMetaProtocol.dropFromTimestampCacheIfNeeded(dataMessage.timestamp) 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 transcript.messageServerID = messageServerID
if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) { if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) {
PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript) PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript)

View File

@ -137,7 +137,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
return listOf( listOf( path0Snode0, path0Snode1, path0Snode2 ), listOf( path1Snode0, path1Snode1, path1Snode2 ) ) return listOf( listOf( path0Snode0, path0Snode1, path0Snode2 ), listOf( path1Snode0, path1Snode1, path1Snode2 ) )
} }
fun clearOnionRequestPaths() { override fun clearOnionRequestPaths() {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
fun delete(indexPath: String) { fun delete(indexPath: String) {
database.delete(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) database.delete(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath))

View File

@ -63,8 +63,8 @@ class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) :
override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set<ClosedGroupSenderKey> { override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set<ClosedGroupSenderKey> {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" val query = "${Companion.closedGroupPublicKey} = ?"
return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey )) { cursor ->
val chainKey = cursor.getString(Companion.chainKey) val chainKey = cursor.getString(Companion.chainKey)
val keyIndex = cursor.getInt(Companion.keyIndex) val keyIndex = cursor.getInt(Companion.keyIndex)
val senderPublicKey = cursor.getString(Companion.senderPublicKey) val senderPublicKey = cursor.getString(Companion.senderPublicKey)

View File

@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.loki.protocol package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.util.Log import android.util.Log
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -35,55 +36,78 @@ object ClosedGroupsProtocol {
val isSharedSenderKeysEnabled = false val isSharedSenderKeysEnabled = false
val groupSizeLimit = 10 val groupSizeLimit = 10
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): String { public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
// Prepare val deferred = deferred<String, Exception>()
val userPublicKey = TextSecurePreferences.getLocalNumber(context) Thread {
// Generate a key pair for the group // Prepare
val groupKeyPair = Curve.generateKeyPair() val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix // Generate a key pair for the group
val membersAsData = members.map { Hex.fromStringCondensed(it) } val groupKeyPair = Curve.generateKeyPair()
// Create ratchets for all members val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey -> val membersAsData = members.map { Hex.fromStringCondensed(it) }
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) // Create ratchets for all members
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey ->
} val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
// Create the group ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
val groupID = doubleEncodeGroupID(groupPublicKey) }
val admins = setOf( userPublicKey ) // Create the group
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }), val groupID = doubleEncodeGroupID(groupPublicKey)
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) })) val admins = setOf( userPublicKey )
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
// Establish sessions if needed null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
establishSessionsWithMembersIfNeeded(context, members) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Send a closed group update message to all members using established channels // Establish sessions if needed
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } establishSessionsWithMembersIfNeeded(context, members)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), // Send a closed group update message to all members using established channels
senderKeys, membersAsData, adminsAsData) val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
for (member in members) { val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) senderKeys, membersAsData, adminsAsData)
ApplicationContext.getInstance(context).jobManager.add(job) for (member in members) {
} if (member == userPublicKey) { continue }
// TODO: Wait for the messages to finish sending val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
// Add the group to the user's set of public keys to poll for job.setContext(context)
DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) job.onRun() // Run the job immediately
// Notify the user }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) // Add the group to the user's set of public keys to poll for
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) 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
return groupID return deferred.promise
} }
public fun addMembers(context: Context, newMembers: Collection<String>, groupPublicKey: String) { @JvmStatic
// Prepare 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<String>, name: String) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val sskDatabase = DatabaseFactory.getSSKDatabase(context) val sskDatabase = DatabaseFactory.getSSKDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null) {
Log.d("Loki", "Can't add users to nonexistent closed group.") Log.d("Loki", "Can't update nonexistent closed group.")
return 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 admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
@ -91,114 +115,74 @@ object ClosedGroupsProtocol {
Log.d("Loki", "Couldn't get private key for closed group.") Log.d("Loki", "Couldn't get private key for closed group.")
return return
} }
// Add the members to the member list val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
val members = group.members.map { it.serialize() }.toMutableSet() val removedMembers = oldMembers.minus(members)
members.addAll(newMembers) val isUserLeaving = removedMembers.contains(userPublicKey)
val membersAsData = members.map { Hex.fromStringCondensed(it) } if (wasAnyUserRemoved) {
// Generate ratchets for the new members if (isUserLeaving && removedMembers.count() != 1) {
val senderKeys: List<ClosedGroupSenderKey> = newMembers.map { publicKey -> Log.d("Loki", "Can't remove self and others simultaneously.")
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) return
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) }
} // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
// 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),
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, name, setOf(), membersAsData, adminsAsData)
senderKeys, membersAsData, adminsAsData) val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) job.setContext(context)
ApplicationContext.getInstance(context).jobManager.add(job) job.onRun() // Run the job immediately
// Establish sessions if needed // Delete all ratchets (it's important that this happens * after * sending out the update)
establishSessionsWithMembersIfNeeded(context, newMembers) sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
// Send closed group update messages to the new members using established channels // 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
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys // send it out to all members (minus the removed ones) using established channels.
for (member in members) { if (isUserLeaving) {
@Suppress("NAME_SHADOWING") sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupDB.setActive(groupID, false)
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
@Suppress("NAME_SHADOWING") } else {
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) // Establish sessions if needed
ApplicationContext.getInstance(context).jobManager.add(job) establishSessionsWithMembersIfNeeded(context, members)
} // Send out the user's new ratchet to all members (minus the removed ones) using established channels
// Update the group val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
// Notify the user for (member in members) {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) if (member == userPublicKey) { continue }
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) @Suppress("NAME_SHADOWING")
} val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
@Suppress("NAME_SHADOWING")
@JvmStatic val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
public fun leave(context: Context, groupPublicKey: String) { ApplicationContext.getInstance(context).jobManager.add(job)
val userPublicKey = TextSecurePreferences.getLocalNumber(context) }
removeMembers(context, setOf( userPublicKey ), groupPublicKey) }
}
public fun removeMembers(context: Context, membersToRemove: Collection<String>, 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)
} else { } else {
// Generate ratchets for any new members
val newMembers = members.minus(oldMembers)
val newSenderKeys: List<ClosedGroupSenderKey> = 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 // Establish sessions if needed
establishSessionsWithMembersIfNeeded(context, members) establishSessionsWithMembersIfNeeded(context, newMembers)
// Send out the user's new ratchet to all members (minus the removed ones) using established channels // Send closed group update messages to the new members using established channels
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + newSenderKeys
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) for (member in newMembers) {
for (member in members) {
@Suppress("NAME_SHADOWING") @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") @Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
} }
} }
// Update the group // Update the group
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) groupDB.updateTitle(groupID, name)
// Notify the user if (!isUserLeaving) {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
}
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String, admins: Collection<String>) {
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
} }
// 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 // Notify the user
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
@ -206,6 +190,7 @@ object ClosedGroupsProtocol {
@JvmStatic @JvmStatic
public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { 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 // Establish session if needed
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
// Send the request // Send the request
@ -317,11 +302,13 @@ object ClosedGroupsProtocol {
if (wasCurrentUserRemoved) { if (wasCurrentUserRemoved) {
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
groupDB.setActive(groupID, false) groupDB.setActive(groupID, false)
groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
} else { } else {
establishSessionsWithMembersIfNeeded(context, members) establishSessionsWithMembersIfNeeded(context, members)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) { for (member in members) {
if (member == userPublicKey) { continue }
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
@ -330,7 +317,10 @@ object ClosedGroupsProtocol {
} }
// Update the group // Update the group
groupDB.updateTitle(groupID, name) 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 // Notify the user
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
@ -354,6 +344,7 @@ object ClosedGroupsProtocol {
return return
} }
// Respond to the request // Respond to the request
Log.d("Loki", "Responding to sender key request from: $senderPublicKey.")
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
@ -389,6 +380,7 @@ object ClosedGroupsProtocol {
return return
} }
// Store the sender key // Store the sender key
Log.d("Loki", "Received a sender key from: $senderPublicKey.")
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet) sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet)
} }