mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-19 22:08:27 +00:00
Merge pull request #469 from RyanRory/refactor-sending
The Refactor: Message Sending Pipeline
This commit is contained in:
commit
f5e002ece1
@ -32,9 +32,10 @@ import androidx.multidex.MultiDexApplication;
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.session.libsession.messaging.MessagingConfiguration;
|
||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||
import org.session.libsession.snode.SnodeConfiguration;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.SSKEnvironment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
@ -54,7 +55,7 @@ import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
@ -175,6 +176,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
DatabaseFactory.getStorage(this),
|
||||
DatabaseFactory.getAttachmentProvider(this),
|
||||
new SessionProtocolImpl(this));
|
||||
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
|
||||
if (userPublicKey != null) {
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
@ -441,6 +443,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,11 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.LinkPreview;
|
||||
import org.session.libsession.messaging.messages.visible.Quote;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
||||
import org.session.libsession.utilities.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
@ -50,12 +54,12 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
import org.session.libsession.utilities.Util;
|
||||
@ -441,7 +445,28 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void onResendClicked(View v) {
|
||||
MessageSender.resend(MessageDetailsActivity.this, messageRecord);
|
||||
Recipient recipient = messageRecord.getRecipient();
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setId(messageRecord.getId());
|
||||
message.setText(messageRecord.getBody());
|
||||
message.setSentTimestamp(messageRecord.getTimestamp());
|
||||
if (recipient.isGroupRecipient()) {
|
||||
message.setGroupPublicKey(recipient.getAddress().toGroupString());
|
||||
} else {
|
||||
message.setRecipient(messageRecord.getRecipient().getAddress().serialize());
|
||||
}
|
||||
message.setThreadID(messageRecord.getThreadId());
|
||||
if (messageRecord.isMms()) {
|
||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
||||
if (!mmsMessageRecord.getLinkPreviews().isEmpty()) {
|
||||
message.setLinkPreview(LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0)));
|
||||
}
|
||||
if (mmsMessageRecord.getQuote() != null) {
|
||||
message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel()));
|
||||
}
|
||||
message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments());
|
||||
}
|
||||
MessageSender.send(message, recipient.getAddress());
|
||||
resendButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
@ -249,7 +250,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onContactSelected(String number) {
|
||||
Recipient recipient = Recipient.from(this, Address.fromExternal(this, number), true);
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
||||
createConversation(existingThread, recipient.getAddress(), ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
createConversation(existingThread, recipient.getAddress(), DistributionTypes.DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,22 +1,29 @@
|
||||
package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.google.protobuf.ByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
@ -40,6 +47,14 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
return databaseAttachment.toSignalAttachmentStream(context)
|
||||
}
|
||||
|
||||
override fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = database.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
|
||||
val mediaConstraints = MediaConstraints.getPushMediaConstraints()
|
||||
val scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment) ?: return null
|
||||
return getAttachmentFor(scaledAttachment)
|
||||
}
|
||||
|
||||
override fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
|
||||
@ -51,12 +66,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun uploadAttachment(attachmentId: Long) {
|
||||
val attachmentUploadJob = AttachmentUploadJob(AttachmentId(attachmentId, 0), null)
|
||||
attachmentUploadJob.onRun()
|
||||
}
|
||||
|
||||
override fun getMessageForQuote(timestamp: Long, author: Address): Long? {
|
||||
val messagingDatabase = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
return messagingDatabase.getMessageFor(timestamp, author)?.id
|
||||
@ -72,6 +81,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
return messagingDatabase.getMessage(messageID).body
|
||||
}
|
||||
|
||||
override fun getAttachmentIDsFor(messageID: Long): List<Long> {
|
||||
return DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageID).mapNotNull {
|
||||
if (it.isQuote) return@mapNotNull null
|
||||
it.attachmentId.rowId
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? {
|
||||
val message = DatabaseFactory.getMmsDatabase(context).getOutgoingMessage(messageID)
|
||||
return message.linkPreviews.firstOrNull()?.attachmentId?.rowId
|
||||
}
|
||||
|
||||
override fun insertAttachment(messageId: Long, attachmentId: Long, stream: InputStream) {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, AttachmentId(attachmentId, 0), stream)
|
||||
@ -79,7 +100,32 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
|
||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||
return smsDatabase.isOutgoingMessage(timestamp)
|
||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
|
||||
}
|
||||
|
||||
override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
|
||||
attachmentStream.contentType,
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachmentStream.length)),
|
||||
attachmentStream.preview,
|
||||
attachmentStream.width, attachmentStream.height,
|
||||
Optional.fromNullable(uploadResult.digest),
|
||||
attachmentStream.fileName,
|
||||
attachmentStream.voiceNote,
|
||||
attachmentStream.caption,
|
||||
uploadResult.url);
|
||||
val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get()
|
||||
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment)
|
||||
}
|
||||
|
||||
override fun updateAttachmentAfterUploadFailed(attachmentId: Long) {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||
database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId)
|
||||
}
|
||||
|
||||
override fun getMessageID(serverID: Long): Long? {
|
||||
@ -92,6 +138,52 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
messagingDatabase.deleteMessage(messageID)
|
||||
}
|
||||
|
||||
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||
return attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0))
|
||||
}
|
||||
|
||||
private fun scaleAndStripExif(attachmentDatabase: AttachmentDatabase, constraints: MediaConstraints, attachment: Attachment): Attachment? {
|
||||
return try {
|
||||
if (constraints.isSatisfied(context, attachment)) {
|
||||
if (MediaUtil.isJpeg(attachment)) {
|
||||
val stripped = constraints.getResizedMedia(context, attachment)
|
||||
attachmentDatabase.updateAttachmentData(attachment, stripped)
|
||||
} else {
|
||||
attachment
|
||||
}
|
||||
} else if (constraints.canResize(attachment)) {
|
||||
val resized = constraints.getResizedMedia(context, attachment)
|
||||
attachmentDatabase.updateAttachmentData(attachment, resized)
|
||||
} else {
|
||||
throw Exception("Size constraints could not be met!")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAttachmentFor(attachment: Attachment): SignalServiceAttachmentStream? {
|
||||
try {
|
||||
if (attachment.dataUri == null || attachment.size == 0L) throw IOException("Assertion failed, outgoing attachment has no data!")
|
||||
val `is` = PartAuthority.getAttachmentStream(context, attachment.dataUri!!)
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(`is`)
|
||||
.withContentType(attachment.contentType)
|
||||
.withLength(attachment.size)
|
||||
.withFileName(attachment.fileName)
|
||||
.withVoiceNote(attachment.isVoiceNote)
|
||||
.withWidth(attachment.width)
|
||||
.withHeight(attachment.height)
|
||||
.withCaption(attachment.caption)
|
||||
.withListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(attachment, total, progress)) }
|
||||
.build()
|
||||
} catch (ioe: IOException) {
|
||||
Log.w("Loki", "Couldn't open attachment", ioe)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer {
|
||||
@ -117,8 +209,28 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac
|
||||
return attachmentStream
|
||||
}
|
||||
|
||||
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url)
|
||||
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? {
|
||||
if (TextUtils.isEmpty(location)) { return null }
|
||||
if (TextUtils.isEmpty(key)) { return null }
|
||||
|
||||
return try {
|
||||
val id: Long = location!!.toLong()
|
||||
val key: ByteArray = Base64.decode(key!!)
|
||||
SignalServiceAttachmentPointer(id,
|
||||
contentType,
|
||||
key,
|
||||
Optional.of(Util.toIntExact(size)),
|
||||
Optional.absent(),
|
||||
width,
|
||||
height,
|
||||
Optional.fromNullable(digest),
|
||||
Optional.fromNullable(fileName),
|
||||
isVoiceNote,
|
||||
Optional.fromNullable(caption),
|
||||
url)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseAttachment.toSignalAttachmentStream(context: Context): SignalServiceAttachmentStream {
|
||||
|
@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.attachments;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
@ -30,11 +31,11 @@ public class MmsNotificationAttachment extends Attachment {
|
||||
if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED ||
|
||||
status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY)
|
||||
{
|
||||
return AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
|
||||
return AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING;
|
||||
} else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) {
|
||||
return AttachmentDatabase.TRANSFER_PROGRESS_STARTED;
|
||||
return AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED;
|
||||
} else {
|
||||
return AttachmentDatabase.TRANSFER_PROGRESS_FAILED;
|
||||
return AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import org.session.libsession.utilities.Conversions
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.*
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
|
||||
import org.thoughtcrime.securesms.database.*
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
@ -91,7 +90,7 @@ object FullBackupExporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (preference in IdentityKeyUtil.getBackupRecords(context)) {
|
||||
for (preference in BackupUtil.getBackupRecords(context)) {
|
||||
EventBus.getDefault().post(BackupEvent.createProgress(++count))
|
||||
outputStream.writePreferenceEntry(preference)
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@ -94,7 +96,7 @@ public class DocumentView extends FrameLayout {
|
||||
controlToggle.displayQuick(downloadButton);
|
||||
downloadButton.setOnClickListener(new DownloadClickedListener(documentSlide));
|
||||
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
||||
} else if (showControls && documentSlide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
|
||||
} else if (showControls && documentSlide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) {
|
||||
controlToggle.displayQuick(downloadProgress);
|
||||
downloadProgress.spin();
|
||||
} else {
|
||||
|
@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -248,7 +249,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
|
||||
(slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
(slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
{
|
||||
this.playOverlay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
@ -402,7 +403,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
if (thumbnailClickListener != null &&
|
||||
slide != null &&
|
||||
slide.asAttachment().getDataUri() != null &&
|
||||
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
|
||||
slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE)
|
||||
{
|
||||
thumbnailClickListener.onClick(view, slide);
|
||||
} else if (parentClickListener != null) {
|
||||
|
@ -17,6 +17,7 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
@ -105,17 +106,17 @@ public class TransferControlView extends FrameLayout {
|
||||
}
|
||||
|
||||
for (Slide slide : slides) {
|
||||
if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
if (slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
||||
downloadProgress.put(slide.asAttachment(), 1f);
|
||||
}
|
||||
}
|
||||
|
||||
switch (getTransferState(slides)) {
|
||||
case AttachmentDatabase.TRANSFER_PROGRESS_STARTED:
|
||||
case AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED:
|
||||
showProgressSpinner(calculateProgress(downloadProgress));
|
||||
break;
|
||||
case AttachmentDatabase.TRANSFER_PROGRESS_PENDING:
|
||||
case AttachmentDatabase.TRANSFER_PROGRESS_FAILED:
|
||||
case AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING:
|
||||
case AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED:
|
||||
downloadDetailsText.setText(getDownloadText(this.slides));
|
||||
display(downloadDetails);
|
||||
break;
|
||||
@ -174,9 +175,9 @@ public class TransferControlView extends FrameLayout {
|
||||
}
|
||||
|
||||
private int getTransferState(@NonNull List<Slide> slides) {
|
||||
int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE;
|
||||
int transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
|
||||
for (Slide slide : slides) {
|
||||
if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
if (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
||||
transferState = slide.getTransferState();
|
||||
} else {
|
||||
transferState = Math.max(transferState, slide.getTransferState());
|
||||
@ -189,7 +190,7 @@ public class TransferControlView extends FrameLayout {
|
||||
if (slides.size() == 1) {
|
||||
return slides.get(0).getContentDescription();
|
||||
} else {
|
||||
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE ? count + 1 : count);
|
||||
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE ? count + 1 : count);
|
||||
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
|
||||
}
|
||||
}
|
||||
|
@ -4,25 +4,21 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
public class TypingStatusSender {
|
||||
|
||||
private static final String TAG = TypingStatusSender.class.getSimpleName();
|
||||
|
||||
private static final long REFRESH_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
private static final long PAUSE_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
|
||||
|
||||
@ -82,12 +78,16 @@ public class TypingStatusSender {
|
||||
private void sendTyping(long threadId, boolean typingStarted) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||
if (recipient == null) { return; }
|
||||
// Loki - Check whether we want to send a typing indicator to this user
|
||||
if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
|
||||
// Loki - Take into account multi device
|
||||
if (recipient == null) { return; }
|
||||
long threadID = threadDatabase.getOrCreateThreadIdFor(recipient);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadID, typingStarted));
|
||||
TypingIndicator typingIndicator;
|
||||
if (typingStarted) {
|
||||
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
|
||||
} else {
|
||||
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STOPPED);
|
||||
}
|
||||
MessageSender.send(typingIndicator, recipient.getAddress());
|
||||
}
|
||||
|
||||
private class StartRunnable implements Runnable {
|
||||
|
@ -85,6 +85,10 @@ import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
@ -137,7 +141,7 @@ import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
|
||||
@ -153,9 +157,10 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.QuoteId;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
@ -168,9 +173,8 @@ import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.recipients.RecipientFormattingException;
|
||||
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
@ -197,7 +201,6 @@ import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -571,16 +574,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
final Context context = ConversationActivity.this.getApplicationContext();
|
||||
|
||||
sendMediaMessage(false,
|
||||
message,
|
||||
sendMediaMessage(message,
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
expiresIn,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true).addListener(new AssertedSuccessListener<Void>() {
|
||||
Optional.absent(),
|
||||
initiating).addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
@ -648,7 +646,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
if (!isPushGroupConversation()) {
|
||||
inflater.inflate(R.menu.conversation_mms_group_options, menu);
|
||||
if (distributionType == ThreadDatabase.DistributionTypes.BROADCAST) {
|
||||
if (distributionType == DistributionTypes.BROADCAST) {
|
||||
menu.findItem(R.id.menu_distribution_broadcast).setChecked(true);
|
||||
} else {
|
||||
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
|
||||
@ -808,9 +806,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime);
|
||||
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
|
||||
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
|
||||
|
||||
ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime);
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient);
|
||||
try {
|
||||
message.setId(DatabaseFactory.getMmsDatabase(ConversationActivity.this).insertMessageOutbox(outgoingMessage, getAllocatedThreadId(ConversationActivity.this), false, null));
|
||||
MessageSender.send(message, recipient.getAddress());
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -839,17 +843,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void handleConversationSettings() {
|
||||
/*
|
||||
Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class);
|
||||
intent.putExtra(RecipientPreferenceActivity.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA,
|
||||
isSecureText && !isSelfConversation());
|
||||
|
||||
startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar");
|
||||
*/
|
||||
}
|
||||
|
||||
private void handleUnmuteNotifications() {
|
||||
recipient.setMuted(0);
|
||||
|
||||
@ -1018,7 +1011,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
try {
|
||||
if (isClosedGroup) {
|
||||
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey);
|
||||
MessageSender.explicitLeave(groupPublicKey);
|
||||
initializeEnabledCheck();
|
||||
} else {
|
||||
Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
@ -1040,7 +1033,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void handleDistributionBroadcastEnabled(MenuItem item) {
|
||||
distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
|
||||
distributionType = DistributionTypes.BROADCAST;
|
||||
item.setChecked(true);
|
||||
|
||||
if (threadId != -1) {
|
||||
@ -1048,7 +1041,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getThreadDatabase(ConversationActivity.this)
|
||||
.setDistributionType(threadId, ThreadDatabase.DistributionTypes.BROADCAST);
|
||||
.setDistributionType(threadId, DistributionTypes.BROADCAST);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
@ -1056,7 +1049,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void handleDistributionConversationEnabled(MenuItem item) {
|
||||
distributionType = ThreadDatabase.DistributionTypes.CONVERSATION;
|
||||
distributionType = DistributionTypes.CONVERSATION;
|
||||
item.setChecked(true);
|
||||
|
||||
if (threadId != -1) {
|
||||
@ -1064,7 +1057,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getThreadDatabase(ConversationActivity.this)
|
||||
.setDistributionType(threadId, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
.setDistributionType(threadId, DistributionTypes.CONVERSATION);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
@ -1335,7 +1328,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (address == null) { finish(); return; }
|
||||
recipient = Recipient.from(this, address, true);
|
||||
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
|
||||
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT);
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
||||
recipient.addListener(this);
|
||||
@ -1483,14 +1476,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
else if (contactData.numbers.size() > 1) selectContactInfo(contactData);
|
||||
}
|
||||
|
||||
private void sendSharedContact(List<Contact> contacts) {
|
||||
int subscriptionId = -1;
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(false, "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, subscriptionId, initiating, false);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
final CharSequence[] numbers = new CharSequence[contactData.numbers.size()];
|
||||
final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()];
|
||||
@ -1677,7 +1662,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
Context context = ConversationActivity.this;
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false);
|
||||
|
||||
if (!org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) {
|
||||
if (!SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) {
|
||||
for (MarkedMessageInfo messageInfo : messageIds) {
|
||||
MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo());
|
||||
}
|
||||
@ -1735,8 +1720,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
String message = getMessage();
|
||||
int subscriptionId = -1;
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||
@ -1747,9 +1730,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
needsSplit;
|
||||
|
||||
if (isMediaMessage) {
|
||||
sendMediaMessage(expiresIn, subscriptionId, initiating);
|
||||
sendMediaMessage(initiating);
|
||||
} else {
|
||||
sendTextMessage(expiresIn, subscriptionId, initiating);
|
||||
sendTextMessage(initiating);
|
||||
}
|
||||
} catch (RecipientFormattingException ex) {
|
||||
Log.w(TAG, ex);
|
||||
@ -1764,23 +1747,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMediaMessage(final long expiresIn, final int subscriptionId, boolean initiating)
|
||||
private void sendMediaMessage(boolean initiating)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
Log.i(TAG, "Sending media message...");
|
||||
sendMediaMessage(false, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, subscriptionId, initiating, true);
|
||||
sendMediaMessage(getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), linkPreviewViewModel.getActiveLinkPreview(), initiating);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
|
||||
String body,
|
||||
private ListenableFuture<Void> sendMediaMessage(String body,
|
||||
SlideDeck slideDeck,
|
||||
QuoteModel quote,
|
||||
List<Contact> contacts,
|
||||
List<LinkPreview> previews,
|
||||
final long expiresIn,
|
||||
final int subscriptionId,
|
||||
final boolean initiating,
|
||||
final boolean clearComposeBox)
|
||||
Optional<LinkPreview> linkPreview,
|
||||
final boolean initiating)
|
||||
{
|
||||
|
||||
Pair<String, Optional<Slide>> splitMessage = getSplitMessage(body, characterCalculator.calculateCharacters(body).maxPrimaryMessageSize);
|
||||
@ -1790,7 +1768,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
slideDeck.addSlide(splitMessage.second.get());
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, quote, contacts, previews);
|
||||
List<Attachment> attachments = slideDeck.asAttachments();
|
||||
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
message.setText(body);
|
||||
OutgoingMediaMessage outgoingMessageCandidate = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview.orNull());
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = getApplicationContext();
|
||||
@ -1800,11 +1783,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate);
|
||||
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
if (clearComposeBox) {
|
||||
inputPanel.clearQuote();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
silentlySetComposeText("");
|
||||
}
|
||||
inputPanel.clearQuote();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
silentlySetComposeText("");
|
||||
|
||||
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
||||
|
||||
@ -1812,43 +1793,44 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
|
||||
}
|
||||
|
||||
long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
|
||||
sendComplete(result);
|
||||
try {
|
||||
long allocatedThreadId = getAllocatedThreadId(context);
|
||||
message.setId(DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMessage, allocatedThreadId, false, ()->fragment.releaseOutgoingMessage(id)));
|
||||
MessageSender.send(message, recipient.getAddress(), attachments, quote, linkPreview.orNull());
|
||||
sendComplete(allocatedThreadId);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
sendComplete(threadId);
|
||||
}
|
||||
future.set(null);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void sendTextMessage(final long expiresIn, final int subscriptionId, final boolean initiatingConversation)
|
||||
private void sendTextMessage(final boolean initiating)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
final Context context = getApplicationContext();
|
||||
final String messageBody = getMessage();
|
||||
|
||||
OutgoingTextMessage message;
|
||||
|
||||
message = new OutgoingEncryptedMessage(recipient, messageBody, expiresIn);
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
message.setText(messageBody);
|
||||
OutgoingTextMessage outgoingTextMessage = OutgoingTextMessage.from(message, recipient);
|
||||
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
silentlySetComposeText("");
|
||||
final long id = fragment.stageOutgoingMessage(message);
|
||||
final long id = fragment.stageOutgoingMessage(outgoingTextMessage);
|
||||
|
||||
if (initiatingConversation) {
|
||||
if (initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
|
||||
}
|
||||
|
||||
long result = MessageSender.send(context, message, threadId, false, () -> fragment.releaseOutgoingMessage(id));
|
||||
long allocatedThreadId = getAllocatedThreadId(context);
|
||||
message.setId(DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(allocatedThreadId, outgoingTextMessage, false, message.getSentTimestamp(), ()->fragment.releaseOutgoingMessage(id)));
|
||||
MessageSender.send(message, recipient.getAddress());
|
||||
|
||||
sendComplete(result);
|
||||
}
|
||||
|
||||
private void showDefaultSmsPrompt() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(R.string.ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app)
|
||||
.setNegativeButton(R.string.ConversationActivity_no, (dialog, which) -> dialog.dismiss())
|
||||
.setPositiveButton(R.string.ConversationActivity_yes, (dialog, which) -> handleMakeDefaultSms())
|
||||
.show();
|
||||
sendComplete(allocatedThreadId);
|
||||
}
|
||||
|
||||
private void updateToggleButtonState() {
|
||||
@ -1934,7 +1916,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
sendMediaMessage(false, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating, true).addListener(new AssertedSuccessListener<Void>() {
|
||||
sendMediaMessage("", slideDeck, inputPanel.getQuote().orNull(), Optional.absent(), initiating).addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@ -2319,6 +2301,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
// region Loki
|
||||
private long getAllocatedThreadId(Context context) {
|
||||
long allocatedThreadId;
|
||||
if (threadId == -1) {
|
||||
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
} else {
|
||||
allocatedThreadId = threadId;
|
||||
}
|
||||
return allocatedThreadId;
|
||||
}
|
||||
|
||||
private void updateTitleTextView(Recipient recipient) {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (recipient == null) {
|
||||
|
@ -57,6 +57,9 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.Quote;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
@ -77,22 +80,21 @@ import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
@ -405,7 +407,7 @@ public class ConversationFragment extends Fragment
|
||||
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
|
||||
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
boolean userCanModerate = isPublicChat && PublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
|
||||
boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
|
||||
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
||||
// allow banning if moderating a public chat and only one user's messages are selected
|
||||
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
||||
@ -521,7 +523,6 @@ public class ConversationFragment extends Fragment
|
||||
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
||||
ArrayList<Long> failedMessages = new ArrayList<>();
|
||||
boolean isSentByUser = true;
|
||||
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
isSentByUser = isSentByUser && messageRecord.isOutgoing();
|
||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
|
||||
@ -531,8 +532,8 @@ public class ConversationFragment extends Fragment
|
||||
ignoredMessages.add(messageRecord.getId());
|
||||
}
|
||||
}
|
||||
if (publicChat != null && publicChatAPI != null) {
|
||||
publicChatAPI
|
||||
if (publicChat != null) {
|
||||
OpenGroupAPI
|
||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
||||
.success(l -> {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
@ -602,9 +603,7 @@ public class ConversationFragment extends Fragment
|
||||
protected Void doInBackground(String... userPublicKeyParam) {
|
||||
String userPublicKey = userPublicKeyParam[0];
|
||||
if (publicChat != null) {
|
||||
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
|
||||
if (publicChat != null && publicChatAPI != null) {
|
||||
publicChatAPI
|
||||
OpenGroupAPI
|
||||
.ban(userPublicKey, publicChat.getServer())
|
||||
.success(l -> {
|
||||
Log.d("Loki", "User banned");
|
||||
@ -613,7 +612,6 @@ public class ConversationFragment extends Fragment
|
||||
Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + ".");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Tried to ban user from a non-public chat");
|
||||
}
|
||||
@ -696,11 +694,32 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleResendMessage(final MessageRecord message) {
|
||||
final Context context = getActivity().getApplicationContext();
|
||||
new AsyncTask<MessageRecord, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(MessageRecord... messageRecords) {
|
||||
MessageSender.resend(context, messageRecords[0]);
|
||||
MessageRecord messageRecord = messageRecords[0];
|
||||
Recipient recipient = messageRecord.getRecipient();
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setId(messageRecord.getId());
|
||||
message.setText(messageRecord.getBody());
|
||||
message.setSentTimestamp(messageRecord.getTimestamp());
|
||||
if (recipient.isGroupRecipient()) {
|
||||
message.setGroupPublicKey(recipient.getAddress().toGroupString());
|
||||
} else {
|
||||
message.setRecipient(messageRecord.getRecipient().getAddress().serialize());
|
||||
}
|
||||
message.setThreadID(messageRecord.getThreadId());
|
||||
if (messageRecord.isMms()) {
|
||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
||||
if (!mmsMessageRecord.getLinkPreviews().isEmpty()) {
|
||||
message.setLinkPreview(org.session.libsession.messaging.messages.visible.LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0)));
|
||||
}
|
||||
if (mmsMessageRecord.getQuote() != null) {
|
||||
message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel()));
|
||||
}
|
||||
message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments());
|
||||
}
|
||||
MessageSender.send(message, recipient.getAddress());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
|
||||
|
@ -53,6 +53,7 @@ import androidx.annotation.Nullable;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
||||
@ -1003,10 +1004,10 @@ public class ConversationItem extends LinearLayout
|
||||
if (messageRecord.isMms()) {
|
||||
TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide();
|
||||
|
||||
if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
|
||||
message = getResources().getString(R.string.ConversationItem_read_more);
|
||||
action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
|
||||
} else if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
|
||||
} else if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) {
|
||||
message = getResources().getString(R.string.ConversationItem_pending);
|
||||
action = () -> {};
|
||||
} else if (slide != null) {
|
||||
|
@ -1,88 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||
import org.session.libsignal.metadata.SignalProtos;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
public class UnidentifiedAccessUtil {
|
||||
|
||||
private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName();
|
||||
|
||||
@WorkerThread
|
||||
public static Optional<UnidentifiedAccess> getAccessFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
|
||||
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null));
|
||||
|
||||
if (theirUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessCertificate != null)
|
||||
{
|
||||
return Optional.of(new UnidentifiedAccess(theirUnidentifiedAccessKey));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static Optional<UnidentifiedAccess> getAccessForSync(@NonNull Context context) {
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
return Optional.of(new UnidentifiedAccess(ourUnidentifiedAccessKey));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) {
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context));
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
|
||||
byte[] theirProfileKey = recipient.resolve().getProfileKey();
|
||||
|
||||
if (theirProfileKey == null) return Util.getSecretBytes(16);
|
||||
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
||||
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getUnidentifiedAccessCertificate(Context context) {
|
||||
String ourNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
if (ourNumber != null) {
|
||||
SignalProtos.SenderCertificate certificate = SignalProtos.SenderCertificate.newBuilder()
|
||||
.setSender(ourNumber)
|
||||
.setSenderDevice(SignalServiceAddress.DEFAULT_DEVICE_ID)
|
||||
.build();
|
||||
return certificate.toByteArray();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.session.libsignal.libsignal.IdentityKeyPair;
|
||||
import org.session.libsignal.libsignal.state.IdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
|
||||
public class SignalProtocolStoreImpl implements IdentityKeyStore {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SignalProtocolStoreImpl(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
@ -119,12 +120,8 @@ public class AttachmentDatabase extends Database {
|
||||
static final String AUDIO_VISUAL_SAMPLES = "audio_visual_samples"; // Small amount of audio byte samples to visualise the content (e.g. draw waveform).
|
||||
static final String AUDIO_DURATION = "audio_duration"; // Duration of the audio track in milliseconds.
|
||||
|
||||
public static final int TRANSFER_PROGRESS_DONE = 0;
|
||||
public static final int TRANSFER_PROGRESS_STARTED = 1;
|
||||
public static final int TRANSFER_PROGRESS_PENDING = 2;
|
||||
public static final int TRANSFER_PROGRESS_FAILED = 3;
|
||||
|
||||
private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?";
|
||||
private static final String ROW_ID_WHERE = ROW_ID + " = ?";
|
||||
private static final String PART_AUDIO_ONLY_WHERE = CONTENT_TYPE + " LIKE \"audio/%\"";
|
||||
|
||||
private static final String[] PROJECTION = new String[] {ROW_ID,
|
||||
@ -199,21 +196,12 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsStickerPackId(@NonNull String stickerPackId) {
|
||||
String selection = STICKER_PACK_ID + " = ?";
|
||||
String[] args = new String[] { stickerPackId };
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) {
|
||||
return cursor != null && cursor.moveToFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId)
|
||||
throws MmsException
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED);
|
||||
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED);
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
|
||||
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId));
|
||||
@ -225,7 +213,7 @@ public class AttachmentDatabase extends Database {
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, ROW_ID_WHERE, new String[]{String.valueOf(attachmentId.getRowId())}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
List<DatabaseAttachment> list = getAttachment(cursor);
|
||||
@ -268,7 +256,7 @@ public class AttachmentDatabase extends Database {
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
|
||||
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED)}, null, null, null);
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
attachments.addAll(getAttachment(cursor));
|
||||
}
|
||||
@ -372,7 +360,7 @@ public class AttachmentDatabase extends Database {
|
||||
values.put(DATA_RANDOM, dataInfo.random);
|
||||
}
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE);
|
||||
values.put(CONTENT_LOCATION, (String)null);
|
||||
values.put(CONTENT_DISPOSITION, (String)null);
|
||||
values.put(DIGEST, (byte[])null);
|
||||
@ -395,7 +383,7 @@ public class AttachmentDatabase extends Database {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE);
|
||||
values.put(CONTENT_LOCATION, attachment.getLocation());
|
||||
values.put(DIGEST, attachment.getDigest());
|
||||
values.put(CONTENT_DISPOSITION, attachment.getKey());
|
||||
@ -411,7 +399,7 @@ public class AttachmentDatabase extends Database {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED);
|
||||
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED);
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
|
||||
}
|
||||
@ -506,37 +494,17 @@ public class AttachmentDatabase extends Database {
|
||||
databaseAttachment.getUrl());
|
||||
}
|
||||
|
||||
|
||||
public void updateAttachmentFileName(@NonNull AttachmentId attachmentId,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(FILE_NAME, ExternalStorageUtil.getCleanFileName(fileName));
|
||||
|
||||
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings());
|
||||
}
|
||||
|
||||
public void markAttachmentUploaded(long messageId, Attachment attachment) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE);
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings());
|
||||
|
||||
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
|
||||
((DatabaseAttachment) attachment).setUploaded(true);
|
||||
}
|
||||
|
||||
public void setTransferState(long messageId, @NonNull Attachment attachment, int transferState) {
|
||||
if (!(attachment instanceof DatabaseAttachment)) {
|
||||
throw new AssertionError("Attempt to update attachment that doesn't belong to DB!");
|
||||
}
|
||||
|
||||
setTransferState(messageId, ((DatabaseAttachment) attachment).getAttachmentId(), transferState);
|
||||
}
|
||||
|
||||
public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, int transferState) {
|
||||
final ContentValues values = new ContentValues(1);
|
||||
final SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
@ -546,14 +514,6 @@ public class AttachmentDatabase extends Database {
|
||||
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
|
||||
}
|
||||
|
||||
public boolean hasStickerAttachments() {
|
||||
String selection = STICKER_PACK_ID + " NOT NULL";
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, null, null, null, null, "1")) {
|
||||
return cursor != null && cursor.moveToFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset)
|
||||
|
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
public class ContentValuesBuilder {
|
||||
|
||||
private final ContentValues contentValues;
|
||||
|
||||
public ContentValuesBuilder(ContentValues contentValues) {
|
||||
this.contentValues = contentValues;
|
||||
}
|
||||
|
||||
public void add(String key, String charsetKey, EncodedStringValue value) {
|
||||
if (value != null) {
|
||||
contentValues.put(key, Util.toIsoString(value.getTextString()));
|
||||
contentValues.put(charsetKey, value.getCharacterSet());
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String contentKey, byte[] value) {
|
||||
if (value != null) {
|
||||
contentValues.put(contentKey, Util.toIsoString(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void add(String contentKey, int b) {
|
||||
if (b != 0)
|
||||
contentValues.put(contentKey, b);
|
||||
}
|
||||
|
||||
public void add(String contentKey, long value) {
|
||||
if (value != -1L)
|
||||
contentValues.put(contentKey, value);
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
return contentValues;
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@ import android.text.TextUtils;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.session.libsession.database.documents.Document;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.libsignal.IdentityKey;
|
||||
@ -38,27 +38,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
public abstract void markAsSent(long messageId, boolean secure);
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
|
||||
public void setMismatchedIdentity(long messageId, final Address address, final IdentityKey identityKey) {
|
||||
List<IdentityKeyMismatch> items = new ArrayList<IdentityKeyMismatch>() {{
|
||||
add(new IdentityKeyMismatch(address, identityKey));
|
||||
}};
|
||||
|
||||
IdentityKeyMismatchList document = new IdentityKeyMismatchList(items);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.beginTransaction();
|
||||
|
||||
try {
|
||||
setDocument(database, messageId, MISMATCHED_IDENTITIES, document);
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
|
||||
try {
|
||||
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
||||
|
@ -19,9 +19,7 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -38,22 +36,22 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatchList;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
import org.session.libsession.database.documents.NetworkFailureList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
|
@ -86,15 +86,6 @@ public class MmsSmsDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getMessageFor(long messageId) {
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
|
||||
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.ID + " = " + messageId, null, null)) {
|
||||
MmsSmsDatabase.Reader reader = db.readerFor(cursor);
|
||||
return reader.getNext();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
|
||||
|
@ -23,8 +23,6 @@ import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
@ -32,15 +30,15 @@ import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
@ -51,7 +49,6 @@ import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -13,7 +13,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachment
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
@ -30,20 +29,21 @@ import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
|
||||
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
||||
override fun getUserPublicKey(): String? {
|
||||
@ -65,8 +65,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
|
||||
override fun getUserProfileKey(): ByteArray? {
|
||||
val profileKey = TextSecurePreferences.getProfileKey(context) ?: return null
|
||||
return profileKey.toByteArray()
|
||||
return ProfileKeyUtil.getProfileKey(context)
|
||||
}
|
||||
|
||||
override fun getUserProfilePictureURL(): String? {
|
||||
@ -81,7 +80,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
|
||||
override fun getOrGenerateRegistrationID(): Int {
|
||||
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
|
||||
if (registrationID == null) {
|
||||
if (registrationID == 0) {
|
||||
registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
|
||||
}
|
||||
@ -90,7 +89,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
|
||||
override fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long> {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachments = attachments.map { it.toDatabaseAttachment() }
|
||||
val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() }
|
||||
return database.insertAttachments(messageId, databaseAttachments)
|
||||
}
|
||||
|
||||
@ -125,7 +124,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}.mapNotNull {
|
||||
PointerAttachment.forPointer(Optional.of(it)).orNull()
|
||||
}
|
||||
val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull())
|
||||
val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull().firstOrNull())
|
||||
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
|
||||
} else {
|
||||
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
||||
@ -331,9 +330,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID)
|
||||
}
|
||||
|
||||
override fun markAsSent(messageID: Long) {
|
||||
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
||||
return DatabaseFactory.getLokiMessageDatabase(context).getQuoteServerID(quoteID, publicKey)
|
||||
}
|
||||
|
||||
override fun markAsSent(timestamp: Long, author: String) {
|
||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
val messageRecord = database.getMessageFor(messageID)!!
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
if (messageRecord.isMms) {
|
||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
mmsDatabase.markAsSent(messageRecord.getId(), true)
|
||||
@ -343,9 +346,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
}
|
||||
|
||||
override fun markUnidentified(messageID: Long) {
|
||||
override fun markUnidentified(timestamp: Long, author: String) {
|
||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
val messageRecord = database.getMessageFor(messageID)!!
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
if (messageRecord.isMms) {
|
||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
mmsDatabase.markUnidentified(messageRecord.getId(), true)
|
||||
@ -355,15 +358,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
}
|
||||
|
||||
override fun setErrorMessage(messageID: Long, error: Exception) {
|
||||
override fun setErrorMessage(timestamp: Long, author: String, error: Exception) {
|
||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
val messageRecord = database.getMessageFor(messageID) ?: return
|
||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||
if (messageRecord.isMms) {
|
||||
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
mmsDatabase.markAsSentFailed(messageID)
|
||||
mmsDatabase.markAsSentFailed(messageRecord.getId())
|
||||
} else {
|
||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDatabase.markAsSentFailed(messageID)
|
||||
smsDatabase.markAsSentFailed(messageRecord.getId())
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,7 +391,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
|
||||
}
|
||||
|
||||
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>) {
|
||||
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type0)
|
||||
@ -396,13 +399,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true)
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
|
||||
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
|
||||
val userPublicKey = getUserPublicKey()
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
@ -410,8 +414,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.GroupRecord;
|
||||
@ -598,14 +599,6 @@ public class ThreadDatabase extends Database {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public static class DistributionTypes {
|
||||
public static final int DEFAULT = 2;
|
||||
public static final int BROADCAST = 1;
|
||||
public static final int CONVERSATION = 2;
|
||||
public static final int ARCHIVE = 3;
|
||||
public static final int INBOX_ZERO = 4;
|
||||
}
|
||||
|
||||
public class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
@ -1,245 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class XmlBackup {
|
||||
|
||||
private static final String PROTOCOL = "protocol";
|
||||
private static final String ADDRESS = "address";
|
||||
private static final String CONTACT_NAME = "contact_name";
|
||||
private static final String DATE = "date";
|
||||
private static final String READABLE_DATE = "readable_date";
|
||||
private static final String TYPE = "type";
|
||||
private static final String SUBJECT = "subject";
|
||||
private static final String BODY = "body";
|
||||
private static final String SERVICE_CENTER = "service_center";
|
||||
private static final String READ = "read";
|
||||
private static final String STATUS = "status";
|
||||
private static final String TOA = "toa";
|
||||
private static final String SC_TOA = "sc_toa";
|
||||
private static final String LOCKED = "locked";
|
||||
|
||||
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
|
||||
|
||||
private final XmlPullParser parser;
|
||||
|
||||
public XmlBackup(String path) throws XmlPullParserException, FileNotFoundException {
|
||||
this.parser = XmlPullParserFactory.newInstance().newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(new FileInputStream(path), null);
|
||||
}
|
||||
|
||||
public XmlBackupItem getNext() throws IOException, XmlPullParserException {
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = parser.getName();
|
||||
|
||||
if (!name.equalsIgnoreCase("sms")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int attributeCount = parser.getAttributeCount();
|
||||
|
||||
if (attributeCount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XmlBackupItem item = new XmlBackupItem();
|
||||
|
||||
for (int i=0;i<attributeCount;i++) {
|
||||
String attributeName = parser.getAttributeName(i);
|
||||
|
||||
if (attributeName.equals(PROTOCOL )) item.protocol = Integer.parseInt(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(ADDRESS )) item.address = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(CONTACT_NAME )) item.contactName = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(DATE )) item.date = Long.parseLong(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(READABLE_DATE )) item.readableDate = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(TYPE )) item.type = Integer.parseInt(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(SUBJECT )) item.subject = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(BODY )) item.body = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(SERVICE_CENTER)) item.serviceCenter = parser.getAttributeValue(i);
|
||||
else if (attributeName.equals(READ )) item.read = Integer.parseInt(parser.getAttributeValue(i));
|
||||
else if (attributeName.equals(STATUS )) item.status = Integer.parseInt(parser.getAttributeValue(i));
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class XmlBackupItem {
|
||||
private int protocol;
|
||||
private String address;
|
||||
private String contactName;
|
||||
private long date;
|
||||
private String readableDate;
|
||||
private int type;
|
||||
private String subject;
|
||||
private String body;
|
||||
private String serviceCenter;
|
||||
private int read;
|
||||
private int status;
|
||||
|
||||
public XmlBackupItem() {}
|
||||
|
||||
public XmlBackupItem(int protocol, String address, String contactName, long date, int type,
|
||||
String subject, String body, String serviceCenter, int read, int status)
|
||||
{
|
||||
this.protocol = protocol;
|
||||
this.address = address;
|
||||
this.contactName = contactName;
|
||||
this.date = date;
|
||||
this.readableDate = dateFormatter.format(date);
|
||||
this.type = type;
|
||||
this.subject = subject;
|
||||
this.body = body;
|
||||
this.serviceCenter = serviceCenter;
|
||||
this.read = read;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getContactName() {
|
||||
return contactName;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public String getReadableDate() {
|
||||
return readableDate;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public String getServiceCenter() {
|
||||
return serviceCenter;
|
||||
}
|
||||
|
||||
public int getRead() {
|
||||
return read;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Writer {
|
||||
|
||||
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>";
|
||||
private static final String CREATED_BY = "<!-- File Created By Signal -->";
|
||||
private static final String OPEN_TAG_SMSES = "<smses count=\"%d\">";
|
||||
private static final String CLOSE_TAG_SMSES = "</smses>";
|
||||
private static final String OPEN_TAG_SMS = " <sms ";
|
||||
private static final String CLOSE_EMPTYTAG = "/>";
|
||||
private static final String OPEN_ATTRIBUTE = "=\"";
|
||||
private static final String CLOSE_ATTRIBUTE = "\" ";
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("[^\u0020-\uD7FF]");
|
||||
|
||||
private final BufferedWriter bufferedWriter;
|
||||
|
||||
public Writer(String path, int count) throws IOException {
|
||||
bufferedWriter = new BufferedWriter(new FileWriter(path, false));
|
||||
|
||||
bufferedWriter.write(XML_HEADER);
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(CREATED_BY);
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(String.format(Locale.ROOT, OPEN_TAG_SMSES, count));
|
||||
}
|
||||
|
||||
public void writeItem(XmlBackupItem item) throws IOException {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
stringBuilder.append(OPEN_TAG_SMS);
|
||||
appendAttribute(stringBuilder, PROTOCOL, item.getProtocol());
|
||||
appendAttribute(stringBuilder, ADDRESS, escapeXML(item.getAddress()));
|
||||
appendAttribute(stringBuilder, CONTACT_NAME, escapeXML(item.getContactName()));
|
||||
appendAttribute(stringBuilder, DATE, item.getDate());
|
||||
appendAttribute(stringBuilder, READABLE_DATE, item.getReadableDate());
|
||||
appendAttribute(stringBuilder, TYPE, item.getType());
|
||||
appendAttribute(stringBuilder, SUBJECT, escapeXML(item.getSubject()));
|
||||
appendAttribute(stringBuilder, BODY, escapeXML(item.getBody()));
|
||||
appendAttribute(stringBuilder, TOA, "null");
|
||||
appendAttribute(stringBuilder, SC_TOA, "null");
|
||||
appendAttribute(stringBuilder, SERVICE_CENTER, item.getServiceCenter());
|
||||
appendAttribute(stringBuilder, READ, item.getRead());
|
||||
appendAttribute(stringBuilder, STATUS, item.getStatus());
|
||||
appendAttribute(stringBuilder, LOCKED, 0);
|
||||
stringBuilder.append(CLOSE_EMPTYTAG);
|
||||
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(stringBuilder.toString());
|
||||
}
|
||||
|
||||
private <T> void appendAttribute(StringBuilder stringBuilder, String name, T value) {
|
||||
stringBuilder.append(name).append(OPEN_ATTRIBUTE).append(value).append(CLOSE_ATTRIBUTE);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write(CLOSE_TAG_SMSES);
|
||||
bufferedWriter.close();
|
||||
}
|
||||
|
||||
private String escapeXML(String s) {
|
||||
if (TextUtils.isEmpty(s)) return s;
|
||||
|
||||
Matcher matcher = PATTERN.matcher( s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'"));
|
||||
StringBuffer st = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String escaped="";
|
||||
for (char ch: matcher.group(0).toCharArray()) {
|
||||
escaped += ("&#" + ((int) ch) + ";");
|
||||
}
|
||||
matcher.appendReplacement(st, escaped);
|
||||
}
|
||||
matcher.appendTail(st);
|
||||
return st.toString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.SessionJobDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
@ -52,9 +53,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV19 = 40;
|
||||
private static final int lokiV20 = 41;
|
||||
private static final int lokiV21 = 42;
|
||||
private static final int lokiV22 = 43;
|
||||
|
||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV21;
|
||||
private static final int DATABASE_VERSION = lokiV22;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -121,6 +123,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
|
||||
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
||||
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
|
||||
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
||||
|
||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||
@ -252,7 +255,21 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
"SmsSentJob",
|
||||
"SmsReceiveJob",
|
||||
"PushGroupUpdateJob",
|
||||
"ResetThreadSessionJob");
|
||||
"ResetThreadSessionJob",
|
||||
"SendDeliveryReceiptJob");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV22) {
|
||||
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
||||
deleteJobRecords(db,
|
||||
"PushGroupSendJob",
|
||||
"PushMediaSendJob",
|
||||
"PushTextSendJob",
|
||||
"SendReadReceiptJob",
|
||||
"TypingSendJob",
|
||||
"AttachmentUploadJob",
|
||||
"RequestGroupInfoJob",
|
||||
"ClosedGroupUpdateMessageSendJobV2");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -45,6 +45,7 @@ public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
|
||||
return new Pair<>(cursor, leftIsRecent ? cursor.getPosition() : cursor.getCount() - 1 - cursor.getPosition());
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
|
@ -26,8 +26,8 @@ import android.text.style.StyleSpan;
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.ExpirationUtil;
|
||||
@ -158,18 +158,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return SmsDatabase.Types.isIdentityDefault(type);
|
||||
}
|
||||
|
||||
public boolean isIdentityMismatchFailure() {
|
||||
return mismatches != null && !mismatches.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isBundleKeyExchange() {
|
||||
return SmsDatabase.Types.isBundleKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isContentBundleKeyExchange() {
|
||||
return SmsDatabase.Types.isContentBundleKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isIdentityUpdate() {
|
||||
return SmsDatabase.Types.isIdentityUpdate(type);
|
||||
}
|
||||
@ -195,10 +187,6 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return individualRecipient;
|
||||
}
|
||||
|
||||
public int getRecipientDeviceId() {
|
||||
return recipientDeviceId;
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
return type;
|
||||
}
|
||||
@ -211,10 +199,6 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return networkFailures;
|
||||
}
|
||||
|
||||
public boolean hasNetworkFailures() {
|
||||
return networkFailures != null && !networkFailures.isEmpty();
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@ -234,10 +218,6 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return (int)getId();
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
|
@ -23,8 +23,8 @@ import android.text.SpannableString;
|
||||
import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
|
@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.database.model;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
|
||||
@ -42,4 +43,8 @@ public class Quote {
|
||||
public @NonNull SlideDeck getAttachment() {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public QuoteModel getQuoteModel() {
|
||||
return new QuoteModel(id, author, text, missing, attachment.asAttachments());
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
@ -3,42 +3,20 @@ package org.thoughtcrime.securesms.dependencies;
|
||||
import android.content.Context;
|
||||
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module(complete = false, injects = {PushGroupSendJob.class,
|
||||
PushTextSendJob.class,
|
||||
PushMediaSendJob.class,
|
||||
AttachmentDownloadJob.class,
|
||||
RequestGroupInfoJob.class,
|
||||
@Module(complete = false, injects = {AttachmentDownloadJob.class,
|
||||
AvatarDownloadJob.class,
|
||||
RetrieveProfileAvatarJob.class,
|
||||
SendReadReceiptJob.class,
|
||||
AppProtectionPreferenceFragment.class,
|
||||
SendDeliveryReceiptJob.class,
|
||||
TypingSendJob.class,
|
||||
AttachmentUploadJob.class,
|
||||
PushDecryptJob.class,
|
||||
LinkPreviewRepository.class})
|
||||
|
||||
@ -46,30 +24,12 @@ public class SignalCommunicationModule {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private SignalServiceMessageSender messageSender;
|
||||
private SignalServiceMessageReceiver messageReceiver;
|
||||
|
||||
public SignalCommunicationModule(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public synchronized SignalServiceMessageSender provideSignalMessageSender() {
|
||||
if (this.messageSender == null) {
|
||||
this.messageSender = new SignalServiceMessageSender(new SignalProtocolStoreImpl(context),
|
||||
TextSecurePreferences.getLocalNumber(context),
|
||||
DatabaseFactory.getLokiAPIDatabase(context),
|
||||
DatabaseFactory.getLokiThreadDatabase(context),
|
||||
DatabaseFactory.getLokiMessageDatabase(context),
|
||||
new SessionProtocolImpl(context),
|
||||
DatabaseFactory.getLokiUserDatabase(context),
|
||||
DatabaseFactory.getGroupDatabase(context),
|
||||
((ApplicationContext)context.getApplicationContext()).broadcaster);
|
||||
}
|
||||
|
||||
return this.messageSender;
|
||||
}
|
||||
|
||||
@Provides
|
||||
synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() {
|
||||
if (this.messageReceiver == null) {
|
||||
|
@ -2,36 +2,23 @@ package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@ -72,7 +59,7 @@ public class GroupManager {
|
||||
groupDatabase.updateProfilePicture(groupId, avatarBytes);
|
||||
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(
|
||||
groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
groupRecipient, DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadID);
|
||||
}
|
||||
|
||||
@ -95,82 +82,6 @@ public class GroupManager {
|
||||
return groupDatabase.delete(groupId);
|
||||
}
|
||||
|
||||
public static GroupActionResult updateGroup(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull Set<Recipient> members,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name,
|
||||
@NonNull Set<Recipient> admins)
|
||||
{
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final Set<Address> memberAddresses = getMemberAddresses(members);
|
||||
final Set<Address> adminAddresses = getMemberAddresses(admins);
|
||||
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
|
||||
|
||||
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
|
||||
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
|
||||
groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses));
|
||||
groupDatabase.updateTitle(groupId, name);
|
||||
groupDatabase.updateProfilePicture(groupId, avatarBytes);
|
||||
|
||||
if (!GroupUtil.isMmsGroup(groupId)) {
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
|
||||
} else {
|
||||
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
private static GroupActionResult sendGroupUpdate(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull Set<Address> members,
|
||||
@Nullable String groupName,
|
||||
@Nullable byte[] avatar,
|
||||
@NonNull Set<Address> admins)
|
||||
{
|
||||
Attachment avatarAttachment = null;
|
||||
Address groupAddress = Address.fromSerialized(groupId);
|
||||
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
|
||||
|
||||
List<String> numbers = new LinkedList<>();
|
||||
for (Address member : members) {
|
||||
numbers.add(member.serialize());
|
||||
}
|
||||
|
||||
List<String> adminNumbers = new LinkedList<>();
|
||||
for (Address admin : admins) {
|
||||
adminNumbers.add(admin.serialize());
|
||||
}
|
||||
|
||||
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId)))
|
||||
.setType(GroupContext.Type.UPDATE)
|
||||
.addAllMembers(numbers)
|
||||
.addAllAdmins(adminNumbers);
|
||||
if (groupName != null) groupContextBuilder.setName(groupName);
|
||||
GroupContext groupContext = groupContextBuilder.build();
|
||||
|
||||
if (avatar != null) {
|
||||
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null);
|
||||
}
|
||||
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
||||
private static Set<Address> getMemberAddresses(Collection<Recipient> recipients) {
|
||||
final Set<Address> results = new HashSet<>();
|
||||
for (Recipient recipient : recipients) {
|
||||
results.add(recipient.getAddress());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static class GroupActionResult {
|
||||
private Recipient groupRecipient;
|
||||
private long threadId;
|
||||
|
@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
|
||||
@ -96,11 +97,11 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
||||
final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
||||
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
|
||||
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
|
||||
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE;
|
||||
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
|
||||
|
||||
if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
|
||||
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
|
||||
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
|
||||
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,12 +130,12 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
||||
|
||||
if (!manual && !AttachmentUtil.isAutoDownloadPermitted(context, attachment)) {
|
||||
Log.w(TAG, "Attachment can't be auto downloaded...");
|
||||
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_PENDING);
|
||||
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Downloading push part " + attachmentId);
|
||||
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
|
||||
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
|
||||
|
||||
retrieveAttachment(messageId, attachmentId, attachment);
|
||||
}
|
||||
|
@ -1,169 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.loki.api.utilities.HTTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class AttachmentUploadJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "AttachmentUploadJob";
|
||||
|
||||
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_ROW_ID = "row_id";
|
||||
private static final String KEY_UNIQUE_ID = "unique_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
|
||||
private AttachmentId attachmentId;
|
||||
private Address destination;
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
public AttachmentUploadJob(AttachmentId attachmentId, Address destination) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(10)
|
||||
.build(),
|
||||
attachmentId, destination);
|
||||
}
|
||||
|
||||
private AttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId, Address destination) {
|
||||
super(parameters);
|
||||
this.attachmentId = attachmentId;
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId())
|
||||
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
|
||||
.putString(KEY_DESTINATION, destination.serialize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws Exception {
|
||||
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId);
|
||||
|
||||
if (databaseAttachment == null) {
|
||||
throw new IllegalStateException("Cannot find the specified attachment.");
|
||||
}
|
||||
|
||||
// Only upload attachment if necessary
|
||||
if (databaseAttachment.getUrl().isEmpty()) {
|
||||
final Attachment attachment;
|
||||
try {
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
|
||||
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
|
||||
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), false, new SignalServiceAddress(destination.serialize()));
|
||||
attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), 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.updateAttachmentAfterUploadSucceeded(databaseAttachment.getAttachmentId(), attachment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() { }
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return exception instanceof IOException ||
|
||||
exception instanceof HTTP.HTTPRequestFailedException;
|
||||
}
|
||||
|
||||
private SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withFileName(attachment.getFileName())
|
||||
.withVoiceNote(attachment.isVoiceNote())
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withCaption(attachment.getCaption())
|
||||
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
|
||||
.build();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Couldn't open attachment", ioe);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Attachment scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase,
|
||||
@NonNull MediaConstraints constraints,
|
||||
@NonNull Attachment attachment)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
try {
|
||||
if (constraints.isSatisfied(context, attachment)) {
|
||||
if (MediaUtil.isJpeg(attachment)) {
|
||||
MediaStream stripped = constraints.getResizedMedia(context, attachment);
|
||||
return attachmentDatabase.updateAttachmentData(attachment, stripped);
|
||||
} else {
|
||||
return attachment;
|
||||
}
|
||||
} else if (constraints.canResize(attachment)) {
|
||||
MediaStream resized = constraints.getResizedMedia(context, attachment);
|
||||
return attachmentDatabase.updateAttachmentData(attachment, resized);
|
||||
} else {
|
||||
throw new UndeliverableMessageException("Size constraints could not be met!");
|
||||
}
|
||||
} catch (IOException | MmsException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<AttachmentUploadJob> {
|
||||
@Override
|
||||
public @NonNull AttachmentUploadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new AttachmentUploadJob(parameters, new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)), Address.fromSerialized(data.getString(KEY_DESTINATION)));
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -31,21 +30,12 @@ public final class JobManagerFactories {
|
||||
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
|
||||
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
|
||||
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
|
||||
put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory());
|
||||
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
|
||||
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
||||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
|
||||
put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory());
|
||||
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory());
|
||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
|
||||
}};
|
||||
|
@ -15,6 +15,7 @@ import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||
import org.session.libsignal.metadata.InvalidMetadataMessageException;
|
||||
import org.session.libsignal.metadata.ProtocolInvalidMessageException;
|
||||
import org.session.libsignal.service.api.crypto.SignalServiceCipher;
|
||||
@ -33,8 +34,7 @@ import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
@ -59,15 +59,14 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceContent;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
||||
@ -84,8 +83,6 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
@ -102,8 +99,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
private MessageNotifier messageNotifier;
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
public PushDecryptJob(Context context) {
|
||||
this(context, -1);
|
||||
}
|
||||
@ -232,17 +227,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
handleTextMessage(content, message, smsMessageId, Optional.absent());
|
||||
}
|
||||
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) {
|
||||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
}
|
||||
|
||||
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
|
||||
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
|
||||
}
|
||||
|
||||
if (SessionMetaProtocol.shouldSendDeliveryReceipt(message, Address.fromSerialized(content.getSender()))) {
|
||||
handleNeedsDeliveryReceipt(content, message);
|
||||
}
|
||||
} else if (content.getReceiptMessage().isPresent()) {
|
||||
SignalServiceReceiptMessage message = content.getReceiptMessage().get();
|
||||
|
||||
@ -268,16 +255,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group)
|
||||
{
|
||||
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
@NonNull Optional<Long> smsMessageId)
|
||||
@ -346,7 +323,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
attachments,
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
||||
DistributionTypes.DEFAULT, quote.orNull(),
|
||||
sharedContacts.or(Collections.emptyList()),
|
||||
linkPreviews.or(Collections.emptyList()),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
@ -476,10 +453,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
||||
String syncTarget = message.getSyncTarget().orNull();
|
||||
|
||||
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
|
||||
handleExpirationUpdate(content, message, Optional.absent());
|
||||
}
|
||||
|
||||
Long threadId = null;
|
||||
|
||||
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
|
||||
@ -608,14 +581,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message)
|
||||
{
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new SendDeliveryReceiptJob(Address.fromSerialized(content.getSender()), message.getTimestamp()));
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceReceiptMessage message)
|
||||
|
@ -1,298 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SendMessageResult;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Quote;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "PushGroupSendJob";
|
||||
|
||||
private static final String TAG = PushGroupSendJob.class.getSimpleName();
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_FILTER_ADDRESS = "filter_address";
|
||||
|
||||
private long messageId;
|
||||
private String filterAddress;
|
||||
|
||||
public PushGroupSendJob(long messageId, @NonNull Address destination, @Nullable Address filterAddress) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue(destination.toGroupString())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
messageId, filterAddress);
|
||||
|
||||
}
|
||||
|
||||
private PushGroupSendJob(@NonNull Job.Parameters parameters, long messageId, @Nullable Address filterAddress) {
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.filterAddress = filterAddress == null ? null :filterAddress.toString();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination, @Nullable Address filterAddress) {
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
attachments.addAll(message.getAttachments());
|
||||
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
|
||||
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
|
||||
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
|
||||
|
||||
if (attachmentJobs.isEmpty()) {
|
||||
jobManager.add(new PushGroupSendJob(messageId, destination, filterAddress));
|
||||
} else {
|
||||
jobManager.startChain(attachmentJobs)
|
||||
.then(new PushGroupSendJob(messageId, destination, filterAddress))
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
} catch (NoSuchMessageException | MmsException e) {
|
||||
Log.w(TAG, "Failed to enqueue message.", e);
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putString(KEY_FILTER_ADDRESS, filterAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSending(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushSend()
|
||||
throws MmsException, NoSuchMessageException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
|
||||
List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
log(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(TAG, "Sending message: " + messageId);
|
||||
|
||||
List<Address> targets;
|
||||
|
||||
if (filterAddress != null) targets = Collections.singletonList(Address.fromSerialized(filterAddress));
|
||||
else if (!existingNetworkFailures.isEmpty()) targets = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList();
|
||||
else targets = ClosedGroupsProtocolV2.getMessageDestinations(context, message.getRecipient().getAddress().toGroupString());
|
||||
|
||||
List<SendMessageResult> results = deliver(message, targets);
|
||||
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
|
||||
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList();
|
||||
Set<Address> successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet());
|
||||
List<NetworkFailure> resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successAddresses.contains(failure.getAddress())).toList();
|
||||
List<IdentityKeyMismatch> resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successAddresses.contains(failure.getAddress())).toList();
|
||||
List<SendMessageResult> successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList();
|
||||
|
||||
for (NetworkFailure resolvedFailure : resolvedNetworkFailures) {
|
||||
database.removeFailure(messageId, resolvedFailure);
|
||||
existingNetworkFailures.remove(resolvedFailure);
|
||||
}
|
||||
|
||||
for (IdentityKeyMismatch resolvedIdentity : resolvedIdentityFailures) {
|
||||
database.removeMismatchedIdentity(messageId, resolvedIdentity.getAddress(), resolvedIdentity.getIdentityKey());
|
||||
existingIdentityMismatches.remove(resolvedIdentity);
|
||||
}
|
||||
|
||||
if (!networkFailures.isEmpty()) {
|
||||
database.addFailures(messageId, networkFailures);
|
||||
}
|
||||
|
||||
for (IdentityKeyMismatch mismatch : identityMismatches) {
|
||||
database.addMismatchedIdentity(messageId, mismatch.getAddress(), mismatch.getIdentityKey());
|
||||
}
|
||||
|
||||
for (SendMessageResult success : successes) {
|
||||
DatabaseFactory.getGroupReceiptDatabase(context).setUnidentified(Address.fromSerialized(success.getAddress().getNumber()),
|
||||
messageId,
|
||||
success.getSuccess().isUnidentified());
|
||||
}
|
||||
|
||||
if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) {
|
||||
database.markAsSent(messageId, true);
|
||||
|
||||
markAttachmentsUploaded(messageId, message.getAttachments());
|
||||
|
||||
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
database.markExpireStarted(messageId);
|
||||
ApplicationContext.getInstance(context)
|
||||
.getExpiringMessageManager()
|
||||
.scheduleDeletion(messageId, true, message.getExpiresIn());
|
||||
}
|
||||
} else if (!networkFailures.isEmpty()) {
|
||||
throw new RetryLaterException();
|
||||
} else if (!identityMismatches.isEmpty()) {
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
warn(TAG, e);
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
if (exception instanceof IOException) return true;
|
||||
// Loki - Disable since we have our own retrying
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
}
|
||||
|
||||
private List<SendMessageResult> deliver(OutgoingMediaMessage message, @NonNull List<Address> destinations)
|
||||
throws IOException {
|
||||
|
||||
Address address = message.getRecipient().getAddress();
|
||||
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList();
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess = Stream.of(addresses)
|
||||
.map(a -> Address.fromSerialized(a.getNumber()))
|
||||
.map(a -> Recipient.from(context, a, false))
|
||||
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
|
||||
.toList();
|
||||
|
||||
if (message.isGroup() && address.isClosedGroup()) {
|
||||
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
|
||||
String groupId = address.toGroupString();
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
// Loki - Only send GroupUpdate or GroupQuit messages to closed groups
|
||||
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
|
||||
GroupContext groupContext = groupMessage.getGroupContext();
|
||||
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
|
||||
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar, groupContext.getAdminsList());
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration(message.getRecipient().getExpireMessages())
|
||||
.asGroupMessage(group)
|
||||
.build();
|
||||
|
||||
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
|
||||
} else {
|
||||
SignalServiceDataMessage groupMessage = getDataMessage(address, message).build();
|
||||
|
||||
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) {
|
||||
|
||||
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
|
||||
|
||||
String groupId = address.toGroupString();
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType);
|
||||
return SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.asGroupMessage(group)
|
||||
.withAttachments(attachmentPointers)
|
||||
.withBody(message.getBody())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<PushGroupSendJob> {
|
||||
@Override
|
||||
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
String address = data.getString(KEY_FILTER_ADDRESS);
|
||||
Address filter = address != null ? Address.fromSerialized(data.getString(KEY_FILTER_ADDRESS)) : null;
|
||||
|
||||
return new PushGroupSendJob(parameters, data.getLong(KEY_MESSAGE_ID), filter);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,308 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient.UnidentifiedAccessMode;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SendMessageResult;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "PushMediaSendJob";
|
||||
|
||||
private static final String TAG = PushMediaSendJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private long messageId;
|
||||
private long templateMessageId;
|
||||
private Address destination;
|
||||
|
||||
public PushMediaSendJob(long templateMessageId, long messageId, Address destination) {
|
||||
this(constructParameters(destination), templateMessageId, messageId, destination);
|
||||
}
|
||||
|
||||
private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) {
|
||||
super(parameters);
|
||||
this.templateMessageId = templateMessageId;
|
||||
this.messageId = messageId;
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
|
||||
enqueue(context, jobManager, messageId, messageId, destination);
|
||||
}
|
||||
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) {
|
||||
enqueue(context, jobManager, Collections.singletonList(new PushMediaSendJob(templateMessageId, messageId, destination)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, List<PushMediaSendJob> jobs) {
|
||||
if (jobs.size() == 0) { return; }
|
||||
PushMediaSendJob first = jobs.get(0);
|
||||
long messageId = first.templateMessageId;
|
||||
try {
|
||||
List<AttachmentUploadJob> attachmentJobs = getAttachmentUploadJobs(context, messageId, first.destination);
|
||||
|
||||
if (attachmentJobs.isEmpty()) {
|
||||
for (PushMediaSendJob job : jobs) { jobManager.add(job); }
|
||||
} else {
|
||||
jobManager.startChain(attachmentJobs)
|
||||
.then((List<Job>)(List)jobs)
|
||||
.enqueue();
|
||||
}
|
||||
} catch (NoSuchMessageException | MmsException e) {
|
||||
Log.w(TAG, "Failed to enqueue message.", e);
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<AttachmentUploadJob> getAttachmentUploadJobs(@NonNull Context context, long messageId, @NonNull Address destination)
|
||||
throws NoSuchMessageException, MmsException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
attachments.addAll(message.getAttachments());
|
||||
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
|
||||
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
|
||||
|
||||
return Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder()
|
||||
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putString(KEY_DESTINATION, destination.serialize()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSending(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushSend()
|
||||
throws RetryLaterException, MmsException, NoSuchMessageException,
|
||||
UndeliverableMessageException
|
||||
{
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(templateMessageId);
|
||||
|
||||
if (messageId >= 0 && database.isSent(messageId)) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(TAG, "Sending message: " + messageId);
|
||||
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
boolean unidentified = deliver(message);
|
||||
|
||||
if (messageId >= 0) {
|
||||
database.markAsSent(messageId, true);
|
||||
markAttachmentsUploaded(messageId, message.getAttachments());
|
||||
database.markUnidentified(messageId, unidentified);
|
||||
}
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis());
|
||||
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
||||
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {
|
||||
log(TAG, "Marking recipient as UD-unrestricted following a UD send.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED);
|
||||
} else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) {
|
||||
log(TAG, "Marking recipient as UD-enabled following a UD send.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.ENABLED);
|
||||
} else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) {
|
||||
log(TAG, "Marking recipient as UD-disabled following a non-UD send.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED);
|
||||
}
|
||||
|
||||
if (messageId > 0 && message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
database.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
|
||||
}
|
||||
|
||||
log(TAG, "Sent message: " + messageId);
|
||||
|
||||
} catch (SnodeAPI.Error e) {
|
||||
Log.d("Loki", "Couldn't send message due to error: " + e.getDescription());
|
||||
if (messageId >= 0) {
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
lokiMessageDatabase.setErrorMessage(messageId, e.getDescription());
|
||||
database.markAsSentFailed(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
// Loki - Disable since we have our own retrying
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
if (messageId >= 0) {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deliver(OutgoingMediaMessage message)
|
||||
throws RetryLaterException, UndeliverableMessageException, SnodeAPI.Error
|
||||
{
|
||||
try {
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
SignalServiceAddress address = getPushAddress(recipient.getAddress());
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).toList();
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
|
||||
Optional<UnidentifiedAccess> unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage mediaSelfSendMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withSyncTarget(destination.serialize())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.withQuote(quote.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.build();
|
||||
|
||||
if (userPublicKey == address.getNumber()) {
|
||||
// Loki - Device link messages don't go through here
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage, true);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
return result.getSuccess().isUnidentified();
|
||||
}
|
||||
} else {
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage, false);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
boolean isUnidentified = result.getSuccess().isUnidentified();
|
||||
|
||||
try {
|
||||
// send to ourselves to sync multi-device
|
||||
Optional<UnidentifiedAccess> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage, true);
|
||||
if (selfSendResult.getLokiAPIError() != null) {
|
||||
throw selfSendResult.getLokiAPIError();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("Loki", "Error sending message to ourselves", e);
|
||||
}
|
||||
|
||||
return isUnidentified;
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
warn(TAG, e);
|
||||
throw new UndeliverableMessageException(e);
|
||||
} catch (IOException e) {
|
||||
warn(TAG, e);
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<PushMediaSendJob> {
|
||||
@Override
|
||||
public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
|
||||
long messageID = data.getLong(KEY_MESSAGE_ID);
|
||||
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
|
||||
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class PushSendJob extends SendJob {
|
||||
|
||||
private static final String TAG = PushSendJob.class.getSimpleName();
|
||||
|
||||
protected PushSendJob(Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
protected static Job.Parameters constructParameters(Address destination) {
|
||||
return new Parameters.Builder()
|
||||
.setQueue(destination.serialize())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void onSend() throws Exception {
|
||||
onPushSend();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetry() {
|
||||
super.onRetry();
|
||||
Log.i(TAG, "onRetry()");
|
||||
}
|
||||
|
||||
protected Optional<byte[]> getProfileKey(@NonNull Recipient recipient) {
|
||||
if (!recipient.resolve().isSystemContact() && !recipient.resolve().isProfileSharing()) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(ProfileKeyUtil.getProfileKey(context));
|
||||
}
|
||||
|
||||
protected SignalServiceAddress getPushAddress(Address address) {
|
||||
String relay = null;
|
||||
return new SignalServiceAddress(address.toString(), Optional.fromNullable(relay));
|
||||
}
|
||||
|
||||
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
.withLength(attachment.getSize())
|
||||
.withFileName(attachment.getFileName())
|
||||
.withVoiceNote(attachment.isVoiceNote())
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withCaption(attachment.getCaption())
|
||||
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
|
||||
.build();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Couldn't open attachment", ioe);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected @NonNull List<SignalServiceAttachment> getAttachmentPointersFor(List<Attachment> attachments) {
|
||||
return Stream.of(attachments).map(this::getAttachmentPointerFor).filter(a -> a != null).toList();
|
||||
}
|
||||
|
||||
protected @Nullable SignalServiceAttachment getAttachmentPointerFor(Attachment attachment) {
|
||||
if (TextUtils.isEmpty(attachment.getLocation())) {
|
||||
Log.w(TAG, "empty content id");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(attachment.getKey())) {
|
||||
Log.w(TAG, "empty encrypted key");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
long id = Long.parseLong(attachment.getLocation());
|
||||
byte[] key = Base64.decode(attachment.getKey());
|
||||
|
||||
return new SignalServiceAttachmentPointer(id,
|
||||
attachment.getContentType(),
|
||||
key,
|
||||
Optional.of(Util.toIntExact(attachment.getSize())),
|
||||
Optional.absent(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
Optional.fromNullable(attachment.getDigest()),
|
||||
Optional.fromNullable(attachment.getFileName()),
|
||||
attachment.isVoiceNote(),
|
||||
Optional.fromNullable(attachment.getCaption()), attachment.getUrl());
|
||||
} catch (IOException | ArithmeticException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected static void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
||||
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
if (threadId != -1 && recipient != null) {
|
||||
ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMediaMessage message) {
|
||||
if (message.getOutgoingQuote() == null) return Optional.absent();
|
||||
|
||||
long quoteId = message.getOutgoingQuote().getId();
|
||||
String quoteBody = message.getOutgoingQuote().getText();
|
||||
Address quoteAuthor = message.getOutgoingQuote().getAuthor();
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
|
||||
|
||||
for (Attachment attachment : message.getOutgoingQuote().getAttachments()) {
|
||||
BitmapUtil.ScaleResult thumbnailData = null;
|
||||
SignalServiceAttachment thumbnail = null;
|
||||
String thumbnailType = MediaTypes.IMAGE_JPEG;
|
||||
|
||||
try {
|
||||
if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) {
|
||||
Bitmap.CompressFormat format = BitmapUtil.getCompressFormatForContentType(attachment.getContentType());
|
||||
|
||||
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024, format);
|
||||
thumbnailType = attachment.getContentType();
|
||||
} else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) {
|
||||
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
|
||||
}
|
||||
|
||||
if (thumbnailData != null) {
|
||||
thumbnail = SignalServiceAttachment.newStreamBuilder()
|
||||
.withContentType(thumbnailType)
|
||||
.withWidth(thumbnailData.getWidth())
|
||||
.withHeight(thumbnailData.getHeight())
|
||||
.withLength(thumbnailData.getBitmap().length)
|
||||
.withStream(new ByteArrayInputStream(thumbnailData.getBitmap()))
|
||||
.build();
|
||||
}
|
||||
|
||||
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
|
||||
attachment.getFileName(),
|
||||
thumbnail));
|
||||
} catch (BitmapDecodingException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, new SignalServiceAddress(quoteAuthor.serialize()), quoteBody, quoteAttachments));
|
||||
}
|
||||
|
||||
List<SharedContact> getSharedContactsFor(OutgoingMediaMessage mediaMessage) {
|
||||
List<SharedContact> sharedContacts = new LinkedList<>();
|
||||
|
||||
for (Contact contact : mediaMessage.getSharedContacts()) {
|
||||
SharedContact.Builder builder = ContactModelMapper.localToRemoteBuilder(contact);
|
||||
SharedContact.Avatar avatar = null;
|
||||
|
||||
if (contact.getAvatar() != null && contact.getAvatar().getAttachment() != null) {
|
||||
avatar = SharedContact.Avatar.newBuilder().withAttachment(getAttachmentFor(contact.getAvatarAttachment()))
|
||||
.withProfileFlag(contact.getAvatar().isProfile())
|
||||
.build();
|
||||
}
|
||||
|
||||
builder.setAvatar(avatar);
|
||||
sharedContacts.add(builder.build());
|
||||
}
|
||||
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
List<Preview> getPreviewsFor(OutgoingMediaMessage mediaMessage) {
|
||||
return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> {
|
||||
SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null;
|
||||
return new Preview(lp.getUrl(), lp.getTitle(), Optional.fromNullable(attachment));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
protected abstract void onPushSend() throws Exception;
|
||||
}
|
@ -1,241 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient.UnidentifiedAccessMode;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SendMessageResult;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "PushTextSendJob";
|
||||
|
||||
private static final String TAG = PushTextSendJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private long messageId;
|
||||
private long templateMessageId;
|
||||
private Address destination;
|
||||
|
||||
public PushTextSendJob(long messageId, Address destination) {
|
||||
this(messageId, messageId, destination);
|
||||
}
|
||||
|
||||
public PushTextSendJob(long templateMessageId, long messageId, Address destination) {
|
||||
this(constructParameters(destination), templateMessageId, messageId, destination);
|
||||
}
|
||||
|
||||
private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) {
|
||||
super(parameters);
|
||||
this.templateMessageId = templateMessageId;
|
||||
this.messageId = messageId;
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder()
|
||||
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putString(KEY_DESTINATION, destination.serialize()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
if (messageId >= 0) {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushSend() throws NoSuchMessageException, RetryLaterException {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(templateMessageId);
|
||||
|
||||
Recipient recordRecipient = record.getRecipient().resolve();
|
||||
boolean hasSameDestination = destination.equals(recordRecipient.getAddress());
|
||||
|
||||
if (hasSameDestination && !record.isPending() && !record.isFailed()) {
|
||||
Log.d("Loki", "Message with ID: " + templateMessageId + " was already sent; ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(TAG, "Sending message: " + templateMessageId + (hasSameDestination ? "" : "to a linked device."));
|
||||
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
boolean unidentified = deliver(record);
|
||||
|
||||
if (messageId >= 0) {
|
||||
database.markAsSent(messageId, true);
|
||||
database.markUnidentified(messageId, unidentified);
|
||||
}
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent());
|
||||
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
||||
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {
|
||||
log(TAG, "Marking recipient as UD-unrestricted following a UD send.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED);
|
||||
} else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) {
|
||||
log(TAG, "Marking recipient as UD-enabled following a UD send.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.ENABLED);
|
||||
} else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) {
|
||||
log(TAG, "Marking recipient as UD-disabled following a non-UD send.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED);
|
||||
}
|
||||
|
||||
if (record.getExpiresIn() > 0 && messageId >= 0) {
|
||||
database.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
|
||||
}
|
||||
|
||||
log(TAG, "Sent message: " + templateMessageId + (hasSameDestination ? "" : "to a linked device."));
|
||||
|
||||
} catch (SnodeAPI.Error e) {
|
||||
Log.d("Loki", "Couldn't send message due to error: " + e.getDescription());
|
||||
if (messageId >= 0) {
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
lokiMessageDatabase.setErrorMessage(record.getId(), e.getDescription());
|
||||
database.markAsSentFailed(record.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
// Loki - Disable since we have our own retrying
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
if (messageId >= 0) {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
|
||||
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
if (threadId != -1 && recipient != null) {
|
||||
ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deliver(SmsMessageRecord message) throws RetryLaterException, SnodeAPI.Error
|
||||
{
|
||||
try {
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
SignalServiceAddress address = getPushAddress(recipient.getAddress());
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
|
||||
Optional<byte[]> profileKey = getProfileKey(recipient);
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent());
|
||||
|
||||
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getDateSent())
|
||||
.withBody(message.getBody())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getDateSent())
|
||||
.withBody(message.getBody())
|
||||
.withSyncTarget(destination.serialize())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.build();
|
||||
|
||||
if (userPublicKey.equals(address.getNumber())) {
|
||||
// Loki - Device link messages don't go through here
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, true);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
return result.getSuccess().isUnidentified();
|
||||
}
|
||||
} else {
|
||||
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, false);
|
||||
if (result.getLokiAPIError() != null) {
|
||||
throw result.getLokiAPIError();
|
||||
} else {
|
||||
boolean isUnidentified = result.getSuccess().isUnidentified();
|
||||
|
||||
try {
|
||||
// send to ourselves to sync multi-device
|
||||
Optional<UnidentifiedAccess> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage, true);
|
||||
if (selfSendResult.getLokiAPIError() != null) {
|
||||
throw selfSendResult.getLokiAPIError();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("Loki", "Error sending message to ourselves", e);
|
||||
}
|
||||
return isUnidentified;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
warn(TAG, "Failure", e);
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<PushTextSendJob> {
|
||||
@Override
|
||||
public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
|
||||
long messageID = data.getLong(KEY_MESSAGE_ID);
|
||||
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
|
||||
return new PushTextSendJob(parameters, templateMessageID, messageID, destination);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup.Type;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class RequestGroupInfoJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "RequestGroupInfoJob";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = RequestGroupInfoJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_SOURCE = "source";
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private String source;
|
||||
private byte[] groupId;
|
||||
|
||||
public RequestGroupInfoJob(@NonNull String source, @NonNull byte[] groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
source,
|
||||
groupId);
|
||||
|
||||
}
|
||||
|
||||
private RequestGroupInfoJob(@NonNull Job.Parameters parameters, @NonNull String source, @NonNull byte[] groupId) {
|
||||
super(parameters);
|
||||
|
||||
this.source = source;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putString(KEY_SOURCE, source)
|
||||
.putString(KEY_GROUP_ID, GroupUtil.getEncodedClosedGroupID(groupId))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
|
||||
.withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
|
||||
.asGroupMessage(group)
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
|
||||
messageSender.sendMessage(0, new SignalServiceAddress(source),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromExternal(context, source), false)),
|
||||
message, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RequestGroupInfoJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull RequestGroupInfoJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new RequestGroupInfoJob(parameters,
|
||||
data.getString(KEY_SOURCE),
|
||||
GroupUtil.getDecodedGroupIDAsData(data.getString(KEY_GROUP_ID)));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class SendDeliveryReceiptJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "SendDeliveryReceiptJob";
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_TIMESTAMP = "timestamp";
|
||||
|
||||
private static final String TAG = SendReadReceiptJob.class.getSimpleName();
|
||||
|
||||
@Inject
|
||||
transient SignalServiceMessageSender messageSender;
|
||||
|
||||
private String address;
|
||||
private long messageId;
|
||||
private long timestamp;
|
||||
|
||||
public SendDeliveryReceiptJob(@NonNull Address address, long messageId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
address,
|
||||
messageId,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private SendDeliveryReceiptJob(@NonNull Job.Parameters parameters,
|
||||
@NonNull Address address,
|
||||
long messageId,
|
||||
long timestamp)
|
||||
{
|
||||
super(parameters);
|
||||
|
||||
this.address = address.serialize();
|
||||
this.messageId = messageId;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putString(KEY_ADDRESS, address)
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putLong(KEY_TIMESTAMP, timestamp)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
Log.d("Loki", "Sending delivery receipt.");
|
||||
SignalServiceAddress remoteAddress = new SignalServiceAddress(address);
|
||||
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
|
||||
Collections.singletonList(messageId),
|
||||
timestamp);
|
||||
|
||||
messageSender.sendReceipt(remoteAddress,
|
||||
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)),
|
||||
receiptMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
if (e instanceof PushNetworkException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "Failed to send delivery receipt to: " + address);
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<SendDeliveryReceiptJob> {
|
||||
@Override
|
||||
public @NonNull SendDeliveryReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new SendDeliveryReceiptJob(parameters,
|
||||
Address.fromSerialized(data.getString(KEY_ADDRESS)),
|
||||
data.getLong(KEY_MESSAGE_ID),
|
||||
data.getLong(KEY_TIMESTAMP));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class SendJob extends BaseJob {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = SendJob.class.getSimpleName();
|
||||
|
||||
public SendJob(Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onRun() throws Exception {
|
||||
Log.i(TAG, "Starting message send attempt");
|
||||
onSend();
|
||||
Log.i(TAG, "Message send completed");
|
||||
}
|
||||
|
||||
protected abstract void onSend() throws Exception;
|
||||
|
||||
protected void markAttachmentsUploaded(long messageId, @NonNull List<Attachment> attachments) {
|
||||
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
database.markAttachmentUploaded(messageId, attachment);
|
||||
}
|
||||
}
|
||||
|
||||
protected void log(@NonNull String tag, @NonNull String message) {
|
||||
Log.i(tag, JobLogger.format(this, message));
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @NonNull String message) {
|
||||
warn(tag, message, null);
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @Nullable Throwable t) {
|
||||
warn(tag, "", t);
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @NonNull String message, @Nullable Throwable t) {
|
||||
Log.w(tag, JobLogger.format(this, message), t);
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class SendReadReceiptJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "SendReadReceiptJob";
|
||||
|
||||
private static final String TAG = SendReadReceiptJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
private static final String KEY_MESSAGE_IDS = "message_ids";
|
||||
private static final String KEY_TIMESTAMP = "timestamp";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private String address;
|
||||
private List<Long> messageIds;
|
||||
private long timestamp;
|
||||
|
||||
public SendReadReceiptJob(Address address, List<Long> messageIds) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
address,
|
||||
messageIds,
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private SendReadReceiptJob(@NonNull Job.Parameters parameters,
|
||||
@NonNull Address address,
|
||||
@NonNull List<Long> messageIds,
|
||||
long timestamp)
|
||||
{
|
||||
super(parameters);
|
||||
|
||||
this.address = address.serialize();
|
||||
this.messageIds = messageIds;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
long[] ids = new long[messageIds.size()];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
ids[i] = messageIds.get(i);
|
||||
}
|
||||
|
||||
return new Data.Builder().putString(KEY_ADDRESS, address)
|
||||
.putLongArray(KEY_MESSAGE_IDS, ids)
|
||||
.putLong(KEY_TIMESTAMP, timestamp)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return;
|
||||
|
||||
SignalServiceAddress remoteAddress = new SignalServiceAddress(address);
|
||||
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp);
|
||||
|
||||
messageSender.sendReceipt(remoteAddress,
|
||||
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)),
|
||||
receiptMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
if (e instanceof PushNetworkException) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "Failed to send read receipts to: " + address);
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<SendReadReceiptJob> {
|
||||
@Override
|
||||
public @NonNull SendReadReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
Address address = Address.fromSerialized(data.getString(KEY_ADDRESS));
|
||||
long timestamp = data.getLong(KEY_TIMESTAMP);
|
||||
long[] ids = data.hasLongArray(KEY_MESSAGE_IDS) ? data.getLongArray(KEY_MESSAGE_IDS) : new long[0];
|
||||
List<Long> messageIds = new ArrayList<>(ids.length);
|
||||
|
||||
for (long id : ids) {
|
||||
messageIds.add(id);
|
||||
}
|
||||
|
||||
return new SendReadReceiptJob(parameters, address, messageIds, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage.Action;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class TypingSendJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "TypingSendJob";
|
||||
|
||||
private static final String TAG = TypingSendJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_THREAD_ID = "thread_id";
|
||||
private static final String KEY_TYPING = "typing";
|
||||
|
||||
private long threadId;
|
||||
private boolean typing;
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
public TypingSendJob(long threadId, boolean typing) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("TYPING_" + threadId)
|
||||
.setMaxAttempts(1)
|
||||
.setLifespan(TimeUnit.SECONDS.toMillis(5))
|
||||
.build(),
|
||||
threadId,
|
||||
typing);
|
||||
}
|
||||
|
||||
private TypingSendJob(@NonNull Job.Parameters parameters, long threadId, boolean typing) {
|
||||
super(parameters);
|
||||
|
||||
this.threadId = threadId;
|
||||
this.typing = typing;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_THREAD_ID, threadId)
|
||||
.putBoolean(KEY_TYPING, typing)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws Exception {
|
||||
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sending typing " + (typing ? "started" : "stopped") + " for thread " + threadId);
|
||||
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
if (recipient == null) {
|
||||
throw new IllegalStateException("Tried to send a typing indicator to a non-existent thread.");
|
||||
}
|
||||
|
||||
List<Recipient> recipients = Collections.singletonList(recipient);
|
||||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.getAddress().toGroupString(), false);
|
||||
}
|
||||
|
||||
List<SignalServiceAddress> addresses = Stream.of(recipients).map(r -> new SignalServiceAddress(r.getAddress().serialize())).toList();
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess = Stream.of(recipients).map(r -> UnidentifiedAccessUtil.getAccessFor(context, r)).toList();
|
||||
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis());
|
||||
|
||||
messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<TypingSendJob> {
|
||||
@Override
|
||||
public @NonNull TypingSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new TypingSendJob(parameters, data.getLong(KEY_THREAD_ID), data.getBoolean(KEY_TYPING));
|
||||
}
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.gms.common.util.IOUtils;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.net.CallRequestController;
|
||||
@ -192,7 +192,7 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
return Optional.of(new UriAttachment(uri,
|
||||
uri,
|
||||
contentType,
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
|
||||
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
||||
bytes.length,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
|
@ -43,13 +43,13 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
return linkPreviewState.getValue() != null && linkPreviewState.getValue().getLinkPreview().isPresent();
|
||||
}
|
||||
|
||||
public @NonNull List<LinkPreview> getActiveLinkPreviews() {
|
||||
public Optional<LinkPreview> getActiveLinkPreview() {
|
||||
final LinkPreviewState state = linkPreviewState.getValue();
|
||||
|
||||
if (state == null || !state.getLinkPreview().isPresent()) {
|
||||
return Collections.emptyList();
|
||||
return Optional.absent();
|
||||
} else {
|
||||
return Collections.singletonList(state.getLinkPreview().get());
|
||||
return state.getLinkPreview();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,18 +13,18 @@ import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_create_closed_group.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.DistributionTypes
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
|
||||
//TODO Refactor to avoid using kotlinx.android.synthetic
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
@ -104,13 +104,13 @@ 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() >= ClosedGroupsProtocolV2.groupSizeLimit) { // Minus one because we're going to include self later
|
||||
if (selectedMembers.count() >= 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)!!
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
ClosedGroupsProtocolV2.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
@ -128,7 +128,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.DistributionTypes
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
@ -69,7 +70,7 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
||||
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
@ -260,7 +260,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
val maxGroupMembers = if (isClosedGroup) ClosedGroupsProtocolV2.groupSizeLimit else legacyGroupSizeLimit
|
||||
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
|
||||
if (members.size >= maxGroupMembers) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
@ -277,17 +277,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
||||
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!)
|
||||
MessageSender.explicitLeave(groupPublicKey!!)
|
||||
} else {
|
||||
task {
|
||||
if (hasNameChanged) {
|
||||
ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity, groupPublicKey!!, name)
|
||||
MessageSender.explicitNameChange(groupPublicKey!!, name)
|
||||
}
|
||||
members.filterNot { it in originalMembers }.let { adds ->
|
||||
if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
|
||||
if (adds.isNotEmpty()) MessageSender.explicitAddMembers(groupPublicKey!!, adds.map { it.address.serialize() })
|
||||
}
|
||||
originalMembers.filterNot { it in members }.let { removes ->
|
||||
if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
|
||||
if (removes.isNotEmpty()) MessageSender.explicitRemoveMembers(groupPublicKey!!, removes.map { it.address.serialize() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -296,13 +296,11 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
isLoading = false
|
||||
finish()
|
||||
}.failUi { exception ->
|
||||
val message = if (exception is ClosedGroupsProtocolV2.Error) exception.description else "An error occurred"
|
||||
val message = if (exception is MessageSender.Error) exception.description else "An error occurred"
|
||||
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
|
||||
loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
}
|
||||
} else {
|
||||
GroupManager.updateGroup(this, groupID, members, null, name, admins)
|
||||
}
|
||||
}
|
||||
}
|
@ -26,13 +26,11 @@ import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.ProfilePictureModifiedEvent
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsession.utilities.*
|
||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
@ -42,7 +40,6 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.dialogs.*
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.loki.views.ConversationView
|
||||
@ -344,7 +341,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
isClosedGroup = false
|
||||
}
|
||||
if (isClosedGroup) {
|
||||
ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!)
|
||||
MessageSender.explicitLeave(groupPublicKey!!)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
|
@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.loki.activities
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.views.FakeChatView
|
||||
|
@ -35,7 +35,7 @@ import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
|
@ -17,6 +17,7 @@ import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.DistributionTypes
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
@ -60,7 +61,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import org.session.libsignal.service.loki.crypto.MnemonicCodec
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
|
@ -23,7 +23,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.KeyHelper
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import java.util.*
|
||||
|
@ -12,7 +12,7 @@ import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
@ -7,12 +7,11 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
|
@ -14,36 +14,12 @@ import org.session.libsignal.service.loki.api.crypto.SessionProtocol
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
class SessionProtocolImpl(private val context: Context) : SessionProtocol {
|
||||
|
||||
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
|
||||
|
||||
override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
|
||||
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
|
||||
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
try {
|
||||
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't sign message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.SigningFailed
|
||||
}
|
||||
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
|
||||
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
|
||||
try {
|
||||
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
|
||||
throw SessionProtocol.Exception.EncryptionFailed
|
||||
}
|
||||
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
|
||||
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
|
||||
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
|
@ -13,7 +13,7 @@ import org.session.libsignal.service.loki.api.Snode
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
||||
|
@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.loki.utilities.*
|
||||
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||
|
||||
companion object {
|
||||
private val sessionJobTable = "loki_thread_session_reset_database"
|
||||
private val sessionJobTable = "session_job_database"
|
||||
val jobID = "job_id"
|
||||
val jobType = "job_type"
|
||||
val failureCount = "failure_count"
|
||||
@ -22,12 +22,12 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
|
||||
fun persistJob(job: Job) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val contentValues = ContentValues(2)
|
||||
val contentValues = ContentValues(4)
|
||||
contentValues.put(jobID, job.id)
|
||||
contentValues.put(jobType, job.getFactoryKey())
|
||||
contentValues.put(failureCount, job.failureCount)
|
||||
contentValues.put(serializedData, SessionJobHelper.dataSerializer.serialize(job.serialize()))
|
||||
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID.toString()))
|
||||
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID))
|
||||
}
|
||||
|
||||
fun markJobAsSucceeded(job: Job) {
|
||||
@ -51,7 +51,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
database.getAll(sessionJobTable, "$jobType = ?", arrayOf(AttachmentUploadJob.KEY)) { cursor ->
|
||||
result.add(jobFromCursor(cursor) as AttachmentUploadJob)
|
||||
}
|
||||
return result.first { job -> job.attachmentID == attachmentID }
|
||||
return result.firstOrNull { job -> job.attachmentID == attachmentID }
|
||||
}
|
||||
|
||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {
|
||||
|
@ -11,7 +11,7 @@ import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
|
||||
import org.session.libsession.utilities.KeyPairUtilities
|
||||
|
||||
class ClearAllDataDialog : DialogFragment() {
|
||||
|
||||
|
@ -13,7 +13,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.session.libsignal.service.loki.crypto.MnemonicCodec
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
|
||||
|
@ -1,253 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.session.libsession.messaging.jobs.Data
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.TTLUtilities
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters,
|
||||
private val destination: String,
|
||||
private val kind: Kind,
|
||||
private val sentTime: Long) : BaseJob(parameters) {
|
||||
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
class Update(val name: String, val members: Collection<ByteArray>) : Kind()
|
||||
object Leave : Kind()
|
||||
class RemoveMembers(val members: Collection<ByteArray>) : Kind()
|
||||
class AddMembers(val members: Collection<ByteArray>) : Kind()
|
||||
class NameChange(val name: String) : Kind()
|
||||
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY = "ClosedGroupUpdateMessageSendJobV2"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper {
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(20)
|
||||
.build(),
|
||||
destination,
|
||||
kind,
|
||||
sentTime)
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder()
|
||||
builder.putString("destination", destination)
|
||||
builder.putLong("sentTime", sentTime)
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
builder.putString("kind", "New")
|
||||
builder.putByteArray("publicKey", kind.publicKey)
|
||||
builder.putString("name", kind.name)
|
||||
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize())
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("admins", admins)
|
||||
}
|
||||
is Kind.Update -> {
|
||||
builder.putString("kind", "Update")
|
||||
builder.putString("name", kind.name)
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
builder.putString("kind", "RemoveMembers")
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
Kind.Leave -> {
|
||||
builder.putString("kind", "Leave")
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
builder.putString("kind", "AddMembers")
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
builder.putString("kind", "NameChange")
|
||||
builder.putString("name", kind.name)
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
builder.putString("kind", "EncryptionKeyPair")
|
||||
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
|
||||
builder.putString("wrappers", wrappers)
|
||||
builder.putString("targetUser", kind.targetUser)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<ClosedGroupUpdateMessageSendJobV2> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
|
||||
val destination = data.getString("destination")
|
||||
val rawKind = data.getString("kind")
|
||||
val sentTime = data.getLong("sentTime")
|
||||
val kind: Kind
|
||||
when (rawKind) {
|
||||
"New" -> {
|
||||
val publicKey = data.getByteArray("publicKey")
|
||||
val name = data.getString("name")
|
||||
val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey")
|
||||
val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey")
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey))
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
"Update" -> {
|
||||
val name = data.getString("name")
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.Update(name, members)
|
||||
}
|
||||
"EncryptionKeyPair" -> {
|
||||
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
|
||||
val targetUser = data.getString("targetUser")
|
||||
kind = Kind.EncryptionKeyPair(wrappers, targetUser)
|
||||
}
|
||||
"RemoveMembers" -> {
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.RemoveMembers(members)
|
||||
}
|
||||
"AddMembers" -> {
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.AddMembers(members)
|
||||
}
|
||||
"NameChange" -> {
|
||||
val name = data.getString("name")
|
||||
kind = Kind.NameChange(name)
|
||||
}
|
||||
"Leave" -> {
|
||||
kind = Kind.Leave
|
||||
}
|
||||
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||
}
|
||||
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val sendDestination = if (kind is Kind.EncryptionKeyPair && kind.targetUser != null) {
|
||||
kind.targetUser
|
||||
} else {
|
||||
destination
|
||||
}
|
||||
val contentMessage = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage = DataMessage.newBuilder()
|
||||
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
|
||||
closedGroupUpdate.name = kind.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.Update -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.UPDATE
|
||||
closedGroupUpdate.name = kind.name
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
if (kind.targetUser != null) {
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(destination))
|
||||
}
|
||||
}
|
||||
Kind.Leave -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
closedGroupUpdate.name = kind.name
|
||||
}
|
||||
}
|
||||
dataMessage.closedGroupControlMessage = closedGroupUpdate.build()
|
||||
contentMessage.dataMessage = dataMessage.build()
|
||||
val serializedContentMessage = contentMessage.build().toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(sendDestination)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(sendDestination), false)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
val ttl = when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000
|
||||
else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
|
||||
}
|
||||
try {
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess,
|
||||
sentTime, serializedContentMessage, false, ttl,
|
||||
true, false, false, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCanceled() { }
|
||||
}
|
@ -3,32 +3,25 @@ package org.thoughtcrime.securesms.loki.protocol
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
|
||||
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
@ -37,258 +30,8 @@ import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key")
|
||||
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
|
||||
object InvalidUpdate : Error("Invalid group update.")
|
||||
}
|
||||
|
||||
fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Generate the group's public key
|
||||
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
|
||||
val sentTime = System.currentTimeMillis()
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
// Create the group
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }), System.currentTimeMillis())
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Send a closed group update message to all members individually
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately to make all of this sync
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
deferred.resolve(groupID)
|
||||
}
|
||||
// Return
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @param notifyUser Inserts an outgoing info message for the user's leave message, useful to set `false` if
|
||||
* you are exiting asynchronously and deleting the thread from [HomeActivity][org.thoughtcrime.securesms.loki.activities.HomeActivity.deleteConversation]
|
||||
*/
|
||||
@JvmStatic @JvmOverloads
|
||||
fun explicitLeave(context: Context, groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
// Send the update to the group
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.QUIT
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
if (notifyUser) {
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List<String>) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
|
||||
val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) }
|
||||
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() ?: Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)).orNull()
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
|
||||
ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List<String>) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
|
||||
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
throw Error.InvalidUpdate
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, newName)
|
||||
}
|
||||
|
||||
private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection<String>) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
|
||||
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
|
||||
return
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// replace call will not succeed if no value already set
|
||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
do {
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
|
||||
// Store it * after * having sent out the message to the group
|
||||
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
|
||||
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true) {
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.mapNotNull { publicKey ->
|
||||
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
|
||||
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
|
||||
}
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis())
|
||||
if (force) {
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
} else {
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
|
||||
@ -361,11 +104,11 @@ object ClosedGroupsProtocolV2 {
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user (if we didn't make the group)
|
||||
if (userPublicKey != senderPublicKey) {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
} else if (prevGroup == null) {
|
||||
// only notify if we created this group
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
@ -410,7 +153,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val (contextType, signalType) =
|
||||
@ -418,9 +161,9 @@ object ClosedGroupsProtocolV2 {
|
||||
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -448,9 +191,9 @@ object ClosedGroupsProtocolV2 {
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
@ -460,7 +203,7 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
for (user in updateMembers) {
|
||||
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -487,9 +230,9 @@ object ClosedGroupsProtocolV2 {
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,15 +266,15 @@ object ClosedGroupsProtocolV2 {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
// Notify user
|
||||
if (userLeft) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -598,59 +341,6 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
}
|
||||
|
||||
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type0)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
|
||||
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long,
|
||||
sentTimestamp: Long) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTimestamp)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getMessageDestinations(context: Context, groupID: String): List<Address> {
|
||||
return if (GroupUtil.isOpenGroup(groupID)) {
|
||||
listOf(Address.fromSerialized(groupID))
|
||||
} else {
|
||||
var groupPublicKey: String? = null
|
||||
try {
|
||||
groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() // TODO: The toHexString() here might be unnecessary
|
||||
} catch (exception: Exception) {
|
||||
// Do nothing
|
||||
}
|
||||
if (groupPublicKey != null && DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)) {
|
||||
listOf(Address.fromSerialized(groupPublicKey))
|
||||
} else {
|
||||
DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Deprecated
|
||||
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
@ -685,7 +375,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
@ -700,9 +390,9 @@ object ClosedGroupsProtocolV2 {
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
@ -3,30 +3,26 @@ package org.thoughtcrime.securesms.loki.protocol
|
||||
import android.content.Context
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import java.util.*
|
||||
|
||||
object MultiDeviceProtocol {
|
||||
|
||||
// TODO: refactor this to use new message sending job
|
||||
@JvmStatic
|
||||
fun syncConfigurationIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
@ -39,22 +35,10 @@ object MultiDeviceProtocol {
|
||||
ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey)
|
||||
}
|
||||
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
|
||||
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(userPublicKey)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess,
|
||||
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(),
|
||||
true, false, false, Optional.absent())
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||
}
|
||||
MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey))
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
|
||||
}
|
||||
|
||||
// TODO: refactor this to use new message sending job
|
||||
fun forceSyncConfigurationNowIfNeeded(context: Context) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||
val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
|
||||
@ -63,19 +47,8 @@ object MultiDeviceProtocol {
|
||||
ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey)
|
||||
}
|
||||
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
|
||||
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(userPublicKey)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
try {
|
||||
messageSender.sendMessage(0, address, udAccess,
|
||||
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(),
|
||||
true, false, false, Optional.absent())
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send configuration message due to error: $e.")
|
||||
}
|
||||
MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))
|
||||
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
// TODO: remove this after we migrate to new message receiving pipeline
|
||||
|
@ -20,6 +20,7 @@ import network.loki.messenger.R
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
@ -152,7 +153,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
downloadProgress.progress = 0
|
||||
}
|
||||
}
|
||||
(showControls && audio.transferState == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) -> {
|
||||
(showControls && audio.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) -> {
|
||||
controlToggle.displayQuick(downloadProgress)
|
||||
seekBar.isEnabled = false
|
||||
downloadProgress.isIndeterminate = true
|
||||
|
@ -145,14 +145,14 @@ class ProfilePictureView : RelativeLayout {
|
||||
|
||||
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
|
||||
if (publicKey.isNotEmpty()) {
|
||||
if (imagesCached.contains(publicKey)) return
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||
if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return
|
||||
val signalProfilePicture = recipient.contactPhoto
|
||||
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
|
||||
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
|
||||
glide.clear(imageView)
|
||||
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
|
||||
imagesCached.add(publicKey)
|
||||
imagesCached.add(recipient.profileAvatar.orEmpty())
|
||||
} else {
|
||||
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
|
||||
glide.clear(imageView)
|
||||
@ -162,7 +162,7 @@ class ProfilePictureView : RelativeLayout {
|
||||
publicKey,
|
||||
displayName
|
||||
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||
imagesCached.add(publicKey)
|
||||
imagesCached.add(recipient.profileAvatar.orEmpty())
|
||||
}
|
||||
} else {
|
||||
imageView.setImageDrawable(null)
|
||||
|
@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
import org.session.libsession.utilities.MediaTypes;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
@ -38,7 +39,7 @@ public class AudioSlide extends Slide {
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
|
||||
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null));
|
||||
super(context, new UriAttachment(uri, null, contentType, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Attachment attachment) {
|
||||
|
@ -23,7 +23,7 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
@ -135,8 +135,8 @@ public abstract class Slide {
|
||||
}
|
||||
|
||||
public boolean isPendingDownload() {
|
||||
return getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_FAILED ||
|
||||
getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
|
||||
return getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED ||
|
||||
getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING;
|
||||
}
|
||||
|
||||
public int getTransferState() {
|
||||
@ -172,7 +172,7 @@ public abstract class Slide {
|
||||
return new UriAttachment(uri,
|
||||
hasThumbnail ? uri : null,
|
||||
resolvedType,
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
|
||||
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
|
@ -25,18 +25,19 @@ import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.app.RemoteInput;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -72,17 +73,29 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
|
||||
|
||||
long replyThreadId;
|
||||
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
if (threadId == -1) {
|
||||
replyThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
} else {
|
||||
replyThreadId = threadId;
|
||||
}
|
||||
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setText(responseText.toString());
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
MessageSender.send(message, recipient.getAddress());
|
||||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
|
||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||
try {
|
||||
DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, replyThreadId, false, null);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
} else {
|
||||
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
|
||||
OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId);
|
||||
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
|
||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
||||
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null);
|
||||
}
|
||||
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId, true);
|
||||
|
@ -11,13 +11,15 @@ import androidx.core.app.NotificationManagerCompat;
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.messages.control.ReadReceipt;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
@ -25,7 +27,6 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
@ -68,8 +69,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
public static void process(@NonNull Context context, @NonNull List<MarkedMessageInfo> markedReadMessages) {
|
||||
if (markedReadMessages.isEmpty()) return;
|
||||
|
||||
List<SyncMessageId> syncMessageIds = new LinkedList<>();
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return;
|
||||
|
||||
for (MarkedMessageInfo messageInfo : markedReadMessages) {
|
||||
scheduleDeletion(context, messageInfo.getExpirationInfo());
|
||||
@ -83,10 +83,9 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
|
||||
// Loki - Check whether we want to send a read receipt to this user
|
||||
if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; }
|
||||
// Loki - Take into account multi device
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new SendReadReceiptJob(address, timestamps));
|
||||
ReadReceipt readReceipt = new ReadReceipt(timestamps);
|
||||
readReceipt.setSentTimestamp(System.currentTimeMillis());
|
||||
MessageSender.send(readReceipt, address);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,17 +25,19 @@ import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.core.app.RemoteInput;
|
||||
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -68,21 +70,27 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
long threadId;
|
||||
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
message.setText(responseText.toString());
|
||||
|
||||
switch (replyMethod) {
|
||||
case GroupMessage: {
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
threadId = MessageSender.send(context, reply, -1, false, null);
|
||||
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
|
||||
try {
|
||||
DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, threadId, false, null);
|
||||
MessageSender.send(message, address);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SecureMessage: {
|
||||
OutgoingEncryptedMessage reply = new OutgoingEncryptedMessage(recipient, responseText.toString(), expiresIn);
|
||||
threadId = MessageSender.send(context, reply, -1, false, null);
|
||||
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
|
||||
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null);
|
||||
MessageSender.send(message, address);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
|
||||
import java.util.Comparator;
|
||||
@ -70,6 +70,9 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Address address = Address.fromSerialized(senderPublicKey);
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
|
||||
if (recipient.isBlocked()) return;
|
||||
|
||||
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
||||
if (content.getDataMessage().hasGroup()) {
|
||||
GroupContext groupContext = content.getDataMessage().getGroup();
|
||||
@ -103,17 +106,18 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startAnyExpiration(long messageID) {
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(messageID);
|
||||
public void startAnyExpiration(long timestamp, @NotNull String author) {
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(timestamp, author);
|
||||
if (messageRecord != null) {
|
||||
boolean mms = messageRecord.isMms();
|
||||
Recipient recipient = messageRecord.getRecipient();
|
||||
if (recipient.getExpireMessages() <= 0) return;
|
||||
if (mms) {
|
||||
mmsDatabase.markExpireStarted(messageID);
|
||||
mmsDatabase.markExpireStarted(messageRecord.getId());
|
||||
} else {
|
||||
smsDatabase.markExpireStarted(messageID);
|
||||
smsDatabase.markExpireStarted(messageRecord.getId());
|
||||
}
|
||||
scheduleDeletion(messageID, mms, recipient.getExpireMessages());
|
||||
scheduleDeletion(messageRecord.getId(), mms, recipient.getExpireMessages() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,13 @@ import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
@ -47,13 +49,11 @@ public class QuickResponseService extends IntentService {
|
||||
number = URLDecoder.decode(number);
|
||||
}
|
||||
|
||||
Address address = Address.fromExternal(this, number);
|
||||
Recipient recipient = Recipient.from(this, address, false);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
|
||||
if (!TextUtils.isEmpty(content)) {
|
||||
MessageSender.send(this, new OutgoingTextMessage(recipient, content, expiresIn, subscriptionId), -1, false, null);
|
||||
VisibleMessage message = new VisibleMessage();
|
||||
message.setText(content);
|
||||
message.setSentTimestamp(System.currentTimeMillis());
|
||||
MessageSender.send(message, Address.fromExternal(this, number));
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
Toast.makeText(this, R.string.QuickResponseService_problem_sending_message, Toast.LENGTH_LONG).show();
|
||||
|
@ -1,213 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
public class MessageSender {
|
||||
|
||||
private static final String TAG = MessageSender.class.getSimpleName();
|
||||
|
||||
public static long send(final Context context,
|
||||
final OutgoingTextMessage message,
|
||||
final long threadId,
|
||||
final boolean forceSms,
|
||||
final SmsDatabase.InsertListener insertListener)
|
||||
{
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
Recipient recipient = message.getRecipient();
|
||||
|
||||
long allocatedThreadId;
|
||||
|
||||
if (threadId == -1) {
|
||||
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
} else {
|
||||
allocatedThreadId = threadId;
|
||||
}
|
||||
|
||||
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
|
||||
|
||||
sendTextMessage(context, recipient, forceSms, messageId);
|
||||
|
||||
return allocatedThreadId;
|
||||
}
|
||||
|
||||
public static long send(final Context context,
|
||||
final OutgoingMediaMessage message,
|
||||
final long threadId,
|
||||
final boolean forceSms,
|
||||
final SmsDatabase.InsertListener insertListener)
|
||||
{
|
||||
try {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
long allocatedThreadId;
|
||||
|
||||
if (threadId == -1) {
|
||||
allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType());
|
||||
} else {
|
||||
allocatedThreadId = threadId;
|
||||
}
|
||||
|
||||
Recipient recipient = message.getRecipient();
|
||||
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
|
||||
sendMediaMessage(context, recipient, forceSms, messageId, message.getExpiresIn());
|
||||
return allocatedThreadId;
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
return threadId;
|
||||
}
|
||||
}
|
||||
|
||||
public static void resend(Context context, MessageRecord messageRecord) {
|
||||
long messageId = messageRecord.getId();
|
||||
boolean forceSms = messageRecord.isForcedSms();
|
||||
long expiresIn = messageRecord.getExpiresIn();
|
||||
Recipient recipient = messageRecord.getRecipient();
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
sendMediaMessage(context, recipient, forceSms, messageId, expiresIn);
|
||||
} else {
|
||||
sendTextMessage(context, recipient, forceSms, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, long expiresIn)
|
||||
{
|
||||
if (isLocalSelfSend(context, recipient, forceSms)) {
|
||||
sendLocalMediaSelf(context, messageId);
|
||||
} else if (isGroupPushSend(recipient)) {
|
||||
sendGroupPush(context, recipient, messageId, null);
|
||||
} else {
|
||||
sendMediaPush(context, recipient, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendTextMessage(Context context, Recipient recipient,
|
||||
boolean forceSms, long messageId)
|
||||
{
|
||||
if (isLocalSelfSend(context, recipient, forceSms)) {
|
||||
sendLocalTextSelf(context, messageId);
|
||||
} else {
|
||||
sendTextPush(context, recipient, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
|
||||
}
|
||||
|
||||
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
|
||||
}
|
||||
|
||||
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), filterAddress);
|
||||
}
|
||||
|
||||
private static boolean isGroupPushSend(Recipient recipient) {
|
||||
return recipient.getAddress().isGroup() &&
|
||||
!recipient.getAddress().isMmsGroup();
|
||||
}
|
||||
|
||||
private static boolean isLocalSelfSend(@NonNull Context context, @NonNull Recipient recipient, boolean forceSms) {
|
||||
return recipient.isLocalNumber() &&
|
||||
!forceSms &&
|
||||
TextSecurePreferences.isPushRegistered(context);
|
||||
}
|
||||
|
||||
private static void sendLocalMediaSelf(Context context, long messageId) {
|
||||
try {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
OutgoingMediaMessage message = mmsDatabase.getOutgoingMessage(messageId);
|
||||
SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getSentTimeMillis());
|
||||
|
||||
for (Attachment attachment : message.getAttachments()) {
|
||||
attachmentDatabase.markAttachmentUploaded(messageId, attachment);
|
||||
}
|
||||
|
||||
mmsDatabase.markAsSent(messageId, true);
|
||||
mmsDatabase.markUnidentified(messageId, true);
|
||||
|
||||
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
|
||||
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
|
||||
|
||||
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
mmsDatabase.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
|
||||
}
|
||||
} catch (NoSuchMessageException | MmsException e) {
|
||||
Log.w("Failed to update self-sent message.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendLocalTextSelf(Context context, long messageId) {
|
||||
try {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
SmsMessageRecord message = smsDatabase.getMessage(messageId);
|
||||
SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getDateSent());
|
||||
|
||||
smsDatabase.markAsSent(messageId, true);
|
||||
smsDatabase.markUnidentified(messageId, true);
|
||||
|
||||
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
|
||||
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
|
||||
|
||||
if (message.getExpiresIn() > 0) {
|
||||
smsDatabase.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(message.getId(), message.isMms(), message.getExpiresIn());
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w("Failed to update self-sent message.", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
|
||||
public class OutgoingEncryptedMessage extends OutgoingTextMessage {
|
||||
|
||||
public OutgoingEncryptedMessage(Recipient recipient, String body, long expiresIn) {
|
||||
super(recipient, body, expiresIn, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecureMessage() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -51,6 +51,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
|
||||
return;
|
||||
}
|
||||
|
||||
if (Recipient.from(context, author, false).isBlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Typist> typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>());
|
||||
Typist typist = new Typist(Recipient.from(context, author, false), device, threadId);
|
||||
|
||||
@ -76,6 +80,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
|
||||
return;
|
||||
}
|
||||
|
||||
if (Recipient.from(context, author, false).isBlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Typist> typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>());
|
||||
Typist typist = new Typist(Recipient.from(context, author, false), device, threadId);
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
package org.thoughtcrime.securesms.transport;
|
||||
|
||||
public class RetryLaterException extends Exception {
|
||||
public RetryLaterException() {}
|
||||
|
||||
public RetryLaterException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package org.thoughtcrime.securesms.transport;
|
||||
|
||||
public class UndeliverableMessageException extends Exception {
|
||||
|
||||
public UndeliverableMessageException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public UndeliverableMessageException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
@ -7,35 +7,73 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.R
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupEvent
|
||||
import org.thoughtcrime.securesms.backup.BackupPassphrase
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
|
||||
import org.thoughtcrime.securesms.backup.FullBackupExporter
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.session.libsession.utilities.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.database.BackupFileRecord
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object BackupUtil {
|
||||
private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences"
|
||||
private const val TAG = "BackupUtil"
|
||||
const val BACKUP_FILE_MIME_TYPE = "application/session-backup"
|
||||
const val BACKUP_PASSPHRASE_LENGTH = 30
|
||||
|
||||
fun getBackupRecords(context: Context): List<SharedPreference> {
|
||||
val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME
|
||||
val preferences = context.getSharedPreferences(prefName, 0)
|
||||
val prefList = LinkedList<SharedPreference>()
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build())
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build())
|
||||
if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) {
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null))
|
||||
.build())
|
||||
}
|
||||
if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) {
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.ED25519_SECRET_KEY)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null))
|
||||
.build())
|
||||
}
|
||||
prefList.add(SharedPreference.newBuilder()
|
||||
.setFile(prefName)
|
||||
.setKey(IdentityKeyUtil.LOKI_SEED)
|
||||
.setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null))
|
||||
.build())
|
||||
return prefList
|
||||
}
|
||||
|
||||
/**
|
||||
* Set app-wide configuration to enable the backups and schedule them.
|
||||
*
|
||||
@ -91,7 +129,7 @@ object BackupUtil {
|
||||
@JvmStatic
|
||||
fun generateBackupPassphrase(): Array<String> {
|
||||
val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) }
|
||||
return Array(6) {i ->
|
||||
return Array(6) { i ->
|
||||
String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
package org.session.libsession.database
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
|
||||
import java.io.InputStream
|
||||
@ -14,10 +12,13 @@ interface MessageDataProvider {
|
||||
fun getMessageID(serverID: Long): Long?
|
||||
fun deleteMessage(messageID: Long)
|
||||
|
||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||
|
||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||
|
||||
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
|
||||
|
||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||
@ -26,12 +27,15 @@ interface MessageDataProvider {
|
||||
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun uploadAttachment(attachmentId: Long)
|
||||
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
||||
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
||||
|
||||
// Quotes
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Long?
|
||||
fun getAttachmentsAndLinkPreviewFor(messageID: Long): List<Attachment>
|
||||
fun getMessageBodyFor(messageID: Long): String
|
||||
|
||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
package org.session.libsession.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
@ -59,6 +59,8 @@ interface StorageProtocol {
|
||||
fun getThreadID(openGroupID: String): String?
|
||||
fun getAllOpenGroups(): Map<Long, PublicChat>
|
||||
fun addOpenGroup(server: String, channel: Long)
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||
|
||||
// Open Group Public Keys
|
||||
fun getOpenGroupPublicKey(server: String): String?
|
||||
@ -94,10 +96,9 @@ interface StorageProtocol {
|
||||
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
||||
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||
fun markAsSent(messageID: Long)
|
||||
fun markUnidentified(messageID: Long)
|
||||
fun setErrorMessage(messageID: Long, error: Exception)
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
|
||||
// Closed Groups
|
||||
fun getGroup(groupID: String): GroupRecord?
|
||||
@ -112,9 +113,9 @@ interface StorageProtocol {
|
||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>)
|
||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long)
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||
fun isClosedGroup(publicKey: String): Boolean
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
|
@ -1,40 +0,0 @@
|
||||
package org.session.libsession.messaging.avatars;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.color.MaterialColor;
|
||||
|
||||
|
||||
/**
|
||||
* Used for migrating legacy colors to modern colors. For normal color generation, use
|
||||
* {@link ContactColors}.
|
||||
*/
|
||||
public class ContactColorsLegacy {
|
||||
|
||||
private static final String[] LEGACY_PALETTE = new String[] {
|
||||
"red",
|
||||
"pink",
|
||||
"purple",
|
||||
"deep_purple",
|
||||
"indigo",
|
||||
"blue",
|
||||
"light_blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"green",
|
||||
"light_green",
|
||||
"orange",
|
||||
"deep_orange",
|
||||
"amber",
|
||||
"blue_grey"
|
||||
};
|
||||
|
||||
public static MaterialColor generateFor(@NonNull String name) {
|
||||
String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length];
|
||||
try {
|
||||
return MaterialColor.fromSerialized(serialized);
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
return ContactColors.generateFor(name);
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,6 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
val server = "https://file.getsession.org"
|
||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
// endregion
|
||||
|
||||
|
@ -8,11 +8,15 @@ import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
|
||||
import org.session.libsignal.service.internal.crypto.PaddingInputStream
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData
|
||||
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
|
||||
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
|
||||
|
||||
@ -28,7 +32,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 20
|
||||
companion object {
|
||||
val TAG = AttachmentUploadJob::class.qualifiedName
|
||||
val TAG = AttachmentUploadJob::class.simpleName
|
||||
val KEY: String = "AttachmentUploadJob"
|
||||
|
||||
val maxFailureCount: Int = 20
|
||||
@ -41,46 +45,54 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
try {
|
||||
val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
ThreadUtils.queue {
|
||||
try {
|
||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||
?: return@queue handleFailure(Error.NoAttachment)
|
||||
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
val server = openGroup?.server ?: FileServerAPI.server
|
||||
var server = FileServerAPI.shared.server
|
||||
var shouldEncrypt = true
|
||||
val usePadding = false
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
||||
openGroup?.let {
|
||||
server = it.server
|
||||
shouldEncrypt = false
|
||||
}
|
||||
|
||||
//TODO add some encryption stuff here
|
||||
val isEncryptionRequired = false
|
||||
//val isEncryptionRequired = (server == FileServerAPI.server)
|
||||
val attachmentKey = Util.getSecretBytes(64)
|
||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
||||
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
||||
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
||||
|
||||
val attachmentKey = Util.getSecretBytes(64)
|
||||
val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val ciphertextLength = attachmentStream.length
|
||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||
|
||||
val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener)
|
||||
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||
|
||||
FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is Error && e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
} catch (e: java.lang.Exception) {
|
||||
if (e is Error && e == Error.NoAttachment) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
||||
this.handlePermanentFailure(e)
|
||||
} else {
|
||||
this.handleFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
||||
Log.w(TAG, "Attachment uploaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
|
||||
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
||||
//TODO interaction stuff, not sure how to deal with that
|
||||
}
|
||||
|
||||
private fun handlePermanentFailure(e: Exception) {
|
||||
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
|
||||
delegate?.handleJobFailedPermanently(this, e)
|
||||
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
|
||||
failAssociatedMessageSendJob(e)
|
||||
}
|
||||
|
||||
@ -95,7 +107,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
private fun failAssociatedMessageSendJob(e: Exception) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
|
||||
MessageSender.handleFailedMessageSend(this.message!!, e)
|
||||
MessageSender.handleFailedMessageSend(this.message, e)
|
||||
if (messageSendJob != null) {
|
||||
storage.markJobAsFailed(messageSendJob)
|
||||
}
|
||||
@ -119,7 +131,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<AttachmentUploadJob> {
|
||||
|
@ -15,8 +15,8 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageReceiveJob::class.qualifiedName
|
||||
val KEY: String = "AttachmentUploadJob"
|
||||
val TAG = MessageReceiveJob::class.simpleName
|
||||
val KEY: String = "MessageReceiveJob"
|
||||
|
||||
//keys used for database storage purpose
|
||||
private val KEY_DATA = "data"
|
||||
@ -75,7 +75,7 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<MessageReceiveJob> {
|
||||
|
@ -19,7 +19,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 10
|
||||
companion object {
|
||||
val TAG = MessageSendJob::class.qualifiedName
|
||||
val TAG = MessageSendJob::class.simpleName
|
||||
val KEY: String = "MessageSendJob"
|
||||
|
||||
//keys used for database storage purpose
|
||||
@ -32,13 +32,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
val message = message as? VisibleMessage
|
||||
message?.let {
|
||||
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
|
||||
val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull()
|
||||
val attachmentsToUpload = attachments.filter { !it.isUploaded }
|
||||
val attachmentIDs = mutableListOf<Long>()
|
||||
attachmentIDs.addAll(message.attachmentIDs)
|
||||
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||
message.linkPreview?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||
val attachments = attachmentIDs.mapNotNull { messageDataProvider.getDatabaseAttachment(it) }
|
||||
val attachmentsToUpload = attachments.filter { it.url.isNullOrEmpty() }
|
||||
attachmentsToUpload.forEach {
|
||||
if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) {
|
||||
if (MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) {
|
||||
// Wait for it to finish
|
||||
} else {
|
||||
val job = AttachmentUploadJob(it.attachmentId, message.threadID!!.toString(), message, id!!)
|
||||
val job = AttachmentUploadJob(it.attachmentId.rowId, message.threadID!!.toString(), message, id!!)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
@ -82,10 +86,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
val serializedMessage = ByteArray(4096)
|
||||
val serializedDestination = ByteArray(4096)
|
||||
var output = Output(serializedMessage)
|
||||
kryo.writeObject(output, message)
|
||||
kryo.writeClassAndObject(output, message)
|
||||
output.close()
|
||||
output = Output(serializedDestination)
|
||||
kryo.writeObject(output, destination)
|
||||
kryo.writeClassAndObject(output, destination)
|
||||
output.close()
|
||||
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
|
||||
.putByteArray(KEY_DESTINATION, serializedDestination)
|
||||
@ -93,7 +97,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<MessageSendJob> {
|
||||
@ -103,10 +107,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
//deserialize Message and Destination properties
|
||||
val kryo = Kryo()
|
||||
var input = Input(serializedMessage)
|
||||
val message: Message = kryo.readObject(input, Message::class.java)
|
||||
val message = kryo.readClassAndObject(input) as Message
|
||||
input.close()
|
||||
input = Input(serializedDestination)
|
||||
val destination: Destination = kryo.readObject(input, Destination::class.java)
|
||||
val destination = kryo.readClassAndObject(input) as Destination
|
||||
input.close()
|
||||
return MessageSendJob(message, destination)
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return AttachmentDownloadJob.KEY
|
||||
return KEY
|
||||
}
|
||||
|
||||
class Factory: Job.Factory<NotifyPNServerJob> {
|
||||
|
@ -3,26 +3,50 @@ package org.session.libsession.messaging.messages
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact(val publicKey: String) : Destination()
|
||||
class ClosedGroup(val groupPublicKey: String) : Destination()
|
||||
class OpenGroup(val channel: Long, val server: String) : Destination()
|
||||
class Contact() : Destination() {
|
||||
var publicKey: String = ""
|
||||
internal constructor(publicKey: String): this() {
|
||||
this.publicKey = publicKey
|
||||
}
|
||||
}
|
||||
class ClosedGroup() : Destination() {
|
||||
var groupPublicKey: String = ""
|
||||
internal constructor(groupPublicKey: String): this() {
|
||||
this.groupPublicKey = groupPublicKey
|
||||
}
|
||||
}
|
||||
class OpenGroup() : Destination() {
|
||||
var channel: Long = 0
|
||||
var server: String = ""
|
||||
internal constructor(channel: Long, server: String): this() {
|
||||
this.channel = channel
|
||||
this.server = server
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(address: Address): Destination {
|
||||
if (address.isContact) {
|
||||
return Contact(address.contactIdentifier())
|
||||
} else if (address.isClosedGroup) {
|
||||
val groupID = address.contactIdentifier()
|
||||
val groupPublicKey = GroupUtil.getDecodedGroupID(groupID)
|
||||
return ClosedGroup(groupPublicKey)
|
||||
} else if (address.isOpenGroup) {
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!!
|
||||
return OpenGroup(openGroup.channel, openGroup.server)
|
||||
} else {
|
||||
throw Exception("TODO: Handle legacy closed groups.")
|
||||
return when {
|
||||
address.isContact -> {
|
||||
Contact(address.contactIdentifier())
|
||||
}
|
||||
address.isClosedGroup -> {
|
||||
val groupID = address.toGroupString()
|
||||
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
|
||||
ClosedGroup(groupPublicKey)
|
||||
}
|
||||
address.isOpenGroup -> {
|
||||
val threadID = MessagingConfiguration.shared.storage.getThreadID(address.contactIdentifier())!!
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)!!
|
||||
OpenGroup(openGroup.channel, openGroup.server)
|
||||
}
|
||||
else -> {
|
||||
throw Exception("TODO: Handle legacy closed groups.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
abstract class Message {
|
||||
@ -18,11 +20,23 @@ abstract class Message {
|
||||
|
||||
// validation
|
||||
open fun isValid(): Boolean {
|
||||
sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false
|
||||
receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false
|
||||
sentTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
receivedTimestamp?.let {
|
||||
if (it <= 0) return false
|
||||
}
|
||||
return sender != null && recipient != null
|
||||
}
|
||||
|
||||
abstract fun toProto(): SignalServiceProtos.Content?
|
||||
|
||||
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
|
||||
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
|
||||
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
|
||||
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
|
||||
dataMessage.group = groupProto.build()
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
@ -35,8 +40,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
class NameChange(val name: String) : Kind()
|
||||
class MembersAdded(val members: List<ByteString>) : Kind()
|
||||
class MembersRemoved( val members: List<ByteString>) : Kind()
|
||||
object MemberLeft : Kind()
|
||||
object EncryptionKeyPairRequest: Kind()
|
||||
class MemberLeft : Kind()
|
||||
class EncryptionKeyPairRequest: Kind()
|
||||
|
||||
val description: String = run {
|
||||
when(this) {
|
||||
@ -46,8 +51,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
is NameChange -> "nameChange"
|
||||
is MembersAdded -> "membersAdded"
|
||||
is MembersRemoved -> "membersRemoved"
|
||||
MemberLeft -> "memberLeft"
|
||||
EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
|
||||
is MemberLeft -> "memberLeft"
|
||||
is EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,10 +97,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
|
||||
kind = Kind.MemberLeft
|
||||
kind = Kind.MemberLeft()
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR_REQUEST -> {
|
||||
kind = Kind.EncryptionKeyPairRequest
|
||||
kind = Kind.EncryptionKeyPairRequest()
|
||||
}
|
||||
}
|
||||
return ClosedGroupControlMessage(kind)
|
||||
@ -139,16 +144,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupControlMessage.publicKey = kind.publicKey
|
||||
closedGroupControlMessage.name = kind.name
|
||||
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize())
|
||||
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
|
||||
try {
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
|
||||
return null
|
||||
}
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
closedGroupControlMessage.addAllAdmins(kind.admins)
|
||||
}
|
||||
@ -185,6 +184,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
dataMessageProto.expireTimer = Recipient.from(MessagingConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
|
@ -1,10 +1,13 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
|
||||
var syncTarget: String? = null
|
||||
var duration: Int? = 0
|
||||
|
||||
companion object {
|
||||
@ -12,7 +15,7 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? {
|
||||
val dataMessageProto = proto.dataMessage ?: return null
|
||||
val isExpirationTimerUpdate = (dataMessageProto.flags and SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 //TODO validate that 'and' operator equivalent to Swift '&'
|
||||
val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0
|
||||
if (!isExpirationTimerUpdate) return null
|
||||
val duration = dataMessageProto.expireTimer
|
||||
return ExpirationTimerUpdate(duration)
|
||||
@ -39,6 +42,16 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
dataMessageProto.expireTimer = duration
|
||||
syncTarget?.let { dataMessageProto.syncTarget = it }
|
||||
// Group context
|
||||
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessageProto)
|
||||
} catch(e: Exception) {
|
||||
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user