Merge branch 'dev'

This commit is contained in:
Niels Andriesse 2021-04-15 10:19:30 +10:00
commit f2c9ea2d64
18 changed files with 114 additions and 1322 deletions

View File

@ -157,8 +157,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 150 def canonicalVersionCode = 151
def canonicalVersionName = "1.9.0" def canonicalVersionName = "1.9.1"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -84,7 +84,6 @@ import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager; import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
@ -363,7 +362,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
} }
private void initializePeriodicTasks() { private void initializePeriodicTasks() {
LocalBackupListener.schedule(this);
BackgroundPollWorker.schedulePeriodic(this); // Loki BackgroundPollWorker.schedulePeriodic(this); // Loki
if (BuildConfig.PLAY_STORE_DISABLED) { if (BuildConfig.PLAY_STORE_DISABLED) {

View File

@ -20,33 +20,13 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase.Reader;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker; import org.thoughtcrime.securesms.util.VersionTracker;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import network.loki.messenger.R;
public class DatabaseUpgradeActivity extends BaseActivity { public class DatabaseUpgradeActivity extends BaseActivity {
private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName();
@Override @Override
public void onCreate(Bundle bundle) { public void onCreate(Bundle bundle) {

View File

@ -311,11 +311,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private int cleanupMedia() { private int cleanupMedia() {
int restartItem = mediaPager.getCurrentItem(); int restartItem = mediaPager.getCurrentItem();
PagerAdapter adapter = mediaPager.getAdapter();
if (adapter instanceof CursorPagerAdapter) {
((CursorPagerAdapter)adapter).cursor.close();
}
mediaPager.removeAllViews(); mediaPager.removeAllViews();
mediaPager.setAdapter(null); mediaPager.setAdapter(null);

View File

@ -52,6 +52,8 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.session.libsession.messaging.jobs.AttachmentDownloadJob;
import org.session.libsession.messaging.jobs.JobQueue;
import org.session.libsession.messaging.opengroups.OpenGroupAPI; import org.session.libsession.messaging.opengroups.OpenGroupAPI;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
@ -67,7 +69,6 @@ import org.session.libsession.utilities.views.Stub;
import org.session.libsignal.libsignal.util.guava.Optional; 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.PublicChat;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MessageDetailsActivity; import org.thoughtcrime.securesms.MessageDetailsActivity;
@ -85,7 +86,6 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.loki.views.MessageAudioView; import org.thoughtcrime.securesms.loki.views.MessageAudioView;
@ -1075,10 +1075,10 @@ public class ConversationItem extends LinearLayout
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items"); Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
for (Slide slide : slides) { for (Slide slide : slides) {
ApplicationContext.getInstance(context) JobQueue.getShared().add(
.getJobManager() new AttachmentDownloadJob(messageRecord.getId(),
.add(new AttachmentDownloadJob(messageRecord.getId(), ((DatabaseAttachment)slide.asAttachment()).getAttachmentId().getRowId())
((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true)); );
} }
} }
} }

View File

@ -28,12 +28,14 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd; import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduHeaders; import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.session.libsession.utilities.GroupUtil;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
import org.session.libsession.database.documents.IdentityKeyMismatch; import org.session.libsession.database.documents.IdentityKeyMismatch;
@ -686,7 +688,14 @@ public class MmsDatabase extends MessagingDatabase {
throws MmsException throws MmsException
{ {
if (threadId == -1) { if (threadId == -1) {
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(retrieved.getRecipient()); if(retrieved.isGroup()) {
ByteString decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupContext().getId();
String groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId.toByteArray());
Recipient group = Recipient.from(context, Address.fromSerialized(groupId), false);
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(group);
} else {
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(retrieved.getRecipient());
}
} }
long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp); long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp);
if (messageId == -1) { if (messageId == -1) {

View File

@ -417,7 +417,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.setName(name) .setName(name)
.addAllMembers(members) .addAllMembers(members)
.addAllAdmins(admins) .addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf()) val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, false, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsDB = DatabaseFactory.getMmsDatabase(context)
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return

View File

@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.dependencies;
import android.content.Context; import android.content.Context;
import org.session.libsignal.service.api.SignalServiceMessageReceiver; import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
@ -13,11 +11,9 @@ import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@Module(complete = false, injects = {AttachmentDownloadJob.class, @Module(complete = false, injects = {AvatarDownloadJob.class,
AvatarDownloadJob.class,
RetrieveProfileAvatarJob.class, RetrieveProfileAvatarJob.class,
AppProtectionPreferenceFragment.class, AppProtectionPreferenceFragment.class,
PushDecryptJob.class,
LinkPreviewRepository.class}) LinkPreviewRepository.class})
public class SignalCommunicationModule { public class SignalCommunicationModule {

View File

@ -1,283 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.text.TextUtils;
import androidx.annotation.NonNull;
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;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import org.session.libsignal.service.loki.utilities.DownloadUtilities;
import org.thoughtcrime.securesms.ApplicationContext;
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.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.MmsException;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.session.libsignal.utilities.Base64;
import org.session.libsignal.utilities.Hex;
import org.session.libsession.utilities.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class AttachmentDownloadJob extends BaseJob implements InjectableType {
public static final String KEY = "AttachmentDownloadJob";
private static final int MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
private static final String TAG = AttachmentDownloadJob.class.getSimpleName();
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_PART_ROW_ID = "part_row_id";
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
private static final String KEY_MANUAL = "part_manual";
private long messageId;
private long partRowId;
private long partUniqueId;
private boolean manual;
public AttachmentDownloadJob(long messageId, AttachmentId attachmentId, boolean manual) {
this(new Job.Parameters.Builder()
.setQueue("AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(5)
.build(),
messageId,
attachmentId,
manual);
}
private AttachmentDownloadJob(@NonNull Job.Parameters parameters, long messageId, AttachmentId attachmentId, boolean manual) {
super(parameters);
this.messageId = messageId;
this.partRowId = attachmentId.getRowId();
this.partUniqueId = attachmentId.getUniqueId();
this.manual = manual;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_PART_ROW_ID, partRowId)
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
.putBoolean(KEY_MANUAL, manual)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onAdded() {
Log.i(TAG, "onAdded() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
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() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
}
}
@Override
public void onRun() throws IOException {
doWork();
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, 0);
}
public void doWork() throws IOException {
Log.i(TAG, "onRun() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
if (attachment == null) {
Log.w(TAG, "attachment no longer exists.");
return;
}
if (!attachment.isInProgress()) {
Log.w(TAG, "Attachment was already downloaded.");
return;
}
if (!manual && !AttachmentUtil.isAutoDownloadPermitted(context, attachment)) {
Log.w(TAG, "Attachment can't be auto downloaded...");
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING);
return;
}
Log.i(TAG, "Downloading push part " + attachmentId);
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
retrieveAttachment(messageId, attachmentId, attachment);
}
@Override
public void onCanceled() {
Log.w(TAG, "onCanceled() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
markFailed(messageId, attachmentId);
}
@Override
protected boolean onShouldRetry(@NonNull Exception exception) {
return (exception instanceof PushNetworkException);
}
private void retrieveAttachment(long messageId,
final AttachmentId attachmentId,
final Attachment attachment)
throws IOException
{
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
File attachmentFile = null;
try {
attachmentFile = createTempFile();
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
// InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)));
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
DownloadUtilities.downloadFile(attachmentFile, pointer.getUrl(), MAX_ATTACHMENT_SIZE, (total, progress) -> {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
});
final InputStream stream;
// Assume we're retrieving an attachment for an open group server if the digest is not set
if (!pointer.getDigest().isPresent()) {
stream = new FileInputStream(attachmentFile);
} else {
stream = AttachmentCipherInputStream.createForAttachment(attachmentFile, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
Log.w(TAG, "Experienced exception while trying to download an attachment.", e);
markFailed(messageId, attachmentId);
} finally {
if (attachmentFile != null) {
//noinspection ResultOfMethodCallIgnored
attachmentFile.delete();
}
}
}
@VisibleForTesting
SignalServiceAttachmentPointer createAttachmentPointer(Attachment attachment)
throws InvalidPartException
{
boolean isOpenGroupContext = TextUtils.isEmpty(attachment.getKey()) && attachment.getDigest() == null;
if (TextUtils.isEmpty(attachment.getLocation())) {
throw new InvalidPartException("empty content id");
}
if (TextUtils.isEmpty(attachment.getKey()) && !isOpenGroupContext) {
throw new InvalidPartException("empty encrypted key");
}
try {
long id = Long.parseLong(attachment.getLocation());
if (isOpenGroupContext) {
return new SignalServiceAttachmentPointer(id,
null,
new byte[0],
Optional.of(Util.toIntExact(attachment.getSize())),
Optional.absent(),
0,
0,
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
Optional.absent(), attachment.getUrl());
}
byte[] key = Base64.decode(attachment.getKey());
if (attachment.getDigest() != null) {
Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.getDigest()));
} else {
Log.i(TAG, "Downloading attachment with no digest...");
}
return new SignalServiceAttachmentPointer(id, null, key,
Optional.of(Util.toIntExact(attachment.getSize())),
Optional.absent(),
0, 0,
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
Optional.absent(), attachment.getUrl());
} catch (IOException | ArithmeticException e) {
Log.w(TAG, e);
throw new InvalidPartException(e);
}
}
private File createTempFile() throws InvalidPartException {
try {
File file = File.createTempFile("push-attachment", "tmp", context.getCacheDir());
file.deleteOnExit();
return file;
} catch (IOException e) {
throw new InvalidPartException(e);
}
}
private void markFailed(long messageId, AttachmentId attachmentId) {
try {
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
database.setTransferProgressFailed(attachmentId, messageId);
} catch (MmsException e) {
Log.w(TAG, e);
}
}
@VisibleForTesting static class InvalidPartException extends Exception {
InvalidPartException(String s) {super(s);}
InvalidPartException(Exception e) {super(e);}
}
public static final class Factory implements Job.Factory<AttachmentDownloadJob> {
@Override
public @NonNull AttachmentDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new AttachmentDownloadJob(parameters,
data.getLong(KEY_MESSAGE_ID),
new AttachmentId(data.getLong(KEY_PART_ROW_ID), data.getLong(KEY_PAR_UNIQUE_ID)),
data.getBoolean(KEY_MANUAL));
}
}
}

View File

@ -29,11 +29,8 @@ public final class JobManagerFactories {
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) { public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{ HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory()); put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application)); put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory());

View File

@ -1,50 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
public class PushContentReceiveJob extends PushReceivedJob {
public static final String KEY = "PushContentReceiveJob";
public PushContentReceiveJob(Context context) {
this(new Job.Parameters.Builder().build());
setContext(context);
}
private PushContentReceiveJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
public @NonNull
Data serialize() {
return Data.EMPTY;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() { }
@Override
public void onCanceled() { }
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
return false;
}
public static final class Factory implements Job.Factory<PushContentReceiveJob> {
@Override
public @NonNull PushContentReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new PushContentReceiveJob(parameters);
}
}
}

View File

@ -1,855 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
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;
import org.thoughtcrime.securesms.ApplicationContext;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
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;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase;
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.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
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.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
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.messages.SignalServiceContent;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
import org.session.libsignal.service.api.messages.shared.SharedContact;
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import network.loki.messenger.R;
public class PushDecryptJob extends BaseJob implements InjectableType {
public static final String KEY = "PushDecryptJob";
public static final String TAG = PushDecryptJob.class.getSimpleName();
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_SMS_MESSAGE_ID = "sms_message_id";
private long messageId;
private long smsMessageId;
private MessageNotifier messageNotifier;
public PushDecryptJob(Context context) {
this(context, -1);
}
public PushDecryptJob(Context context, long pushMessageId) {
this(context, pushMessageId, -1);
}
public PushDecryptJob(Context context, long pushMessageId, long smsMessageId) {
this(new Job.Parameters.Builder()
.setQueue("__PUSH_DECRYPT_JOB__")
.setMaxAttempts(10)
.build(),
pushMessageId,
smsMessageId);
setContext(context);
this.messageNotifier = ApplicationContext.getInstance(context).messageNotifier;
}
private PushDecryptJob(@NonNull Job.Parameters parameters, long pushMessageId, long smsMessageId) {
super(parameters);
this.messageId = pushMessageId;
this.smsMessageId = smsMessageId;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_SMS_MESSAGE_ID, smsMessageId)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws NoSuchMessageException {
synchronized (PushReceivedJob.RECEIVE_LOCK) {
if (needsMigration()) {
Log.w(TAG, "Skipping, waiting for migration...");
postMigrationNotification();
return;
}
PushDatabase database = DatabaseFactory.getPushDatabase(context);
SignalServiceEnvelope envelope = database.get(messageId);
Optional<Long> optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent();
handleMessage(envelope, optionalSmsMessageId, false);
database.delete(messageId);
}
}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
return false;
}
@Override
public void onCanceled() { }
public void processMessage(@NonNull SignalServiceEnvelope envelope, boolean isPushNotification) {
synchronized (PushReceivedJob.RECEIVE_LOCK) {
if (needsMigration()) {
Log.w(TAG, "Skipping and storing envelope, waiting for migration...");
DatabaseFactory.getPushDatabase(context).insert(envelope);
postMigrationNotification();
return;
}
handleMessage(envelope, Optional.absent(), isPushNotification);
}
}
private boolean needsMigration() {
return !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context);
}
private void postMigrationNotification() {
NotificationManagerCompat.from(context).notify(494949,
new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
.setSmallIcon(R.drawable.ic_notification)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
.setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages))
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, HomeActivity.class), 0))
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE)
.build());
}
private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional<Long> smsMessageId, boolean isPushNotification) {
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context);
SignalServiceCipher cipher = new SignalServiceCipher(new SessionProtocolImpl(context), apiDB);
SignalServiceContent content = cipher.decrypt(envelope);
if (shouldIgnore(content)) {
Log.i(TAG, "Ignoring message.");
return;
}
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (content.configurationMessageProto.isPresent()) {
MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender(), content.getTimestamp());
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent();
if (message.getClosedGroupControlMessage().isPresent()) {
ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupControlMessage().get(), message.getTimestamp(), envelope.getSource(), content.getSender());
}
if (message.isExpirationUpdate()) {
handleExpirationUpdate(content, message, smsMessageId);
} else if (isMediaMessage) {
handleMediaMessage(content, message, smsMessageId, Optional.absent());
} else if (message.getBody().isPresent()) {
handleTextMessage(content, message, smsMessageId, Optional.absent());
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
}
} else if (content.getReceiptMessage().isPresent()) {
SignalServiceReceiptMessage message = content.getReceiptMessage().get();
if (message.isReadReceipt()) handleReadReceipt(content, message);
else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message);
} else if (content.getTypingMessage().isPresent()) {
handleTypingMessage(content, content.getTypingMessage().get());
} else {
Log.w(TAG, "Got unrecognized message...");
}
resetRecipientToPush(Recipient.from(context, Address.fromSerialized(content.getSender()), false));
} catch (ProtocolInvalidMessageException e) {
Log.w(TAG, e);
if (!isPushNotification) { // This can be triggered if a PN encrypted with an old session comes in after the user performed a session reset
handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId, e);
}
}catch (StorageFailedException e) {
Log.w(TAG, e);
handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId, e);
} catch (InvalidMetadataMessageException e) {
Log.w(TAG, e);
}
}
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId)
throws StorageFailedException
{
try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient recipient = getMessageDestination(content, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(getMessageMasterDestination(content.getSender()).getAddress(),
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, true,
content.isNeedsReceipt(),
Optional.absent(),
message.getGroupInfo(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, message.getExpiresInSeconds());
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
}
}
public void handleMediaMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId,
@NonNull Optional<Long> messageServerIDOrNull)
throws StorageFailedException
{
Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Address masterAddress = masterRecipient.getAddress();
if (message.isGroupMessage()) {
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
}
// Handle sync message from ourselves
if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
Address targetAddress = masterRecipient.getAddress();
if (message.getGroupInfo().isPresent()) {
targetAddress = Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
} else if (syncTarget != null && !syncTarget.isEmpty()) {
targetAddress = Address.fromSerialized(syncTarget);
}
List<Attachment> attachments = PointerAttachment.forPointers(message.getAttachments());
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(),
attachments,
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000,
false,
DistributionTypes.DEFAULT, quote.orNull(),
sharedContacts.or(Collections.emptyList()),
linkPreviews.or(Collections.emptyList()),
Collections.emptyList(), Collections.emptyList());
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
Log.d("Loki","Message already exists, don't insert again");
return;
}
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
// Ignore message if it has no body and no attachments
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
return;
}
Optional<InsertResult> insertResult;
try {
// Check if we have the thread already
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp());
if (insertResult.isPresent()) {
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
List<DatabaseAttachment> dbAttachments = Stream.of(allAttachments).toList();
for (DatabaseAttachment attachment : dbAttachments) {
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
}
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
database.setTransactionSuccessful();
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
} finally {
database.endTransaction();
}
} else {
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
// Ignore message if it has no body and no attachments
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
return;
}
Optional<InsertResult> insertResult;
try {
if (message.isGroupMessage()) {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp());
} else {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
}
if (insertResult.isPresent()) {
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
List<DatabaseAttachment> attachments = Stream.of(allAttachments).toList();
for (DatabaseAttachment attachment : attachments) {
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
}
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
database.setTransactionSuccessful();
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
} finally {
database.endTransaction();
}
if (insertResult.isPresent()) {
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
if (insertResult.isPresent()) {
InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
// Loki - Store message open group server ID if needed
if (messageServerIDOrNull.isPresent()) {
long messageID = result.getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
}
// Loki - Update mapping of message ID to original thread ID
if (result.getMessageId() > -1) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
}
}
}
}
public void handleTextMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId,
@NonNull Optional<Long> messageServerIDOrNull)
throws StorageFailedException
{
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
Long threadId = null;
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
} else if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
Address targetAddress = masterRecipient.getAddress();
if (message.getGroupInfo().isPresent()) {
targetAddress = Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
} else if (syncTarget != null && !syncTarget.isEmpty()) {
targetAddress = Address.fromSerialized(syncTarget);
}
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
Log.d("Loki","Message already exists, don't insert again");
return;
}
OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false),
body, message.getExpiresInSeconds(), -1, message.getTimestamp());
// Ignore the message if it has no body
if (tm.getMessageBody().length() == 0) { return; }
// Check if we have the thread already
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
// Insert the message into the database
Optional<InsertResult> insertResult;
insertResult = database.insertMessageOutbox(threadID, tm, content.getTimestamp());
if (insertResult.isPresent()) {
threadId = insertResult.get().getThreadId();
}
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
if (threadId != null) {
messageNotifier.updateNotification(context, threadId);
}
if (insertResult.isPresent()) {
InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
}
} else {
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Address masterAddress = masterRecipient.getAddress();
IncomingTextMessage tm = new IncomingTextMessage(masterAddress,
content.getSenderDevice(),
message.getTimestamp(), body,
message.getGroupInfo(),
message.getExpiresInSeconds() * 1000L,
content.isNeedsReceipt());
IncomingEncryptedMessage textMessage = new IncomingEncryptedMessage(tm, body);
// Ignore the message if it has no body
if (textMessage.getMessageBody().length() == 0) { return; }
// Insert the message into the database
Optional<InsertResult> insertResult;
if (message.isGroupMessage()) {
insertResult = database.insertMessageInbox(textMessage, content.getTimestamp());
} else {
insertResult = database.insertMessageInbox(textMessage);
}
if (insertResult.isPresent()) {
threadId = insertResult.get().getThreadId();
}
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
if (threadId != null) {
messageNotifier.updateNotification(context, threadId);
}
if (insertResult.isPresent()) {
InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
// Loki - Store message open group server ID if needed
if (messageServerIDOrNull.isPresent()) {
long messageID = result.getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
}
// Loki - Update mapping of message ID to original thread ID
if (result.getMessageId() > -1) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
}
}
}
}
private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional<Long> smsMessageId, @NonNull Throwable e)
{
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
if (!SessionMetaProtocol.shouldIgnoreDecryptionException(context, timestamp)) {
if (!smsMessageId.isPresent()) {
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
if (insertResult.isPresent()) {
smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId());
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
} else {
smsDatabase.markAsDecryptFailed(smsMessageId.get());
}
}
}
@SuppressLint("DefaultLocale")
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message)
{
// Redirect message to master device conversation
Address masterAddress = Address.fromSerialized(content.getSender());
if (masterAddress.isContact()) {
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
masterAddress = masterRecipient.getAddress();
}
for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context)
.incrementDeliveryReceiptCount(new SyncMessageId(masterAddress, timestamp), System.currentTimeMillis());
}
}
@SuppressLint("DefaultLocale")
private void handleReadReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message)
{
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
// Redirect message to master device conversation
Address masterAddress = Address.fromSerialized(content.getSender());
if (masterAddress.isContact()) {
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
masterAddress = masterRecipient.getAddress();
}
for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context)
.incrementReadReceiptCount(new SyncMessageId(masterAddress, timestamp), content.getTimestamp());
}
}
}
private void handleTypingMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceTypingMessage typingMessage)
{
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
return;
}
long threadId;
Recipient author = getMessageMasterDestination(content.getSender());
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(author);
if (threadId <= 0) {
Log.w(TAG, "Couldn't find a matching thread for a typing message.");
return;
}
if (typingMessage.isTypingStarted()) {
Log.d(TAG, "Typing started on thread " + threadId);
ApplicationContext.getInstance(context).getTypingStatusRepository().didReceiveTypingStartedMessage(context,threadId, author.getAddress(), content.getSenderDevice());
} else {
Log.d(TAG, "Typing stopped on thread " + threadId);
ApplicationContext.getInstance(context).getTypingStatusRepository().didReceiveTypingStoppedMessage(context, threadId, author.getAddress(), content.getSenderDevice(), false);
}
}
private Optional<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> quote) {
if (!quote.isPresent()) return Optional.absent();
if (quote.get().getId() <= 0) {
Log.w(TAG, "Received quote without an ID! Ignoring...");
return Optional.absent();
}
if (quote.get().getAuthor() == null) {
Log.w(TAG, "Received quote without an author! Ignoring...");
return Optional.absent();
}
Address author = Address.fromSerialized(quote.get().getAuthor().getNumber());
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quote.get().getId(), author);
if (message != null) {
Log.i(TAG, "Found matching message record...");
List<Attachment> attachments = new LinkedList<>();
if (message.isMms()) {
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
attachments = mmsMessage.getSlideDeck().asAttachments();
if (attachments.isEmpty()) {
attachments.addAll(Stream.of(mmsMessage.getLinkPreviews())
.filter(lp -> lp.getThumbnail().isPresent())
.map(lp -> lp.getThumbnail().get())
.toList());
}
}
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
}
Log.w(TAG, "Didn't find matching message record...");
return Optional.of(new QuoteModel(quote.get().getId(),
author,
quote.get().getText(),
true,
PointerAttachment.forPointersOfDataMessage(quote.get().getAttachments())));
}
private Optional<List<Contact>> getContacts(Optional<List<SharedContact>> sharedContacts) {
if (!sharedContacts.isPresent()) return Optional.absent();
List<Contact> contacts = new ArrayList<>(sharedContacts.get().size());
for (SharedContact sharedContact : sharedContacts.get()) {
contacts.add(ContactModelMapper.remoteToLocal(sharedContact));
}
return Optional.of(contacts);
}
private Optional<List<LinkPreview>> getLinkPreviews(Optional<List<Preview>> previews, @NonNull String message) {
if (!previews.isPresent()) return Optional.absent();
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
for (Preview preview : previews.get()) {
Optional<Attachment> thumbnail = PointerAttachment.forPointer(preview.getImage());
Optional<String> url = Optional.fromNullable(preview.getUrl());
Optional<String> title = Optional.fromNullable(preview.getTitle());
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent();
boolean presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findWhitelistedUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get());
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidLinkUrl(url.get());
if (hasContent && presentInBody && validDomain) {
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), thumbnail);
linkPreviews.add(linkPreview);
} else {
Log.w(TAG, String.format("Discarding an invalid link preview. hasContent: %b presentInBody: %b validDomain: %b", hasContent, presentInBody, validDomain));
}
}
return Optional.of(linkPreviews);
}
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
Recipient masterRecipient = getMessageMasterDestination(sender);
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(masterRecipient.getAddress(),
senderDevice, timestamp, "",
Optional.absent(), 0, false);
textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(textMessage);
}
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) {
return Recipient.from(context, Address.fromExternal(context, GroupUtil.getEncodedClosedGroupID(message.getGroupInfo().get().getGroupId())), false);
} else {
return Recipient.from(context, Address.fromExternal(context, content.getSender()), false);
}
}
private Recipient getMessageMasterDestination(String publicKey) {
if (!PublicKeyValidation.isValid(publicKey)) {
return Recipient.from(context, Address.fromSerialized(publicKey), false);
} else {
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
if (publicKey.equals(userPublicKey)) {
return Recipient.from(context, Address.fromSerialized(userPublicKey), false);
} else {
return Recipient.from(context, Address.fromSerialized(publicKey), false);
}
}
}
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) {
Recipient author = Recipient.from(context, Address.fromSerialized(sender), false);
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(conversationRecipient);
if (threadId > 0) {
Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message.");
ApplicationContext.getInstance(context).getTypingStatusRepository().didReceiveTypingStoppedMessage(context, threadId, author.getAddress(), device, true);
}
}
private boolean shouldIgnore(@Nullable SignalServiceContent content) {
if (content == null) {
Log.w(TAG, "Got a message with null content.");
return true;
}
if (SessionMetaProtocol.shouldIgnoreMessage(content.getTimestamp())) {
Log.d("Loki", "Ignoring duplicate message.");
return true;
}
if (content.getSender().equals(TextSecurePreferences.getLocalNumber(context)) &&
DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), content.getSender()) != null) {
Log.d("Loki", "Skipping message from self we already have");
return true;
}
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
Recipient conversation = getMessageDestination(content, message);
if (conversation.isGroupRecipient() && conversation.isBlocked()) {
return true;
} else if (conversation.isGroupRecipient()) {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get()))
: Optional.absent();
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
return false;
}
boolean isTextMessage = message.getBody().isPresent();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
boolean isExpireMessage = message.isExpirationUpdate();
boolean isContentMessage = !message.isGroupUpdate() && (isTextMessage || isMediaMessage || isExpireMessage);
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT;
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage);
} else {
return sender.isBlocked();
}
}
return false;
}
private void resetRecipientToPush(@NonNull Recipient recipient) {
if (recipient.isForceSmsSelection()) {
DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient, false);
}
}
@SuppressWarnings("WeakerAccess")
private static class StorageFailedException extends Exception {
private final String sender;
private final int senderDevice;
private StorageFailedException(Exception e, String sender, int senderDevice) {
super(e);
this.sender = sender;
this.senderDevice = senderDevice;
}
public String getSender() {
return sender;
}
public int getSenderDevice() {
return senderDevice;
}
}
public static final class Factory implements Job.Factory<PushDecryptJob> {
@Override
public @NonNull PushDecryptJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new PushDecryptJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getLong(KEY_SMS_MESSAGE_ID));
}
}
}

View File

@ -1,52 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Job;
public abstract class PushReceivedJob extends BaseJob {
private static final String TAG = PushReceivedJob.class.getSimpleName();
public static final Object RECEIVE_LOCK = new Object();
protected PushReceivedJob(Job.Parameters parameters) {
super(parameters);
}
public void processEnvelope(@NonNull SignalServiceEnvelope envelope, boolean isPushNotification) {
synchronized (RECEIVE_LOCK) {
try {
if (envelope.hasSource()) {
Address source = Address.fromExternal(context, envelope.getSource());
Recipient recipient = Recipient.from(context, source, false);
if (!isActiveNumber(recipient)) {
DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, Recipient.RegisteredState.REGISTERED);
}
}
if (envelope.isUnidentifiedSender() || envelope.isClosedGroupCiphertext()) {
handleMessage(envelope, isPushNotification);
} else {
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
}
} catch (Exception e) {
Log.d("Loki", "Failed to process envelope due to error: " + e);
}
}
}
private void handleMessage(SignalServiceEnvelope envelope, boolean isPushNotification) {
new PushDecryptJob(context).processMessage(envelope, isPushNotification);
}
private boolean isActiveNumber(@NonNull Recipient recipient) {
return recipient.resolve().getRegistered() == Recipient.RegisteredState.REGISTERED;
}
}

View File

@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.service;
import android.content.Context; import android.content.Context;
import com.google.protobuf.ByteString;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.messaging.threads.DistributionTypes;
@ -73,32 +75,88 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
@Override @Override
public void setExpirationTimer(@NotNull ExpirationTimerUpdate message) { public void setExpirationTimer(@NotNull ExpirationTimerUpdate message) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String userPublicKey = TextSecurePreferences.getLocalNumber(context); String userPublicKey = TextSecurePreferences.getLocalNumber(context);
String senderPublicKey = message.getSender(); String senderPublicKey = message.getSender();
int duration = message.getDuration();
String groupPK = message.getGroupPublicKey(); // Notify the user
if (userPublicKey.equals(senderPublicKey)) {
// sender is a linked device
insertOutgoingExpirationTimerMessage(message);
} else {
insertIncomingExpirationTimerMessage(message);
}
if (message.getId() != null) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(message.getId());
}
}
private void insertIncomingExpirationTimerMessage(ExpirationTimerUpdate message) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String senderPublicKey = message.getSender();
Long sentTimestamp = message.getSentTimestamp(); Long sentTimestamp = message.getSentTimestamp();
String groupId = message.getGroupPublicKey();
int duration = message.getDuration();
Optional<SignalServiceGroup> groupInfo = Optional.absent(); Optional<SignalServiceGroup> groupInfo = Optional.absent();
Address address; Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
Recipient recipient = Recipient.from(context, address, false);
// if the sender is blocked, we don't display the update, except if it's in a closed group
if (recipient.isBlocked() && groupId == null) return;
try { try {
if (groupPK != null) { if (groupId != null) {
String groupID = GroupUtil.doubleEncodeGroupID(groupPK); String groupID = GroupUtil.doubleEncodeGroupID(groupId);
groupInfo = Optional.of(new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL)); groupInfo = Optional.of(new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL));
address = Address.fromSerialized(groupID);
} else { Address groupAddress = Address.fromSerialized(groupID);
address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey); recipient = Recipient.from(context, groupAddress, false);
} }
Recipient recipient = Recipient.from(context, address, false);
if (recipient.isBlocked()) return; IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
duration * 1000L, true,
false,
Optional.absent(),
groupInfo,
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
//insert the timer update message
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
// Notify the user //set the timer to the conversation
if (userPublicKey.equals(senderPublicKey)) { DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
// sender is a linked device
} catch (IOException | MmsException ioe) {
Log.e("Loki", "Failed to insert expiration update message.");
}
}
private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String senderPublicKey = message.getSender();
Long sentTimestamp = message.getSentTimestamp();
String groupId = message.getGroupPublicKey();
int duration = message.getDuration();
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
Recipient recipient = Recipient.from(context, address, false);
try {
if (groupId != null) {
// conversation is a closed group
GroupContext groupContext = SignalServiceProtos.GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId))).build();
OutgoingGroupMediaMessage infoMessage = new OutgoingGroupMediaMessage(recipient, groupContext, null, sentTimestamp, duration * 1000L, true, null, Collections.emptyList(), Collections.emptyList());
database.insertSecureDecryptedMessageOutbox(infoMessage, -1, sentTimestamp);
// we need the group ID as recipient for setExpireMessages below
recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false);
} else {
// conversation is a 1-1
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient, OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient,
null, null,
Collections.emptyList(), Collections.emptyList(),
@ -113,29 +171,12 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());
database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp); database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp);
} else {
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
duration * 1000L, true,
false,
Optional.absent(),
groupInfo,
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
//insert the timer update message
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
} }
//set the timer to the conversation //set the timer to the conversation
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration); DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
if (message.getId() != null) { } catch (MmsException | IOException ioe) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(message.getId());
}
} catch (MmsException e) {
Log.e("Loki", "Failed to insert expiration update message.");
} catch (IOException ioe) {
Log.e("Loki", "Failed to insert expiration update message."); Log.e("Loki", "Failed to insert expiration update message.");
} }
} }

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.service; package org.thoughtcrime.securesms.service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;

View File

@ -6,7 +6,9 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
@ -16,6 +18,8 @@ import kotlin.math.roundToLong
class JobQueue : JobDelegate { class JobQueue : JobDelegate {
private var hasResumedPendingJobs = false // Just for debugging private var hasResumedPendingJobs = false // Just for debugging
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = GlobalScope + SupervisorJob() private val scope = GlobalScope + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED) private val queue = Channel<Job>(UNLIMITED)
@ -44,7 +48,14 @@ class JobQueue : JobDelegate {
} }
private fun addWithoutExecuting(job: Job) { private fun addWithoutExecuting(job: Job) {
job.id = System.currentTimeMillis().toString() // When adding multiple jobs in rapid succession, timestamps might not be good enough as a unique ID. To
// deal with this we keep track of the number of jobs with a given timestamp and that to the end of the
// timestamp to make it a unique ID. We can't use a random number because we do still want to keep track
// of the order in which the jobs were added.
val currentTime = System.currentTimeMillis()
jobTimestampMap.putIfAbsent(currentTime, AtomicInteger())
job.id = currentTime.toString() + jobTimestampMap[currentTime]!!.getAndIncrement().toString()
MessagingConfiguration.shared.storage.persistJob(job) MessagingConfiguration.shared.storage.persistJob(job)
} }

View File

@ -58,6 +58,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@Nullable final Attachment avatar, @Nullable final Attachment avatar,
long sentTime, long sentTime,
long expireIn, long expireIn,
boolean expirationUpdate,
@Nullable QuoteModel quote, @Nullable QuoteModel quote,
@NonNull List<Contact> contacts, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews) @NonNull List<LinkPreview> previews)
@ -65,7 +66,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
super(recipient, Base64.encodeBytes(group.toByteArray()), super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}}, new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
sentTime, sentTime,
DistributionTypes.CONVERSATION, expireIn, false, quote, contacts, previews); DistributionTypes.CONVERSATION, expireIn, expirationUpdate, quote, contacts, previews);
this.group = group; this.group = group;
} }

View File

@ -78,6 +78,11 @@ object GroupUtil {
return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray())
} }
@JvmStatic
fun doubleEncodeGroupID(groupID: ByteArray): String {
return getEncodedClosedGroupID(getEncodedClosedGroupID(groupID).toByteArray())
}
@JvmStatic @JvmStatic
@Throws(IOException::class) @Throws(IOException::class)
fun doubleDecodeGroupID(groupID: String): ByteArray { fun doubleDecodeGroupID(groupID: String): ByteArray {