Merge branch 'dev'

This commit is contained in:
Niels Andriesse 2021-05-06 16:06:04 +10:00
commit 2f23a0e59f
231 changed files with 4009 additions and 17827 deletions

View File

@ -46,11 +46,12 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'androidx.activity:activity-ktx:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.activity:activity-ktx:1.2.2'
implementation 'androidx.fragment:fragment-ktx:1.3.2'
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.work:work-runtime-ktx:2.4.0"
@ -157,8 +158,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2'
}
def canonicalVersionCode = 151
def canonicalVersionName = "1.9.1"
def canonicalVersionCode = 157
def canonicalVersionName = "1.10.0"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms
import android.util.Log
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.jvm.asDispatcher
import org.session.libsignal.utilities.ThreadUtils
import java.util.concurrent.Executors
object AppContext {
fun configureKovenant() {
Kovenant.context {
callbackContext.dispatcher = Executors.newSingleThreadExecutor().asDispatcher()
workerContext.dispatcher = ThreadUtils.executorPool.asDispatcher()
multipleCompletion = { v1, v2 ->
Log.d("Loki", "Promise resolved more than once (first with $v1, then with $v2); ignoring $v2.")
}
}
}
}

View File

@ -30,15 +30,17 @@ import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.multidex.MultiDexApplication;
import org.conscrypt.Conscrypt;
import org.session.libsession.messaging.MessagingConfiguration;
import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.avatars.AvatarHelper;
import org.session.libsession.messaging.file_server.FileServerAPI;
import org.session.libsession.messaging.jobs.JobQueue;
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
import org.session.libsession.messaging.mentions.MentionsManager;
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller;
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.snode.SnodeConfiguration;
import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.IdentityKeyUtil;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@ -47,12 +49,7 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWr
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsignal.service.api.util.StreamDetails;
import org.session.libsignal.service.loki.api.PushNotificationAPI;
import org.session.libsignal.service.loki.api.SnodeAPI;
import org.session.libsignal.service.loki.api.SwarmAPI;
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import org.session.libsignal.utilities.logging.Log;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
@ -106,6 +103,7 @@ import dagger.ObjectGraph;
import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig;
import nl.komponents.kovenant.Kovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
@ -166,6 +164,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
// Loki
// ========
AppContext.INSTANCE.configureKovenant();
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
broadcaster = new Broadcaster(this);
threadNotificationHandler = new Handler(Looper.getMainLooper());
@ -173,17 +172,14 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
MessagingConfiguration.Companion.configure(this,
MessagingModuleConfiguration.Companion.configure(this,
DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this),
new SessionProtocolImpl(this));
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
SnodeModule.Companion.configure(apiDB, broadcaster);
if (userPublicKey != null) {
SwarmAPI.Companion.configureIfNeeded(apiDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
}
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
setUpStorageAPIIfNeeded();
resubmitProfilePictureIfNeeded();
publicChatManager = new PublicChatManager(this);
@ -428,7 +424,6 @@ 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;
}
@ -458,13 +453,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return;
if (poller != null) {
SnodeAPI.shared.setUserPublicKey(userPublicKey);
poller.setUserPublicKey(userPublicKey);
return;
}
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
SwarmAPI.Companion.configureIfNeeded(apiDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
poller = new Poller();
closedGroupPoller = new ClosedGroupPoller();
}

View File

@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;

View File

@ -52,6 +52,8 @@ import androidx.viewpager.widget.ViewPager;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import com.google.android.material.tabs.TabLayout;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MediaDatabase;
@ -351,6 +353,12 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
actionMode.finish();
// Sending a Data extraction notification (for incoming attachments only)
boolean containsIncoming = mediaRecords.parallelStream().anyMatch(m -> !m.isOutgoing());
if (containsIncoming) {
//TODO uncomment line below when Data extraction will be activated
//sendMediaSavedNotificationIfNeeded();
}
}
}.execute();
})
@ -358,6 +366,16 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
}, mediaRecords.size());
}
/**
* Send a MediaSaved notification to the recipient
*/
private void sendMediaSavedNotificationIfNeeded() {
// we don't send media saved notification for groups
if (recipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
MessageSender.send(message, recipient.getAddress());
}
@SuppressLint("StaticFieldLeak")
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
int recordCount = mediaRecords.size();

View File

@ -39,7 +39,8 @@ import android.view.Window;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@ -52,7 +53,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;
@ -352,11 +352,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
saveTask.executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR,
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
// Sending a Data extraction notification (for incoming attachments only)
if(!mediaItem.outgoing) {
//TODO uncomment line below when Data extraction will be activated
//sendMediaSavedNotificationIfNeeded();
}
})
.execute();
});
}
/**
* Send a MediaSaved notification to the recipient
*/
private void sendMediaSavedNotificationIfNeeded() {
// we don't send media saved notification for groups
if (conversationRecipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
MessageSender.send(message, conversationRecipient.getAddress());
}
@SuppressLint("StaticFieldLeak")
private void deleteMedia() {
MediaItem mediaItem = getCurrentMediaItem();

View File

@ -40,6 +40,7 @@ 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.open_groups.OpenGroup;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
@ -64,7 +65,6 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import java.lang.ref.WeakReference;
import java.sql.Date;
@ -263,7 +263,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
toFrom.setText(toFromRes);
long threadID = messageRecord.getThreadId();
PublicChat openGroup = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadID);
OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadID);
if (openGroup != null && messageRecord.isOutgoing()) {
toFrom.setVisibility(View.GONE);
separator.setVisibility(View.GONE);

View File

@ -84,6 +84,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
}
@Override
public void onServiceDisconnected(ComponentName name) {
keyCachingService.setMasterSecret(new Object());
keyCachingService = null;
}
}, Context.BIND_AUTO_CREATE);
@ -133,7 +134,9 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
private void handleAuthenticated() {
authenticated = true;
//TODO Replace with a proper call.
keyCachingService.setMasterSecret(new Object());
if (keyCachingService != null) {
keyCachingService.setMasterSecret(new Object());
}
// Finish and proceed with the next intent.
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
@ -188,6 +191,8 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
if (!keyguardManager.isKeyguardSecure()) {
Log.w(TAG ,"Keyguard not secure...");
TextSecurePreferences.setScreenLockEnabled(getApplicationContext(), false);
TextSecurePreferences.setScreenLockTimeout(getApplicationContext(), 0);
handleAuthenticated();
return;
}

View File

@ -5,6 +5,7 @@ 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.open_groups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI
@ -26,7 +27,6 @@ import org.thoughtcrime.securesms.util.MediaUtil
import java.io.IOException
import java.io.InputStream
class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider {
override fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? {
@ -104,6 +104,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
}
override fun getOpenGroup(threadID: Long): OpenGroup? {
return null // TODO: Implement
}
override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
@ -133,9 +137,20 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return openGroupMessagingDatabase.getMessageID(serverID)
}
override fun deleteMessage(messageID: Long) {
val messagingDatabase = DatabaseFactory.getSmsDatabase(context)
messagingDatabase.deleteMessage(messageID)
override fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>? {
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
return messageDB.getMessageID(serverId, threadId)
}
override fun deleteMessage(messageID: Long, isSms: Boolean) {
if (isSms) {
val db = DatabaseFactory.getSmsDatabase(context)
db.deleteMessage(messageID)
} else {
val db = DatabaseFactory.getMmsDatabase(context)
db.delete(messageID)
}
DatabaseFactory.getLokiMessageDatabase(context).deleteMessage(messageID, isSms)
}
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {

View File

@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.TextSecurePreferences;

View File

@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import network.loki.messenger.R;
import okhttp3.HttpUrl;

View File

@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -35,7 +36,6 @@ import org.session.libsession.messaging.threads.recipients.RecipientModifiedList
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.Util;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import java.util.List;
@ -201,7 +201,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
long threadID = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(conversationRecipient);
String senderHexEncodedPublicKey = author.getAddress().serialize();
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
} else if (publicChat != null) {

View File

@ -83,21 +83,42 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.session.libsession.messaging.mentions.MentionsManager;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.open_groups.OpenGroupV2;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.threads.GroupRecord;
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.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
import org.session.libsession.utilities.views.Stub;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.utilities.mentions.Mention;
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
import org.session.libsignal.service.loki.Mention;
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
import org.session.libsignal.utilities.concurrent.ListenableFuture;
import org.session.libsignal.utilities.concurrent.SettableFuture;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.MediaOverviewActivity;
@ -121,7 +142,6 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
@ -135,7 +155,6 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
@ -158,9 +177,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
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;
@ -169,30 +185,12 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
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.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import org.session.libsession.utilities.ServiceUtil;
import org.session.libsession.utilities.Util;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.GroupRecord;
import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.views.Stub;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
import org.session.libsignal.utilities.concurrent.ListenableFuture;
import org.session.libsignal.utilities.concurrent.SettableFuture;
import org.session.libsession.utilities.TextSecurePreferences;
import java.io.IOException;
import java.text.SimpleDateFormat;
@ -376,10 +374,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null) {
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
} else if (openGroupV2 != null) {
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
}
View rootView = findViewById(R.id.rootView);
@ -806,15 +807,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime);
ExpirationTimerUpdate message = new ExpirationTimerUpdate(null, expirationTime);
ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime);
message.setRecipient(recipient.getAddress().serialize()); // we need the recipient in ExpiringMessageManager.insertOutgoingExpirationTimerMessage
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);
}
ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(getApplicationContext()).getExpiringMessageManager();
expiringMessageManager.setExpirationTimer(message);
MessageSender.send(message, recipient.getAddress());
return null;
}
@ -1401,12 +1400,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Subscribe(threadMode = ThreadMode.MAIN)
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null &&
publicChat.getChannel() == event.getChannel() &&
publicChat.getServer().equals(event.getUrl())) {
this.updateSubtitleTextView();
}
if (openGroup != null &&
openGroup.getRoom().equals(event.getRoom()) &&
openGroup.getServer().equals(event.getUrl())) {
this.updateSubtitleTextView();
}
}
//////// Helper Methods
@ -1723,7 +1728,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
boolean initiating = threadId == -1;
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
recipient.isGroupRecipient() ||
// recipient.isGroupRecipient() ||
inputPanel.getQuote().isPresent() ||
linkPreviewViewModel.hasLinkPreview() ||
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
@ -2339,11 +2344,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
muteIndicatorImageView.setVisibility(View.VISIBLE);
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
if (userCount == null) { userCount = 0; }
subtitleTextView.setText(userCount + " members");
} else if (openGroup != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer());
if (userCount == null) { userCount = 0; }
subtitleTextView.setText(userCount + " members");
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
subtitleTextView.setText(recipient.getAddress().toString());
} else {

View File

@ -57,48 +57,51 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
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.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
import org.session.libsession.messaging.open_groups.OpenGroupV2;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
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.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.SimpleTask;
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.ShareActivity;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
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.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.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.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.concurrent.SimpleTask;
import java.io.IOException;
import java.io.InputStream;
@ -395,8 +398,9 @@ public class ConversationFragment extends Fragment
boolean isGroupChat = recipient.isGroupRecipient();
if (isGroupChat) {
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
boolean isPublicChat = (publicChat != null);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
boolean isPublicChat = (publicChat != null || openGroupChat != null);
int selectedMessageCount = messageRecords.size();
boolean areAllSentByUser = true;
Set<String> uniqueUserSet = new HashSet<>();
@ -406,8 +410,12 @@ 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 && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext());
boolean userCanModerate =
(isPublicChat &&
((publicChat != null && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()))
|| (openGroupChat != null && OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.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;
@ -507,7 +515,8 @@ public class ConversationFragment extends Fragment
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
@ -518,14 +527,14 @@ public class ConversationFragment extends Fragment
{
@Override
protected Void doInBackground(MessageRecord... messageRecords) {
if (publicChat != null) {
if (publicChat != null || openGroupChat != null) {
ArrayList<Long> serverIDs = new ArrayList<>();
ArrayList<Long> ignoredMessages = new ArrayList<>();
ArrayList<Long> failedMessages = new ArrayList<>();
boolean isSentByUser = true;
for (MessageRecord messageRecord : messageRecords) {
isSentByUser = isSentByUser && messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
if (serverID != null) {
serverIDs.add(serverID);
} else {
@ -537,7 +546,7 @@ public class ConversationFragment extends Fragment
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
.success(l -> {
for (MessageRecord messageRecord : messageRecords) {
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
if (l.contains(serverID)) {
if (messageRecord.isMms()) {
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
@ -554,7 +563,25 @@ public class ConversationFragment extends Fragment
Log.w("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
return null;
});
}
} else if (openGroupChat != null) {
for (Long serverId : serverIDs) {
OpenGroupAPIV2
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
.success(l -> {
for (MessageRecord messageRecord : messageRecords) {
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
if (serverID != null && serverID.equals(serverId)) {
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
break;
}
}
return null;
}).fail(e->{
Log.e("Loki", "Couldn't delete message due to error",e);
return null;
});
}
}
} else {
for (MessageRecord messageRecord : messageRecords) {
if (messageRecord.isMms()) {
@ -590,7 +617,8 @@ public class ConversationFragment extends Fragment
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
builder.setCancelable(true);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
final OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
ConversationAdapter chatAdapter = getListAdapter();
@ -609,9 +637,19 @@ public class ConversationFragment extends Fragment
Log.d("Loki", "User banned");
return Unit.INSTANCE;
}).fail(e -> {
Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + ".");
Log.e("Loki", "Couldn't ban user due to error",e);
return null;
});
} else if (openGroupChat != null) {
OpenGroupAPIV2
.ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())
.success(l -> {
Log.d("Loki", "User banned");
return Unit.INSTANCE;
}).fail(e -> {
Log.e("Loki", "Failed to ban user",e);
return null;
});
} else {
Log.d("Loki", "Tried to ban user from a non-public chat");
}
@ -745,6 +783,11 @@ public class ConversationFragment extends Fragment
if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
// Sending a Data extraction notification (for incoming attachments only)
if(!message.isOutgoing()) {
//TODO uncomment line below when Data extraction will be activated
//sendMediaSavedNotificationIfNeeded();
}
return;
}
@ -757,6 +800,16 @@ public class ConversationFragment extends Fragment
});
}
/**
* Send a MediaSaved notification to the recipient
*/
private void sendMediaSavedNotificationIfNeeded() {
// we don't send media saved notification for groups
if (recipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
MessageSender.send(message, recipient.getAddress());
}
@Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.i(TAG, "onCreateLoader");

View File

@ -54,10 +54,13 @@ 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.open_groups.OpenGroup;
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
import org.session.libsession.messaging.open_groups.OpenGroupV2;
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.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
import org.session.libsession.utilities.GroupUtil;
@ -67,7 +70,6 @@ import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.views.Stub;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.MediaPreviewActivity;
@ -88,6 +90,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
import org.thoughtcrime.securesms.mms.GlideRequests;
@ -724,9 +727,9 @@ public class ConversationItem extends LinearLayout
String publicKey = recipient.getAddress().toString();
profilePictureView.setPublicKey(publicKey);
String displayName = recipient.getName();
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
if (displayName == null && publicChat != null) {
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.getId(), publicKey);
OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
if (displayName == null && openGroup != null) {
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(openGroup.getId(), publicKey);
}
profilePictureView.setDisplayName(displayName);
profilePictureView.setAdditionalPublicKey(null);
@ -867,7 +870,12 @@ public class ConversationItem extends LinearLayout
try {
String serverId = GroupUtil.getDecodedGroupID(conversationRecipient.getAddress().serialize());
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize());
if (senderDisplayName != null) { displayName = senderDisplayName; }
if (senderDisplayName != null) {
displayName = senderDisplayName;
} else {
// opengroupv2 format
displayName = OpenGroupUtilities.getDisplayName(recipient);
}
} catch (Exception e) {
// Do nothing
}
@ -911,10 +919,14 @@ public class ConversationItem extends LinearLayout
profilePictureView.setVisibility(VISIBLE);
int visibility = View.GONE;
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId());
if (publicChat != null) {
boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
visibility = isModerator ? View.VISIBLE : View.GONE;
} else if (openGroupV2 != null) {
boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer());
visibility = isModerator ? View.VISIBLE : View.GONE;
}
moderatorIconImageView.setVisibility(visibility);

View File

@ -14,11 +14,10 @@ import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.utilities.GroupDescription;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsignal.libsignal.util.guava.Optional;
@ -106,6 +105,8 @@ public class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else if (messageRecord.isScreenshotExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT);
else if (messageRecord.isMediaSavedExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED);
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
else if (messageRecord.isIdentityVerified() ||
@ -149,6 +150,22 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(GONE);
}
private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) {
@ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme());
if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) {
icon.setImageResource(R.drawable.quick_camera_dark);
} else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) {
icon.setImageResource(R.drawable.ic_file_download_white_36dp);
}
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
body.setText(messageRecord.getDisplayBody(getContext()));
title.setVisibility(VISIBLE);
body.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
private void setIdentityRecord(final MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_security_white_24dp);
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
@ -174,8 +191,6 @@ public class ConversationUpdateItem extends LinearLayout
private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp);
icon.clearColorFilter();
GroupDescription.Companion.getDescription(getContext(), messageRecord.getBody()).addListener(this);
body.setText(messageRecord.getDisplayBody(getContext()));
title.setVisibility(GONE);

View File

@ -25,10 +25,9 @@ import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol;
import org.session.libsignal.service.loki.LokiOpenGroupDatabaseProtocol;
import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.LinkedList;
@ -44,6 +43,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
static final String GROUP_ID = "group_id";
private static final String TITLE = "title";
private static final String MEMBERS = "members";
private static final String ZOMBIE_MEMBERS = "zombie_members";
private static final String AVATAR = "avatar";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
@ -64,6 +64,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
GROUP_ID + " TEXT, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
ZOMBIE_MEMBERS + " TEXT, " +
AVATAR + " BLOB, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
@ -81,7 +82,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
};
private static final String[] GROUP_PROJECTION = {
GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
GROUP_ID, TITLE, MEMBERS, ZOMBIE_MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS
};
@ -162,7 +163,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
List<Address> members = getCurrentMembers(groupId);
List<Address> members = getCurrentMembers(groupId, false);
List<Recipient> recipients = new LinkedList<>();
for (Address member : members) {
@ -177,6 +178,17 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
return recipients;
}
public @NonNull List<Recipient> getGroupZombieMembers(String groupId) {
List<Address> members = getCurrentZombieMembers(groupId);
List<Recipient> recipients = new LinkedList<>();
for (Address member : members) {
recipients.add(Recipient.from(context, member, false));
}
return recipients;
}
public long create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins, @NonNull Long formationTimestamp)
{
@ -300,6 +312,16 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
});
}
public void updateZombieMembers(String groupId, List<Address> members) {
Collections.sort(members);
ContentValues contents = new ContentValues();
contents.put(ZOMBIE_MEMBERS, Address.toSerializedList(members, ','));
contents.put(ACTIVE, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId});
}
public void updateAdmins(String groupId, List<Address> admins) {
Collections.sort(admins);
@ -311,7 +333,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}
public void removeMember(String groupId, Address source) {
List<Address> currentMembers = getCurrentMembers(groupId);
List<Address> currentMembers = getCurrentMembers(groupId, false);
currentMembers.remove(source);
ContentValues contents = new ContentValues();
@ -329,18 +351,22 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
});
}
private List<Address> getCurrentMembers(String groupId) {
private List<Address> getCurrentMembers(String groupId, boolean zombieMembers) {
Cursor cursor = null;
String membersColumn = MEMBERS;
if (zombieMembers) membersColumn = ZOMBIE_MEMBERS;
try {
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {MEMBERS},
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {membersColumn},
GROUP_ID + " = ?",
new String[] {groupId},
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS));
return Address.fromSerializedList(serializedMembers, ',');
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(membersColumn));
if (serializedMembers != null && !serializedMembers.isEmpty())
return Address.fromSerializedList(serializedMembers, ',');
}
return new LinkedList<>();
@ -350,6 +376,10 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}
}
private List<Address> getCurrentZombieMembers(String groupId) {
return getCurrentMembers(groupId, true);
}
public boolean isActive(String groupId) {
Optional<GroupRecord> record = getGroup(groupId);
return record.isPresent() && record.get().isActive();

View File

@ -28,7 +28,6 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase;
@ -60,7 +59,7 @@ 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.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;
@ -514,15 +513,7 @@ public class MmsDatabase extends MessagingDatabase {
}
}
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts, previews);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
}
boolean expirationTimer = (outboxType & Types.EXPIRATION_TIMER_UPDATE_BIT) != 0;
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, expirationTimer, distributionType, quote, contacts, previews, networkFailures, mismatches);
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, previews, networkFailures, mismatches);
if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message);
@ -532,8 +523,6 @@ public class MmsDatabase extends MessagingDatabase {
}
throw new NoSuchMessageException("No record found for id: " + messageId);
} catch (IOException e) {
throw new MmsException(e);
} finally {
if (cursor != null)
cursor.close();
@ -689,8 +678,19 @@ public class MmsDatabase extends MessagingDatabase {
{
if (threadId == -1) {
if(retrieved.isGroup()) {
ByteString decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupContext().getId();
String groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId.toByteArray());
String decodedGroupId;
if (retrieved instanceof OutgoingExpirationUpdateMessage) {
decodedGroupId = ((OutgoingExpirationUpdateMessage)retrieved).getGroupId();
} else {
decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupId();
}
String groupId;
try {
groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId);
} catch (IOException e) {
Log.e(TAG, "Couldn't encrypt group ID");
throw new MmsException(e);
}
Recipient group = Recipient.from(context, Address.fromSerialized(groupId), false);
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(group);
} else {
@ -718,6 +718,14 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
}
if (retrieved.isScreenshotDataExtraction()) {
type |= Types.SCREENSHOT_EXTRACTION_BIT;
}
if (retrieved.isMediaSavedDataExtraction()) {
type |= Types.MEDIA_SAVED_EXTRACTION_BIT;
}
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp);
}
@ -745,9 +753,8 @@ public class MmsDatabase extends MessagingDatabase {
if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT);
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
if (message.isGroup()) {
if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT;
else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
if (message.isGroup() && message instanceof OutgoingGroupMediaMessage) {
if (((OutgoingGroupMediaMessage)message).isUpdateMessage()) type |= Types.GROUP_UPDATE_MESSAGE_BIT;
}
if (message.isExpirationUpdate()) {

View File

@ -70,6 +70,11 @@ public interface MmsSmsColumns {
protected static final long GROUP_UPDATE_BIT = 0x10000;
protected static final long GROUP_QUIT_BIT = 0x20000;
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
protected static final long GROUP_UPDATE_MESSAGE_BIT = 0x80000;
// Data Extraction Information
protected static final long MEDIA_SAVED_EXTRACTION_BIT = 0x01000;
protected static final long SCREENSHOT_EXTRACTION_BIT = 0x02000;
// Encrypted Storage Information XXX
public static final long ENCRYPTION_MASK = 0xFF000000;
@ -197,6 +202,14 @@ public interface MmsSmsColumns {
return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0;
}
public static boolean isMediaSavedExtraction(long type) {
return (type & MEDIA_SAVED_EXTRACTION_BIT) != 0;
}
public static boolean isScreenshotExtraction(long type) {
return (type & SCREENSHOT_EXTRACTION_BIT) != 0;
}
public static boolean isIncomingCall(long type) {
return type == INCOMING_CALL_TYPE;
}
@ -213,6 +226,8 @@ public interface MmsSmsColumns {
return (type & GROUP_UPDATE_BIT) != 0;
}
public static boolean isGroupUpdateMessage(long type) { return (type & GROUP_UPDATE_MESSAGE_BIT) != 0; }
public static boolean isGroupQuit(long type) {
return (type & GROUP_QUIT_BIT) != 0;
}

View File

@ -347,8 +347,7 @@ public class SmsDatabase extends MessagingDatabase {
type |= Types.SECURE_MESSAGE_BIT;
} else if (message.isGroup()) {
type |= Types.SECURE_MESSAGE_BIT;
if (((IncomingGroupMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT;
else if (((IncomingGroupMessage)message).isQuit()) type |= Types.GROUP_QUIT_BIT;
if (((IncomingGroupMessage)message).isUpdateMessage()) type |= Types.GROUP_UPDATE_MESSAGE_BIT;
}
if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import com.google.protobuf.ByteString
import okhttp3.HttpUrl
import org.session.libsession.messaging.StorageProtocol
import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.Job
@ -13,14 +13,18 @@ import org.session.libsession.messaging.messages.signal.*
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.opengroups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupV2
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.linkpreview.LinkPreview
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.Address.Companion.fromSerialized
import org.session.libsession.messaging.threads.GroupRecord
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.messaging.utilities.ClosedGroupUpdateMessageData
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.session.libsession.utilities.TextSecurePreferences
@ -30,7 +34,6 @@ import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
@ -83,6 +86,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return recipient.profileKey
}
override fun getDisplayNameForRecipient(recipientPublicKey: String): String? {
val database = DatabaseFactory.getLokiUserDatabase(context)
return database.getDisplayName(recipientPublicKey)
}
override fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) {
val address = Address.fromSerialized(recipientPublicKey)
val recipient = Recipient.from(context, address, false)
@ -138,7 +146,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
val insertResult = if (message.sender == getUserPublicKey()) {
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
mmsDatabase.beginTransaction()
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
@ -221,6 +228,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
}
override fun getAuthToken(room: String, server: String): String? {
val id = "$server.$room"
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id)
}
override fun setAuthToken(room: String, server: String, newValue: String) {
val id = "$server.$room"
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, newValue)
}
override fun removeAuthToken(room: String, server: String) {
val id = "$server.$room"
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null)
}
override fun getOpenGroup(threadID: String): OpenGroup? {
if (threadID.toInt() < 0) { return null }
val database = databaseHelper.readableDatabase
@ -230,6 +252,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
override fun getV2OpenGroup(threadId: String): OpenGroupV2? {
if (threadId.toInt() < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
OpenGroupV2.fromJson(publicChatAsJson)
}
}
override fun getThreadID(openGroupID: String): String {
val address = Address.fromSerialized(openGroupID)
val recipient = Recipient.from(context, address, false)
@ -249,11 +280,33 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
}
override fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String) {
val groupID = "$server.$room"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
}
override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? {
val groupID = "$server.$channel"
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
}
override fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? {
val groupID = "$server.$room"
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
}
override fun getLastMessageServerId(room: String, server: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server)
}
override fun setLastMessageServerId(room: String, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue)
}
override fun removeLastMessageServerId(room: String, server: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server)
}
override fun getLastMessageServerID(group: Long, server: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
}
@ -266,6 +319,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
}
override fun getLastDeletionServerId(room: String, server: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
}
override fun setLastDeletionServerId(room: String, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
}
override fun removeLastDeletionServerId(room: String, server: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
}
override fun setUserCount(room: String, server: String, newValue: Int) {
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
}
override fun getLastDeletionServerID(group: Long, server: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server)
}
@ -310,9 +379,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
SessionMetaProtocol.addTimestamp(timestamp)
}
// override fun removeReceivedMessageTimestamps(timestamps: Set<Long>) {
// TODO("Not yet implemented")
// }
override fun removeReceivedMessageTimestamps(timestamps: Set<Long>) {
SessionMetaProtocol.removeTimestamps(timestamps)
}
override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? {
val database = DatabaseFactory.getMmsSmsDatabase(context)
@ -320,8 +389,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return database.getMessageFor(timestamp, address)?.getId()
}
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long) {
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID)
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
}
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
@ -386,6 +456,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
}
override fun getZombieMember(groupID: String): Set<String> {
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
}
override fun removeMember(groupID: String, member: Address) {
DatabaseFactory.getGroupDatabase(context).removeMember(groupID, member)
}
@ -394,33 +468,28 @@ 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>, sentTimestamp: Long) {
val groupContextBuilder = SignalServiceProtos.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())
override fun updateZombieMembers(groupID: String, members: List<Address>) {
DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
}
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
val group = SignalServiceGroup(type, 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 updateData = ClosedGroupUpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
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, sentTimestamp: Long) {
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.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)))
.setType(type)
.setName(name)
.addAllMembers(members)
.addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, false, null, listOf(), listOf())
val updateData = ClosedGroupUpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: ""
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context)
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true)
}
@ -471,8 +540,27 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
override fun addOpenGroup(server: String, channel: Long) {
OpenGroupUtilities.addGroup(context, server, channel)
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
}
override fun addOpenGroup(serverUrl: String, channel: Long) {
val httpUrl = HttpUrl.parse(serverUrl) ?: return
if (httpUrl.queryParameterNames().contains("public_key")) {
// open group v2
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
// non-standard port, add to server
this.port(httpUrl.port())
}
}.build()
val room = httpUrl.pathSegments().firstOrNull() ?: return
val publicKey = httpUrl.queryParameter("public_key") ?: return
OpenGroupUtilities.addGroup(context, server.toString().removeSuffix("/"), room, publicKey)
} else {
OpenGroupUtilities.addGroup(context, serverUrl, channel)
}
}
override fun getAllGroups(): List<GroupRecord> {
@ -509,6 +597,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return if (threadID < 0) null else threadID
}
override fun getThreadIdForMms(mmsId: Long): Long {
val mmsDb = DatabaseFactory.getMmsDatabase(context)
val cursor = mmsDb.getMessage(mmsId)
val reader = mmsDb.readerFor(cursor)
val threadId = reader.next.threadId
cursor.close()
return threadId
}
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
}
@ -578,4 +675,26 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri {
return PartAuthority.getAttachmentThumbnailUri(attachmentId)
}
// Data Extraction Notification
override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) {
val database = DatabaseFactory.getMmsDatabase(context)
val address = fromSerialized(senderPublicKey)
val recipient = Recipient.from(context, address, false)
if (recipient.isBlocked) return
val mediaMessage = IncomingMediaMessage(address, sentTimestamp, -1,
0, false,
false,
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(message))
database.insertSecureDecryptedMessageInbox(mediaMessage, -1)
}
}

View File

@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@ -410,6 +411,7 @@ public class ThreadDatabase extends Database {
deleteThread(threadId);
notifyConversationListeners(threadId);
notifyConversationListListeners();
SessionMetaProtocol.clearReceivedMessages();
}
public boolean hasThread(long threadId) {

View File

@ -54,9 +54,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV20 = 41;
private static final int lokiV21 = 42;
private static final int lokiV22 = 43;
private static final int lokiV23 = 44;
private static final int lokiV24 = 45;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV22;
private static final int DATABASE_VERSION = lokiV24;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -124,6 +126,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -272,6 +276,21 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
"SendDeliveryReceiptJob");
}
if (oldVersion < lokiV23) {
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
}
if (oldVersion < lokiV24) {
String swarmTable = LokiAPIDatabase.Companion.getSwarmTable();
String snodePoolTable = LokiAPIDatabase.Companion.getSnodePoolTable();
db.execSQL("DROP TABLE " + swarmTable);
db.execSQL("DROP TABLE " + snodePoolTable);
db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand());
db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -109,6 +109,7 @@ public abstract class DisplayRecord {
public boolean isLokiSessionRestoreDone() { return SmsDatabase.Types.isLokiSessionRestoreDoneType(type); }
// TODO isGroupUpdate and isGroupQuit are kept for compatibility with old update messages, they can be removed later on
public boolean isGroupUpdate() {
return SmsDatabase.Types.isGroupUpdate(type);
}
@ -117,14 +118,33 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isGroupQuit(type);
}
public boolean isGroupUpdateMessage() {
return SmsDatabase.Types.isGroupUpdateMessage(type);
}
//TODO isGroupAction can be replaced by isGroupUpdateMessage in the code when the 2 functions above are removed
public boolean isGroupAction() {
return isGroupUpdate() || isGroupQuit();
return isGroupUpdate() || isGroupQuit() || isGroupUpdateMessage();
}
public boolean isExpirationTimerUpdate() {
return SmsDatabase.Types.isExpirationTimerUpdate(type);
}
// Data extraction
public boolean isMediaSavedExtraction() {
return MmsSmsColumns.Types.isMediaSavedExtraction(type);
}
public boolean isScreenshotExtraction() {
return MmsSmsColumns.Types.isScreenshotExtraction(type);
}
public boolean isDataExtraction() {
return isMediaSavedExtraction() || isScreenshotExtraction();
}
public boolean isCallLog() {
return SmsDatabase.Types.isCallLog(type);
}

View File

@ -23,7 +23,7 @@ import android.text.SpannableString;
import network.loki.messenger.R;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
import org.session.libsession.database.documents.IdentityKeyMismatch;

View File

@ -24,14 +24,16 @@ import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import network.loki.messenger.R;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.session.libsession.messaging.utilities.ClosedGroupUpdateMessageBuilder;
import org.session.libsession.messaging.utilities.ClosedGroupUpdateMessageData;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
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;
import org.thoughtcrime.securesms.loki.utilities.GroupDescription;
import java.util.List;
@ -90,39 +92,25 @@ public abstract class MessageRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
if (isGroupUpdate() && isOutgoing()) {
if(isGroupUpdateMessage()) {
ClosedGroupUpdateMessageData updateMessageData = ClosedGroupUpdateMessageData.Companion.fromJSON(getBody());
return new SpannableString(ClosedGroupUpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
} else if (isExpirationTimerUpdate()) {
int seconds = (int) (getExpiresIn() / 1000);
return new SpannableString(ClosedGroupUpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
} else if (isDataExtraction()) {
if (isScreenshotExtraction()) return new SpannableString((ClosedGroupUpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
else if (isMediaSavedExtraction()) return new SpannableString((ClosedGroupUpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
}
// TODO below lines are left here for compatibility with older group update messages, it can be deleted later on
else if (isGroupUpdate() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
} else if (isGroupUpdate()) {
return new SpannableString(GroupDescription.Companion.getDescription(context, getBody()).toString(getIndividualRecipient()));
return new SpannableString(context.getString(R.string.MessageRecord_s_updated_group, getIndividualRecipient().toShortString()));
} else if (isGroupQuit() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_left_group));
} else if (isGroupQuit()) {
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString()));
} else if (isIncomingCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().toShortString()));
} else if (isOutgoingCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_called));
} else if (isMissedCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_missed_call));
} else if (isJoined()) {
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().toShortString()));
} else if (isExpirationTimerUpdate()) {
int seconds = (int)(getExpiresIn() / 1000);
if (seconds <= 0) {
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().toShortString()));
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(), time));
} else if (isIdentityUpdate()) {
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString()));
} else if (isIdentityVerified()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString()));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString()));
} else if (isIdentityDefault()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString()));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString()));
}
return new SpannableString(getBody());
@ -175,7 +163,7 @@ public abstract class MessageRecord extends DisplayRecord {
}
public boolean isUpdate() {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isDataExtraction() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone();
}

View File

@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.database.documents.IdentityKeyMismatch;

View File

@ -73,7 +73,7 @@ public class ThreadRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
Recipient recipient = getRecipient();
if (isGroupUpdate()) {
if (isGroupUpdate() || isGroupUpdateMessage()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
} else if (isGroupQuit()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
@ -103,12 +103,16 @@ public class ThreadRecord extends DisplayRecord {
} else if (SmsDatabase.Types.isJoinedType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
int seconds = (int)(getExpiresIn() / 1000);
int seconds = (int) (getExpiresIn() / 1000);
if (seconds <= 0) {
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString()));
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isIdentityUpdate(type)) {
if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString()));

View File

@ -4,6 +4,8 @@ import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.utilities.DownloadUtilities;
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.session.libsession.messaging.threads.GroupRecord;
@ -22,6 +24,7 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -91,9 +94,20 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
attachment.deleteOnExit();
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
DownloadUtilities.downloadFile(attachment, pointer.getUrl(), MAX_AVATAR_SIZE, null);
// Assume we're retrieving an attachment for an open group server if the digest is not set
InputStream inputStream;
if (!pointer.getDigest().isPresent()) {
inputStream = new FileInputStream(attachment);
} else {
inputStream = AttachmentCipherInputStream.createForAttachment(attachment, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
database.updateProfilePicture(groupId, avatar);
inputStream.close();

View File

@ -10,9 +10,11 @@ import org.session.libsession.messaging.avatars.AvatarHelper;
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.libsession.utilities.DownloadUtilities;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.crypto.ProfileCipherInputStream;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -22,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -102,7 +105,8 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
try {
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
DownloadUtilities.downloadFile(downloadDestination, profileAvatar, MAX_PROFILE_SIZE_BYTES, null);
InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey);
File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
Util.copy(avatarStream, new FileOutputStream(decryptDestination));

View File

@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.utilities.concurrent.SignalExecutors;
import java.io.ByteArrayOutputStream;

View File

@ -13,9 +13,8 @@ import org.session.libsession.utilities.Debouncer;
import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import java.util.Collections;
import java.util.List;

View File

@ -32,6 +32,10 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
set(newValue) { field = newValue; invalidateOptionsMenu() }
private var members = listOf<String>()
set(value) { field = value; selectContactsAdapter.members = value }
private val publicKey: String
get() {
return TextSecurePreferences.getLocalNumber(this)!!
}
private val selectContactsAdapter by lazy {
SelectContactsAdapter(this, GlideApp.with(this))
@ -72,7 +76,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
}
private fun update(members: List<String>) {
this.members = members
//if there is a Note to self conversation, it loads self in the list, so we need to remove it here
this.members = members.minus(publicKey)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu()

View File

@ -37,8 +37,14 @@ import java.io.IOException
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>()
private val zombies = HashSet<String>()
private val members = HashSet<String>()
private val allMembers: Set<String>
get() {
return members + zombies
}
private var hasNameChanged = false
private var isSelfAdmin = false
private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() }
@ -54,7 +60,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
private val memberListAdapter by lazy {
EditClosedGroupMembersAdapter(this, GlideApp.with(this), this::onMemberClick)
if (isSelfAdmin)
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin, this::onMemberClick)
else
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin)
}
private lateinit var mainContentContainer: LinearLayout
@ -81,7 +90,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable))
groupID = intent.getStringExtra(groupIDKey)!!
originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title
val groupInfo = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get()
originalName = groupInfo.title
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
name = originalName
mainContentContainer = findViewById(R.id.mainContentContainer)
@ -116,31 +128,35 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
}
LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<List<String>> {
LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<GroupMembers> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<GroupMembers> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
}
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
override fun onLoadFinished(loader: Loader<GroupMembers>, groupMembers: GroupMembers) {
// We no longer need any subsequent loading events
// (they will occur on every activity resume).
LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID)
members.clear()
members.addAll(groupMembers.members.toHashSet())
zombies.clear()
zombies.addAll(groupMembers.zombieMembers.toHashSet())
originalMembers.clear()
originalMembers.addAll(members.toHashSet())
updateMembers(originalMembers)
originalMembers.addAll(members + zombies)
updateMembers()
}
override fun onLoaderReset(loader: Loader<List<String>>) {
updateMembers(setOf())
override fun onLoaderReset(loader: Loader<GroupMembers>) {
updateMembers()
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_closed_group, menu)
return members.isNotEmpty() && !isLoading
return allMembers.isNotEmpty() && !isLoading
}
// endregion
@ -153,8 +169,8 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return
val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet()
val changedMembers = members + selectedContacts
updateMembers(changedMembers)
members.addAll(selectedContacts)
updateMembers()
}
}
}
@ -173,17 +189,12 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
}
private fun updateMembers(members: Set<String>) {
this.members.clear()
this.members.addAll(members)
memberListAdapter.setMembers(members)
private fun updateMembers() {
memberListAdapter.setMembers(allMembers)
memberListAdapter.setZombieMembers(zombies)
val admins = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().admins.map { it.toString() }.toMutableSet()
admins.remove(TextSecurePreferences.getLocalNumber(this))
memberListAdapter.setLockedMembers(admins)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
mainContentContainer.visibility = if (allMembers.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (allMembers.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu()
}
@ -200,8 +211,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onMemberClick(member: String) {
val bottomSheet = ClosedGroupEditingOptionsBottomSheet()
bottomSheet.onRemoveTapped = {
val changedMembers = members - member
updateMembers(changedMembers)
if (zombies.contains(member)) zombies.remove(member)
else members.remove(member)
updateMembers()
bottomSheet.dismiss()
}
bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet")
@ -209,7 +221,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.usersToExcludeKey, members.toTypedArray())
intent.putExtra(SelectContactsActivity.usersToExcludeKey, allMembers.toTypedArray())
intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add")
startActivityForResult(intent, addUsersRequestCode)
}
@ -229,7 +241,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
private fun commitChanges() {
val hasMemberListChanges = (members != originalMembers)
val hasMemberListChanges = (allMembers != originalMembers)
if (!hasNameChanged && !hasMemberListChanges) {
return finish()
@ -237,15 +249,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
val name = if (hasNameChanged) this.name else originalName
val members = this.members.map {
val members = this.allMembers.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
val originalMembers = this.originalMembers.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
val admins = members.toSet() //TODO For now, consider all the users to be admins.
var isClosedGroup: Boolean
var groupPublicKey: String?
try {
@ -303,4 +313,6 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
}
}
class GroupMembers(val members: List<String>, val zombieMembers: List<String>) { }
}

View File

@ -4,12 +4,19 @@ import android.content.Context
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.AsyncLoader
class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<List<String>>(context) {
class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<EditClosedGroupActivity.GroupMembers>(context) {
override fun loadInBackground(): List<String> {
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true)
return members.map {
it.address.toString()
}
override fun loadInBackground(): EditClosedGroupActivity.GroupMembers {
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
val members = groupDatabase.getGroupMembers(groupID, true)
val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
return EditClosedGroupActivity.GroupMembers(
members.map {
it.address.toString()
},
zombieMembers.map {
it.address.toString()
}
)
}
}

View File

@ -7,15 +7,17 @@ import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.loki.views.UserView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
class EditClosedGroupMembersAdapter(
private val context: Context,
private val glide: GlideRequests,
private val admin: Boolean,
private val memberClickListener: ((String) -> Unit)? = null
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
private val members = ArrayList<String>()
private val lockedMembers = HashSet<String>()
private val zombieMembers = ArrayList<String>()
fun setMembers(members: Collection<String>) {
this.members.clear()
@ -23,9 +25,9 @@ class EditClosedGroupMembersAdapter(
notifyDataSetChanged()
}
fun setLockedMembers(members: Collection<String>) {
this.lockedMembers.clear()
this.lockedMembers.addAll(members)
fun setZombieMembers(members: Collection<String>) {
this.zombieMembers.clear()
this.zombieMembers.addAll(members)
notifyDataSetChanged()
}
@ -39,15 +41,20 @@ class EditClosedGroupMembersAdapter(
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val member = members[position]
val lockedMember = lockedMembers.contains(member)
val unlocked = admin && member != TextSecurePreferences.getLocalNumber(context)
viewHolder.view.bind(Recipient.from(
context,
Address.fromSerialized(member), false),
glide,
if (lockedMember) UserView.ActionIndicator.None else UserView.ActionIndicator.Menu)
if (unlocked) UserView.ActionIndicator.Menu else UserView.ActionIndicator.None)
if (!lockedMember) {
if (zombieMembers.contains(member))
viewHolder.view.alpha = 0.5F
else
viewHolder.view.alpha = 1F
if (unlocked) {
viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) }
}
}

View File

@ -30,10 +30,10 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.sending_receiving.MessageSender
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
import org.thoughtcrime.securesms.ApplicationContext
@ -139,7 +139,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val userDB = DatabaseFactory.getLokiUserDatabase(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
if (userPublicKey != null) {
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
MentionsManager.configureIfNeeded(userPublicKey, userDB)
application.publicChatManager.startPollersIfNeeded()
JobQueue.shared.resumePendingJobs()
}
@ -353,6 +353,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
withContext(Dispatchers.IO) {
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
if (publicChat != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
@ -364,6 +365,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ApplicationContext.getInstance(context).publicChatManager
.removeChat(publicChat.server, publicChat.channel)
} else if (openGroupV2 != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server)
apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server)
ApplicationContext.getInstance(context).publicChatManager
.removeChat(openGroupV2.server, openGroupV2.room)
} else {
threadDB.deleteConversation(threadID)
}

View File

@ -2,31 +2,49 @@ package org.thoughtcrime.securesms.loki.activities
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.activity.viewModels
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.view.isVisible
import androidx.fragment.app.*
import androidx.lifecycle.lifecycleScope
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.activity_join_public_chat.*
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import okhttp3.HttpUrl
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
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.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
import org.thoughtcrime.securesms.loki.viewmodel.State
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private val viewModel by viewModels<DefaultGroupsViewModel>()
private val adapter = JoinPublicChatActivityAdapter(this)
// region Lifecycle
@ -65,16 +83,43 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
}
fun joinPublicChatIfPossible(url: String) {
if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) {
return Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
}
// add http if just an IP style / host style URL is entered but leave it if scheme is included
val properString = if (!url.startsWith("http")) "http://$url" else url
val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
val room = httpUrl.pathSegments().firstOrNull()
val publicKey = httpUrl.queryParameter("public_key")
val isV2OpenGroup = !room.isNullOrEmpty()
showLoader()
val channel: Long = 1
lifecycleScope.launch(Dispatchers.IO) {
try {
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
val (threadID, groupID) = if (isV2OpenGroup) {
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
// non-standard port, add to server
this.port(httpUrl.port())
}
}.build()
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
threadID to groupID
} else {
val channel: Long = 1
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel)
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
threadID to groupID
}
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
withContext(Dispatchers.Main) {
// go to the new conversation and finish this one
openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false))
finish()
}
} catch (e: Exception) {
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
withContext(Dispatchers.Main) {
@ -83,10 +128,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
}
return@launch
}
withContext(Dispatchers.Main) { finish() }
}
}
// endregion
// region Convenience
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, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
context.startActivity(intent)
}
// endregion
}
// region Adapter
@ -109,7 +163,7 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
}
}
override fun getPageTitle(index: Int): CharSequence? {
override fun getPageTitle(index: Int): CharSequence {
return when (index) {
0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title)
1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
@ -122,24 +176,63 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
// region Enter Chat URL Fragment
class EnterChatURLFragment : Fragment() {
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
}
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
defaultRoomsGridLayout.removeAllViews()
groups.forEach { defaultGroup ->
val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip
val drawable = defaultGroup.image?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size)
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
isCircular = true
}
}
chip.chipIcon = drawable
chip.text = defaultGroup.name
chip.setOnClickListener {
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl())
}
defaultRoomsGridLayout.addView(chip)
}
if (groups.size and 1 != 0) {
// add a filler weight 1 view
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
defaultRoomsParent.isVisible = state is State.Success
defaultRoomsLoader.isVisible = state is State.Loading
when (state) {
State.Loading -> {
// show a loader here probs
}
is State.Error -> {
// hide the loader and the
}
is State.Success -> {
populateDefaultGroups(state.value)
}
}
}
}
// region Convenience
private fun joinPublicChatIfPossible() {
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
var chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
if (!chatURL.toLowerCase().startsWith("https")) {
chatURL = "https://$chatURL"
}
val chatURL = chatURLEditText.text.trim().toString().toLowerCase()
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
}
// endregion
}
// endregion

View File

@ -27,7 +27,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log
@ -45,7 +45,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
private var restoreJob: Job? = null
override fun onBackPressed() {
if (restoreJob?.isActive == true) return // don't allow going back with pending job
if (restoreJob?.isActive == true) return // Don't allow going back with a pending job
super.onBackPressed()
}
@ -53,14 +53,12 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply {
setHasViewedSeed(this@LinkDeviceActivity, true)
setConfigurationMessageSynced(this@LinkDeviceActivity, false)
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
}
// registration variables are synced
setContentView(R.layout.activity_link_device)
viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager)

View File

@ -19,12 +19,12 @@ import android.widget.Toast
import androidx.annotation.ColorRes
import kotlinx.android.synthetic.main.activity_path.*
import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.PathDotView
import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsignal.service.loki.Snode
class PathActivity : PassphraseRequiredActionBarActivity() {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.BaseActionBarActivity
@ -30,14 +30,12 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply {
setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true)
setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false)
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
}
// registration variables are synced
setContentView(R.layout.activity_recovery_phrase_restore)
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
restoreButton.setOnClickListener { restore() }

View File

@ -39,14 +39,12 @@ class RegisterActivity : BaseActionBarActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply {
setHasViewedSeed(this@RegisterActivity, false)
setConfigurationMessageSynced(this@RegisterActivity, true)
setRestorationTime(this@RegisterActivity, 0)
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
}
// registration variables are synced
registerButton.setOnClickListener { register() }
copyButton.setOnClickListener { copyPublicKey() }
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")

View File

@ -16,7 +16,7 @@ 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
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
class SeedActivity : BaseActionBarActivity() {

View File

@ -36,6 +36,27 @@ class SelectContactsAdapter(private val context: Context, private val glide: Gli
isSelected)
}
override fun onBindViewHolder(viewHolder: ViewHolder,
position: Int,
payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
// Because these updates can be batched,
// there can be multiple payloads for a single bind
when (payloads[0]) {
Payload.MEMBER_CLICKED -> {
val member = members[position]
val isSelected = selectedMembers.contains(member)
viewHolder.view.toggleCheckbox(isSelected)
}
}
} else {
// When payload list is empty,
// or we don't have logic to handle a given type,
// default to full bind:
this.onBindViewHolder(viewHolder, position)
}
}
private fun onMemberClick(member: String) {
if (selectedMembers.contains(member)) {
selectedMembers.remove(member)
@ -43,6 +64,11 @@ class SelectContactsAdapter(private val context: Context, private val glide: Gli
selectedMembers.add(member)
}
val index = members.indexOf(member)
notifyItemChanged(index)
notifyItemChanged(index, Payload.MEMBER_CLICKED)
}
}
// define below the different events used to notify the adapter
enum class Payload {
MEMBER_CLICKED
}
}

View File

@ -28,13 +28,13 @@ import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.alwaysUi
import org.session.libsession.messaging.avatars.AvatarHelper
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.api.util.StreamDetails
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection

View File

@ -8,13 +8,16 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.opengroups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.api.SnodeAPI
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import java.io.IOException
import java.util.concurrent.TimeUnit
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
@ -70,7 +73,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
// Private chats
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes ->
val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
envelopes.map { envelope ->
MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
}
@ -78,7 +81,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
promises.addAll(privateChatsPromise.get())
// Closed groups
promises.addAll(ApplicationContext.getInstance(context).closedGroupPoller.pollOnce())
promises.addAll(ClosedGroupPoller().pollOnce())
// Open Groups
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { (_,chat)->
@ -89,6 +92,14 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
promises.add(poller.pollForNewMessages())
}
val openGroupsV2 = DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups().values.groupBy(OpenGroupV2::server)
openGroupsV2.values.map { groups ->
OpenGroupV2Poller(groups)
}.forEach { poller ->
promises.add(poller.compactPoll(true).map{ /*Unit*/ })
}
// Wait till all the promises get resolved
all(promises).get()

View File

@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.loki.api
import android.content.Context
import nl.komponents.kovenant.functional.map
import okhttp3.*
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.api.PushNotificationAPI
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsignal.service.loki.utilities.retryIfNeeded
object LokiPushNotificationManager {
@ -16,10 +16,10 @@ object LokiPushNotificationManager {
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
private val server by lazy {
PushNotificationAPI.shared.server
PushNotificationAPI.server
}
private val pnServerPublicKey by lazy {
PushNotificationAPI.pnServerPublicKey
PushNotificationAPI.serverPublicKey
}
enum class ClosedGroupOperation {

View File

@ -2,9 +2,9 @@ package org.thoughtcrime.securesms.loki.api
import android.content.Context
import androidx.work.*
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.session.libsignal.service.loki.api.opengroups.PublicChat
/**
* Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager.
@ -16,6 +16,25 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
private const val DATA_KEY_SERVER_URL = "server_uRL"
private const val DATA_KEY_CHANNEL = "channel"
private const val DATA_KEY_ROOM = "room"
@JvmStatic
fun scheduleInstant(context: Context, serverUrl: String, room :String) {
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setInputData(workDataOf(
DATA_KEY_SERVER_URL to serverUrl,
DATA_KEY_ROOM to room
))
.build()
WorkManager
.getInstance(context)
.enqueue(workRequest)
}
@JvmStatic
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
@ -39,17 +58,35 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
override fun doWork(): Result {
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
val room = inputData.getString(DATA_KEY_ROOM)
val publicChatId = PublicChat.getId(channel, serverUrl)
val isOpenGroupV2 = !room.isNullOrEmpty() && channel == -1L
if (!isOpenGroupV2) {
val publicChatId = OpenGroup.getId(channel, serverUrl)
return try {
Log.v(TAG, "Updating open group info for $publicChatId.")
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
Result.failure()
}
} else {
val openGroupId = "$serverUrl.$room"
return try {
Log.v(TAG, "Updating open group info for $openGroupId.")
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
Result.failure()
}
return try {
Log.v(TAG, "Updating open group info for $publicChatId.")
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
Result.failure()
}
}
}

View File

@ -5,14 +5,12 @@ import android.database.ContentObserver
import android.graphics.Bitmap
import android.text.TextUtils
import androidx.annotation.WorkerThread
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.opengroups.OpenGroup
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsession.messaging.opengroups.OpenGroupInfo
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.*
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.Util
import org.session.libsignal.service.loki.api.opengroups.PublicChat
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
@ -21,15 +19,17 @@ import java.util.concurrent.Executors
class PublicChatManager(private val context: Context) {
private var chats = mutableMapOf<Long, OpenGroup>()
private var v2Chats = mutableMapOf<Long, OpenGroupV2>()
private val pollers = mutableMapOf<Long, OpenGroupPoller>()
private val v2Pollers = mutableMapOf<String, OpenGroupV2Poller>()
private val observers = mutableMapOf<Long, ContentObserver>()
private var isPolling = false
private val executorService = Executors.newScheduledThreadPool(16)
private val executorService = Executors.newScheduledThreadPool(4)
public fun areAllCaughtUp(): Boolean {
var areAllCaughtUp = true
refreshChatsAndPollers()
for ((threadID, chat) in chats) {
for ((threadID, _) in chats) {
val poller = pollers[threadID]
areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true
}
@ -53,6 +53,17 @@ class PublicChatManager(private val context: Context) {
listenToThreadDeletion(threadId)
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
}
v2Pollers.values.forEach { it.stop() }
v2Pollers.clear()
v2Chats.entries.groupBy { (_, group) -> group.server }.forEach { (server, threadedRooms) ->
val poller = OpenGroupV2Poller(threadedRooms.map { it.value }, executorService)
poller.startIfNeeded()
threadedRooms.forEach { (thread, _) ->
listenToThreadDeletion(thread)
}
v2Pollers[server] = poller
}
isPolling = true
}
@ -75,7 +86,7 @@ class PublicChatManager(private val context: Context) {
@WorkerThread
public fun addChat(server: String, channel: Long, info: OpenGroupInfo): OpenGroup {
val chat = PublicChat(channel, server, info.displayName, true)
val chat = OpenGroup(channel, server, info.displayName, true)
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
var profilePicture: Bitmap? = null
// Create the group if we don't have one
@ -96,12 +107,42 @@ class PublicChatManager(private val context: Context) {
// Start polling
Util.runOnMain { startPollersIfNeeded() }
return OpenGroup.from(chat)
return chat
}
@WorkerThread
fun addChat(server: String, room: String, info: OpenGroupAPIV2.Info, publicKey: String): OpenGroupV2 {
val chat = OpenGroupV2(server, room, info.name, publicKey)
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
val profilePicture: Bitmap?
if (threadID < 0) {
val profilePictureAsByteArray = try {
OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id,server).get()
} catch (e: Exception) {
null
}
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, info.name)
threadID = result.threadId
}
DatabaseFactory.getLokiThreadDatabase(context).setOpenGroupChat(chat, threadID)
Util.runOnMain { startPollersIfNeeded() }
return chat
}
public fun removeChat(server: String, channel: Long) {
val threadDB = DatabaseFactory.getThreadDatabase(context)
val groupId = PublicChat.getId(channel, server)
val groupId = OpenGroup.getId(channel, server)
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
GroupManager.deleteGroup(groupAddress, context)
Util.runOnMain { startPollersIfNeeded() }
}
fun removeChat(server: String, room: String) {
val threadDB = DatabaseFactory.getThreadDatabase(context)
val groupId = "$server.$room"
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
GroupManager.deleteGroup(groupAddress, context)
@ -110,13 +151,15 @@ class PublicChatManager(private val context: Context) {
}
private fun refreshChatsAndPollers() {
val storage = MessagingConfiguration.shared.storage
val storage = MessagingModuleConfiguration.shared.storage
val chatsInDB = storage.getAllOpenGroups()
val v2ChatsInDB = storage.getAllV2OpenGroups()
val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }
removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
// Only append to chats if we have a thread for the chat
chats = chatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
v2Chats = v2ChatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
}
private fun listenToThreadDeletion(threadID: Long) {
@ -133,6 +176,8 @@ class PublicChatManager(private val context: Context) {
DatabaseFactory.getLokiThreadDatabase(context).removePublicChat(threadID)
pollers.remove(threadID)?.stop()
v2Pollers.values.forEach { it.stop() }
v2Pollers.clear()
observers.remove(threadID)
startPollersIfNeeded()
}

View File

@ -6,8 +6,8 @@ import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.api.MessageWrapper
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.notifications.NotificationChannels

View File

@ -7,8 +7,8 @@ import org.session.libsession.utilities.TextSecurePreferences
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.service.loki.api.Snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.Snode
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
@ -27,7 +27,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val timestamp = "timestamp"
private val snode = "snode"
// Snode pool
private val snodePoolTable = "loki_snode_pool_cache"
public val snodePoolTable = "loki_snode_pool_cache"
private val dummyKey = "dummy_key"
private val snodePool = "snode_pool_key"
@JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);"
@ -36,7 +36,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val indexPath = "index_path"
@JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
// Swarms
private val swarmTable = "loki_api_swarm_cache"
public val swarmTable = "loki_api_swarm_cache"
private val swarmPublicKey = "hex_encoded_public_key"
private val swarm = "swarm"
@JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
@ -286,6 +286,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}?.toLong()
}
override fun getLastMessageServerID(room: String, server: String): Long? {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
cursor.getInt(lastMessageServerID)
}?.toLong()
}
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
@ -293,12 +301,25 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
}
override fun setLastMessageServerID(room: String, server: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() ))
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
}
fun removeLastMessageServerID(group: Long, server: String) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index))
}
fun removeLastMessageServerID(room: String, server:String) {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index))
}
override fun getLastDeletionServerID(group: Long, server: String): Long? {
val database = databaseHelper.readableDatabase
val index = "$server.$group"
@ -307,6 +328,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}?.toLong()
}
override fun getLastDeletionServerID(room: String, server: String): Long? {
val database = databaseHelper.readableDatabase
val index = "$server.$room"
return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor ->
cursor.getInt(lastDeletionServerID)
}?.toLong()
}
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
@ -314,6 +343,19 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
}
override fun setLastDeletionServerID(room: String, server: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
val row = wrap(mapOf(lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString()))
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
}
fun removeLastDeletionServerID(room: String, server: String) {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
database.delete(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index))
}
fun removeLastDeletionServerID(group: Long, server: String) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
@ -328,6 +370,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}?.toInt()
}
fun getUserCount(room: String, server: String): Int? {
val database = databaseHelper.readableDatabase
val index = "$server.$room"
return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor ->
cursor.getInt(userCount)
}?.toInt()
}
override fun setUserCount(group: Long, server: String, newValue: Int) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
@ -335,6 +385,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
}
override fun setUserCount(room: String, server: String, newValue: Int) {
val database = databaseHelper.writableDatabase
val index = "$server.$room"
val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() ))
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
}
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
val database = databaseHelper.readableDatabase
return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->

View File

@ -2,15 +2,11 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues
import android.content.Context
import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getInt
import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol
import org.thoughtcrime.securesms.loki.utilities.*
import org.session.libsignal.service.loki.LokiMessageDatabaseProtocol
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
@ -23,56 +19,111 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
private val friendRequestStatus = "friend_request_status"
private val threadID = "thread_id"
private val errorMessage = "error_message"
@JvmStatic val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
@JvmStatic val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
private val messageType = "message_type"
@JvmStatic
val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic
val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
@JvmStatic
val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
@JvmStatic
val updateMessageIDTableForType = "ALTER TABLE $messageIDTable ADD COLUMN $messageType INTEGER DEFAULT 0; ALTER TABLE $messageIDTable ADD CONSTRAINT PK_$messageIDTable PRIMARY KEY ($messageID, $serverID);"
@JvmStatic
val updateMessageMappingTable = "ALTER TABLE $messageThreadMappingTable ADD COLUMN $serverID INTEGER DEFAULT 0; ALTER TABLE $messageThreadMappingTable ADD CONSTRAINT PK_$messageThreadMappingTable PRIMARY KEY ($messageID, $serverID);"
const val SMS_TYPE = 0
const val MMS_TYPE = 1
}
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
return if (message != null) getServerID(message.getId()) else null
return if (message != null) getServerID(message.getId(), !message.isMms) else null
}
fun getServerID(messageID: Long): Long? {
val database = databaseHelper.readableDatabase
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
cursor.getInt(serverID)
}?.toLong()
}
fun getServerID(messageID: Long, isSms: Boolean): Long? {
val database = databaseHelper.readableDatabase
return database.get(messageIDTable, "${Companion.messageID} = ? AND $messageType = ?", arrayOf(messageID.toString(), if (isSms) SMS_TYPE.toString() else MMS_TYPE.toString())) { cursor ->
cursor.getInt(serverID)
}?.toLong()
}
fun getMessageID(serverID: Long): Long? {
val database = databaseHelper.readableDatabase
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf(serverID.toString())) { cursor ->
cursor.getInt(messageID)
}?.toLong()
}
override fun setServerID(messageID: Long, serverID: Long) {
fun deleteMessage(messageID: Long, isSms: Boolean) {
val database = databaseHelper.writableDatabase
val serverID = database.get(messageIDTable,
"${Companion.messageID} = ? AND ${Companion.messageType} = ?",
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
cursor.getInt(Companion.serverID).toLong()
} ?: return
database.beginTransaction()
database.delete(messageIDTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
database.delete(messageThreadMappingTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
database.setTransactionSuccessful()
database.endTransaction()
}
fun getMessageID(serverID: Long, threadID: Long): Pair<Long, Boolean>? {
val database = databaseHelper.readableDatabase
val mappingResult = database.get(messageThreadMappingTable, "${Companion.serverID} = ? AND ${Companion.threadID} = ?",
arrayOf(serverID.toString(), threadID.toString())) { cursor ->
cursor.getInt(messageID) to cursor.getInt(Companion.serverID)
} ?: return null
val (mappedID, mappedServerID) = mappingResult
return database.get(messageIDTable,
"$messageID = ? AND ${Companion.serverID} = ?",
arrayOf(mappedID.toString(), mappedServerID.toString())) { cursor ->
cursor.getInt(Companion.messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE)
}
}
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID)
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE)
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
}
fun getOriginalThreadID(messageID: Long): Long {
val database = databaseHelper.readableDatabase
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
cursor.getInt(threadID)
}?.toLong() ?: -1L
}
fun setOriginalThreadID(messageID: Long, threadID: Long) {
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID)
contentValues.put(Companion.threadID, threadID)
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
}
fun getErrorMessage(messageID: Long): String? {
val database = databaseHelper.readableDatabase
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
cursor.getString(errorMessage)
}
}
@ -82,6 +133,6 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.errorMessage, errorMessage)
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
}
}

View File

@ -3,22 +3,20 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.session.libsession.messaging.open_groups.OpenGroup
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.api.opengroups.PublicChat
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol {
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object {
private val sessionResetTable = "loki_thread_session_reset_database"
@ -27,31 +25,55 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
private val friendRequestStatus = "friend_request_status"
private val sessionResetStatus = "session_reset_status"
val publicChat = "public_chat"
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
@JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
@JvmStatic
val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
@JvmStatic
val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
}
override fun getThreadID(hexEncodedPublicKey: String): Long {
fun getThreadID(hexEncodedPublicKey: String): Long {
val address = Address.fromSerialized(hexEncodedPublicKey)
val recipient = Recipient.from(context, address, false)
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
}
fun getAllPublicChats(): Map<Long, PublicChat> {
fun getAllPublicChats(): Map<Long, OpenGroup> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
val result = mutableMapOf<Long, PublicChat>()
val result = mutableMapOf<Long, OpenGroup>()
try {
cursor = database.rawQuery("select * from $publicChatTable", null)
while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat)
val publicChat = PublicChat.fromJSON(string)
if (publicChat != null) { result[threadID] = publicChat }
val publicChat = OpenGroup.fromJSON(string)
if (publicChat != null) {
result[threadID] = publicChat
}
}
} catch (e: Exception) {
// Do nothing
} finally {
} finally {
cursor?.close()
}
return result
}
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
val result = mutableMapOf<Long, OpenGroupV2>()
try {
cursor = database.rawQuery("select * from $publicChatTable", null)
while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat)
val openGroup = OpenGroupV2.fromJson(string)
if (openGroup != null) result[threadID] = openGroup
}
} catch (e: Exception) {
// do nothing
} finally {
cursor?.close()
}
return result
@ -61,25 +83,50 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) }
}
override fun getPublicChat(threadID: Long): PublicChat? {
fun getPublicChat(threadID: Long): OpenGroup? {
if (threadID < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
val publicChatAsJSON = cursor.getString(publicChat)
PublicChat.fromJSON(publicChatAsJSON)
OpenGroup.fromJSON(publicChatAsJSON)
}
}
override fun setPublicChat(publicChat: PublicChat, threadID: Long) {
if (threadID < 0) { return }
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
if (threadID < 0) {
return null
}
val database = databaseHelper.readableDatabase
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
val json = cursor.getString(publicChat)
OpenGroupV2.fromJson(json)
}
}
fun setOpenGroupChat(openGroupV2: OpenGroupV2, threadID: Long) {
if (threadID < 0) {
return
}
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
}
fun setPublicChat(publicChat: OpenGroup, threadID: Long) {
if (threadID < 0) {
return
}
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
}
override fun removePublicChat(threadID: Long) {
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
fun removePublicChat(threadID: Long) {
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
}
}

View File

@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol
import org.session.libsignal.service.loki.LokiUserDatabaseProtocol
class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiUserDatabaseProtocol {

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.dialog_seed.view.*
import network.loki.messenger.R
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.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey

View File

@ -9,7 +9,6 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
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.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseFactory
@ -32,6 +31,7 @@ import org.session.libsession.utilities.TextSecurePreferences
import java.util.*
object ClosedGroupsProtocolV2 {
@JvmStatic
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
@ -41,7 +41,6 @@ object ClosedGroupsProtocolV2 {
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.UPDATE -> handleClosedGroupUpdate(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey)
else -> {
Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}")
@ -65,7 +64,6 @@ object ClosedGroupsProtocolV2 {
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
senderPublicKey.isNotEmpty()
}
DataMessage.ClosedGroupControlMessage.Type.UPDATE,
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> {
!closedGroupUpdate.name.isNullOrEmpty()
}
@ -104,11 +102,11 @@ object ClosedGroupsProtocolV2 {
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user (if we didn't make the group)
if (userPublicKey != senderPublicKey) {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, 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)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
}
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
@ -156,14 +154,14 @@ object ClosedGroupsProtocolV2 {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
}
}
val (contextType, signalType) =
if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
val type =
if (senderLeft) SignalServiceGroup.Type.QUIT
else SignalServiceGroup.Type.UPDATE
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
}
}
@ -191,9 +189,9 @@ object ClosedGroupsProtocolV2 {
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
if (userPublicKey in admins) {
// send current encryption key to the latest added members
@ -230,9 +228,9 @@ object ClosedGroupsProtocolV2 {
// Notify the user
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
}
@ -272,9 +270,9 @@ object ClosedGroupsProtocolV2 {
// Notify user
if (userLeft) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
}
}
@ -385,14 +383,13 @@ object ClosedGroupsProtocolV2 {
}
// Notify the user
val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
val admins = group.admins.map { it.toString() }
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
} else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
}
}
// endregion

View File

@ -2,24 +2,13 @@ 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.MessagingModuleConfiguration
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.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.thoughtcrime.securesms.ApplicationContext
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
object MultiDeviceProtocol {
@ -28,7 +17,7 @@ object MultiDeviceProtocol {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
val now = System.currentTimeMillis()
if (now - lastSyncTime < 2 * 24 * 60 * 60 * 1000) return
if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return
val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
!recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
}.map { recipient ->
@ -51,80 +40,4 @@ object MultiDeviceProtocol {
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
}
// TODO: remove this after we migrate to new message receiving pipeline
@JvmStatic
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
synchronized(this) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, timestamp)) return
if (senderPublicKey != userPublicKey) return
TextSecurePreferences.setConfigurationMessageSynced(context, true)
TextSecurePreferences.setLastProfileUpdateTime(context, timestamp)
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
val storage = MessagingConfiguration.shared.storage
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
val ourRecipient = Recipient.from(context, Address.fromSerialized(userPublicKey),false)
for (closedGroup in configurationMessage.closedGroups) {
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
closedGroupUpdate.name = closedGroup.name
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.privateKey.serialize())
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in configurationMessage.openGroups) {
if (allOpenGroups.contains(openGroup)) continue
OpenGroupUtilities.addGroup(context, openGroup, 1)
}
if (configurationMessage.displayName.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, configurationMessage.displayName)
recipientDatabase.setProfileName(ourRecipient, configurationMessage.displayName)
}
if (configurationMessage.profileKey.isNotEmpty()) {
val profileKey = Base64.encodeBytes(configurationMessage.profileKey)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
recipientDatabase.setProfileKey(ourRecipient, configurationMessage.profileKey)
if (!configurationMessage.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != configurationMessage.profilePicture) {
TextSecurePreferences.setProfilePictureURL(context, configurationMessage.profilePicture)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, configurationMessage.profilePicture))
}
}
for (contact in configurationMessage.contacts) {
val address = Address.fromSerialized(contact.publicKey)
val recipient = Recipient.from(context, address, true)
if (!contact.profilePicture.isNullOrEmpty()) {
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
}
if (contact.profileKey?.isNotEmpty() == true) {
recipientDatabase.setProfileKey(recipient, contact.profileKey)
}
if (contact.name.isNotEmpty()) {
recipientDatabase.setProfileName(recipient, contact.name)
}
recipientDatabase.setProfileSharing(recipient, true)
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
// create Thread if needed
threadDatabase.getOrCreateThreadIdFor(recipient)
}
if (configurationMessage.contacts.isNotEmpty()) {
threadDatabase.notifyUpdatedFromConfig()
}
}
}
}

View File

@ -24,6 +24,15 @@ object SessionMetaProtocol {
timestamps.add(timestamp)
}
@JvmStatic
fun clearReceivedMessages() {
timestamps.clear()
}
fun removeTimestamps(timestamps: Set<Long>) {
this.timestamps.removeAll(timestamps)
}
@JvmStatic
fun shouldIgnoreMessage(timestamp: Long): Boolean {
val shouldIgnoreMessage = timestamps.contains(timestamp)

View File

@ -3,8 +3,9 @@ package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.session.libsignal.service.loki.Broadcaster
class Broadcaster(private val context: Context) : org.session.libsignal.service.loki.utilities.Broadcaster {
class Broadcaster(private val context: Context) : Broadcaster {
override fun broadcast(event: String) {
val intent = Intent(event)

View File

@ -1,103 +0,0 @@
package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64
import org.session.libsignal.service.internal.push.SignalServiceProtos
import java.util.*
import network.loki.messenger.R
import org.session.libsignal.utilities.logging.Log
import java.io.IOException
class GroupDescription(context: Context, groupContext: SignalServiceProtos.GroupContext?) {
private val context: Context
private val groupContext: SignalServiceProtos.GroupContext?
private val newMembers: MutableList<Recipient>
private val removedMembers: MutableList<Recipient>
private var wasCurrentUserRemoved: Boolean = false
private fun toRecipient(hexEncodedPublicKey: String): Recipient {
val address = Address.fromSerialized(hexEncodedPublicKey)
return Recipient.from(context, address, false)
}
fun toString(sender: Recipient): String {
if (wasCurrentUserRemoved) {
return context.getString(R.string.GroupUtil_you_were_removed_from_group)
}
val description = StringBuilder()
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString()))
if (groupContext == null) {
return description.toString()
}
val title = groupContext.name
if (!newMembers.isEmpty()) {
description.append("\n")
description.append(context.resources.getQuantityString(R.plurals.GroupUtil_joined_the_group,
newMembers.size, toString(newMembers)))
}
if (!removedMembers.isEmpty()) {
description.append("\n")
description.append(context.resources.getQuantityString(R.plurals.GroupUtil_removed_from_the_group,
removedMembers.size, toString(removedMembers)))
}
if (title != null && !title.trim { it <= ' ' }.isEmpty()) {
val separator = if (!newMembers.isEmpty() || !removedMembers.isEmpty()) " " else "\n"
description.append(separator)
description.append(context.getString(R.string.GroupUtil_group_name_is_now, title))
}
return description.toString()
}
fun addListener(listener: RecipientModifiedListener?) {
if (!newMembers.isEmpty()) {
for (member in newMembers) {
member.addListener(listener)
}
}
}
private fun toString(recipients: List<Recipient>): String {
var result = ""
for (i in recipients.indices) {
result += recipients[i].toShortString()
if (i != recipients.size - 1) result += ", "
}
return result
}
init {
this.context = context.applicationContext
this.groupContext = groupContext
newMembers = LinkedList()
removedMembers = LinkedList()
if (groupContext != null) {
val newMembers = groupContext.newMembersList
for (member in newMembers) {
this.newMembers.add(toRecipient(member))
}
val removedMembers = groupContext.removedMembersList
for (member in removedMembers) {
this.removedMembers.add(toRecipient(member))
}
wasCurrentUserRemoved = removedMembers.contains(TextSecurePreferences.getLocalNumber(context))
}
}
companion object {
fun getDescription(context: Context, encodedGroup: String?): GroupDescription {
return if (encodedGroup == null) {
GroupDescription(context, null)
} else try {
val groupContext = SignalServiceProtos.GroupContext.parseFrom(Base64.decode(encodedGroup))
GroupDescription(context, groupContext)
} catch (e: IOException) {
Log.w("Loki", e)
GroupDescription(context, null)
}
}
}
}

View File

@ -7,7 +7,7 @@ import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.session.libsignal.utilities.logging.Log
import com.opencsv.CSVReader
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.ThreadUtils
import java.io.File
import java.io.FileOutputStream

View File

@ -1,10 +1,10 @@
package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.messaging.mentions.MentionsManager
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
object MentionManagerUtilities {

View File

@ -1,10 +1,6 @@
package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
object MnemonicUtilities {

View File

@ -3,30 +3,56 @@ package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import androidx.annotation.WorkerThread
import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.opengroups.OpenGroup
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.loki.api.opengroups.PublicChat
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import java.util.*
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
object OpenGroupUtilities {
private const val TAG = "OpenGroupUtilities"
@JvmStatic
@WorkerThread
fun addGroup(context: Context, server: String, room: String, publicKey: String): OpenGroupV2 {
val groupId = "$server.$room"
val threadID = GroupManager.getOpenGroupThreadID(groupId, context)
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
if (openGroup != null) return openGroup
MessagingModuleConfiguration.shared.storage.setOpenGroupPublicKey(server,publicKey)
OpenGroupAPIV2.getAuthToken(room, server).get()
val groupInfo = OpenGroupAPIV2.getInfo(room,server).get()
val application = ApplicationContext.getInstance(context)
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
val storage = MessagingModuleConfiguration.shared.storage
storage.removeLastDeletionServerId(room, server)
storage.removeLastMessageServerId(room, server)
return group
}
@JvmStatic
@WorkerThread
@Throws(Exception::class)
fun addGroup(context: Context, url: String, channel: Long): OpenGroup {
// Check for an existing group.
val groupID = PublicChat.getId(channel, url)
val groupID = OpenGroup.getId(channel, url)
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
if (openGroup != null) { return OpenGroup.from(openGroup) }
if (openGroup != null) { return openGroup }
// Add the new group.
val application = ApplicationContext.getInstance(context)
@ -56,7 +82,7 @@ object OpenGroupUtilities {
@Throws(Exception::class)
fun updateGroupInfo(context: Context, url: String, channel: Long) {
// Check if open group has a related DB record.
val groupId = GroupUtil.getEncodedOpenGroupID(PublicChat.getId(channel, url).toByteArray())
val groupId = GroupUtil.getEncodedOpenGroupID(OpenGroup.getId(channel, url).toByteArray())
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
}
@ -68,5 +94,30 @@ object OpenGroupUtilities {
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
}
data class GroupInfoUpdatedEvent(val url: String, val channel: Long)
@JvmStatic
@WorkerThread
@Throws(Exception::class)
fun updateGroupInfo(context: Context, server: String, room: String) {
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
}
val info = OpenGroupAPIV2.getInfo(room, server).get() // store info again?
OpenGroupAPIV2.getMemberCount(room, server).get()
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
}
/**
* Return a generated name for users in the style of `$name (...$hex.takeLast(8))` for public groups
*/
@JvmStatic
fun getDisplayName(recipient: Recipient): String {
return String.format(Locale.ROOT, PUBLIC_GROUP_STRING_FORMAT, recipient.name, recipient.address.serialize().takeLast(8))
}
const val PUBLIC_GROUP_STRING_FORMAT = "%s (...%s)"
data class GroupInfoUpdatedEvent(val url: String, val channel: Long = -1, val room: String = "")
}

View File

@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.loki.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
typealias GroupState = State<DefaultGroups>
class DefaultGroupsViewModel : ViewModel() {
init {
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
}
val defaultRooms = OpenGroupAPIV2.defaultRooms.map<DefaultGroups, GroupState> {
State.Success(it)
}.onStart {
emit(State.Loading)
}.asLiveData()
}

View File

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.loki.viewmodel
sealed class State<out T> {
object Loading : State<Nothing>()
data class Success<T>(val value: T): State<T>()
data class Error(val error: Exception): State<Nothing>()
}

View File

@ -10,7 +10,7 @@ import android.widget.ListView
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsignal.service.loki.utilities.mentions.Mention
import org.session.libsignal.service.loki.Mention
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var mentionCandidates = listOf<Mention>()

View File

@ -8,8 +8,8 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsignal.service.loki.utilities.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsignal.service.loki.Mention
import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {

View File

@ -11,9 +11,9 @@ import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
class PathStatusView : View {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>()

View File

@ -11,10 +11,10 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.view_profile_picture.view.*
import network.loki.messenger.R
import org.session.libsession.messaging.avatars.ProfileContactPhoto
import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.mms.GlideRequests
@ -146,13 +146,13 @@ class ProfilePictureView : RelativeLayout {
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return
if (imagesCached.contains(publicKey)) 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(recipient.profileAvatar.orEmpty())
imagesCached.add(publicKey)
} 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(recipient.profileAvatar.orEmpty())
imagesCached.add(publicKey)
}
} else {
imageView.setImageDrawable(null)

View File

@ -82,8 +82,13 @@ class UserView : LinearLayout {
}
}
fun toggleCheckbox(isSelected: Boolean = false) {
actionIndicatorImageView.visibility = View.VISIBLE
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
}
fun unbind() {
}
// endregion
}
}

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
import org.session.libsession.messaging.file_server.FileServerAPI;
public class PushMediaConstraints extends MediaConstraints {

View File

@ -102,7 +102,8 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
if (duration == 0) {
TextSecurePreferences.setScreenLockTimeout(getContext(), 0);
} else {
long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration);
// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
}

View File

@ -2,22 +2,17 @@ package org.thoughtcrime.securesms.service;
import android.content.Context;
import com.google.protobuf.ByteString;
import org.jetbrains.annotations.NotNull;
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.OutgoingExpirationUpdateMessage;
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.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
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.GroupContext;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -28,7 +23,6 @@ import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.TreeSet;
import java.util.concurrent.Executor;
@ -79,8 +73,8 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
String senderPublicKey = message.getSender();
// Notify the user
if (userPublicKey.equals(senderPublicKey)) {
// sender is a linked device
if (senderPublicKey == null || userPublicKey.equals(senderPublicKey)) {
// sender is self or a linked device
insertOutgoingExpirationTimerMessage(message);
} else {
insertIncomingExpirationTimerMessage(message);
@ -100,7 +94,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
int duration = message.getDuration();
Optional<SignalServiceGroup> groupInfo = Optional.absent();
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
Address address = Address.fromSerialized(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
@ -123,6 +117,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
//insert the timer update message
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
@ -138,41 +133,21 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
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);
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
Recipient recipient = Recipient.from(context, address, false);
try {
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp);
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,
null,
Collections.emptyList(),
message.getSentTimestamp(),
-1,
duration * 1000L,
true,
DistributionTypes.DEFAULT,
null,
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList());
database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp);
}
//set the timer to the conversation
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);

View File

@ -119,7 +119,6 @@ public class KeyCachingService extends Service {
KeyCachingService.masterSecret = masterSecret;
foregroundService();
startTimeoutIfAppropriate(this);
new AsyncTask<Void, Void, Void>() {
@Override
@ -210,7 +209,7 @@ public class KeyCachingService extends Service {
boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context);
long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context);
boolean screenLockActive = screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context);
boolean screenLockActive = screenTimeout >= 0 && TextSecurePreferences.isScreenLockEnabled(context);
if (!appVisible && secretSet && (passLockActive || screenLockActive)) {
long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context);

View File

@ -1,4 +0,0 @@
package org.thoughtcrime.securesms.sskenvironment
class DataExtractionNotificationManager {
}

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -22,7 +21,36 @@
android:layout_marginTop="@dimen/large_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:inputType="textWebEmailAddress"
android:hint="Enter an open group URL" />
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
<com.github.ybq.android.spinkit.SpinKitView
android:visibility="gone"
android:id="@+id/defaultRoomsLoader"
style="@style/SpinKitView.Small.WanderingCubes"
android:layout_marginVertical="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:visibility="gone"
android:paddingHorizontal="24dp"
android:id="@+id/defaultRoomsParent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_marginVertical="16dp"
android:textSize="18sp"
android:textStyle="bold"
android:text="@string/activity_join_public_chat_join_rooms"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<GridLayout
android:id="@+id/defaultRoomsGridLayout"
android:columnCount="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<View
android:layout_width="0dp"

View File

@ -98,13 +98,6 @@
android:layout_height="match_parent"
android:background="@drawable/home_activity_gradient" />
<org.thoughtcrime.securesms.loki.views.NewConversationButtonSetView
android:id="@+id/newConversationButtonSet"
android:layout_width="276dp"
android:layout_height="236dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true" />
<LinearLayout
android:id="@+id/emptyStateContainer"
android:layout_width="wrap_content"
@ -131,6 +124,13 @@
</LinearLayout>
<org.thoughtcrime.securesms.loki.views.NewConversationButtonSetView
android:id="@+id/newConversationButtonSet"
android:layout_width="276dp"
android:layout_height="236dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true" />
</RelativeLayout>
</LinearLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:theme="@style/Theme.MaterialComponents.DayNight"
style="?attr/chipStyle"
app:chipStartPadding="6dp"
android:layout_columnWeight="1"
android:layout_marginHorizontal="2dp"
tools:text="Main Group"
android:ellipsize="end"
tools:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="52dp" />

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -24,6 +23,35 @@
android:inputType="textWebEmailAddress"
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
<com.github.ybq.android.spinkit.SpinKitView
android:visibility="gone"
android:id="@+id/defaultRoomsLoader"
style="@style/SpinKitView.Small.WanderingCubes"
android:layout_marginVertical="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:visibility="gone"
android:paddingHorizontal="24dp"
android:id="@+id/defaultRoomsParent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_marginVertical="16dp"
android:textSize="18sp"
android:textStyle="bold"
android:text="@string/activity_join_public_chat_join_rooms"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<GridLayout
android:id="@+id/defaultRoomsGridLayout"
android:columnCount="2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_columnWeight="1"
android:layout_height="0dp"/>

View File

@ -418,6 +418,16 @@
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Vous avez reçu un message chiffré avec une ancienne version de Signal qui nest plus prise en charge. Veuillez demander à lexpéditeur de mettre Session à jour vers la version la plus récente et de renvoyer son message.</string>
<string name="MessageRecord_left_group">Vous avez quitté le groupe</string>
<string name="MessageRecord_you_updated_group">Vous avez mis le groupe à jour.</string>
<string name="MessageRecord_you_created_a_new_group">Vous avez créée un nouveau groupe.</string>
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté au groupe.</string>
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en \'%1$s\'</string>
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en \'%2$s\'</string>
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez supprimé %1$s du groupe.</string>
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été supprimé du groupe.</string>
<string name="MessageRecord_you">Vous</string>
<string name="MessageRecord_you_called">Vous avez appelé</string>
<string name="MessageRecord_called_you">Contact appelé</string>
<string name="MessageRecord_missed_call">Appel manqué</string>
@ -430,6 +440,8 @@
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s.</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s.</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture décran.</string>
<string name="MessageRecord_media_saved_by_s">%1$s a sauvegardé un media.</string>
<string name="MessageRecord_your_safety_number_with_s_has_changed">Votre numéro de sécurité avec %s a changé.</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">Vous avez marqué votre numéro de sécurité avec %s comme vérifié</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">Vous avez marqué votre numéro de sécurité avec %s comme vérifié à partir dun autre appareil</string>
@ -1483,4 +1495,35 @@ Vous avez reçu un message déchange de clés pour une version de protocole i
<string name="fragment_contact_selection_closed_groups_title">Groupes privés</string>
<string name="fragment_contact_selection_open_groups_title">Groupes publics</string>
<!-- Next round of translation -->
<string name="menu_apply_button">Appliquer</string>
<string name="menu_done_button">Terminé</string>
<string name="activity_edit_closed_group_title">Modifier le groupe</string>
<string name="activity_edit_closed_group_edit_text_hint">Saisissez un nouveau nom de groupe</string>
<string name="activity_edit_closed_group_edit_members">Membres</string>
<string name="activity_edit_closed_group_add_members">Ajouter des membres</string>
<string name="activity_edit_closed_group_group_name_missing_error">Le nom du groupe ne peut pas être vide</string>
<string name="activity_edit_closed_group_group_name_too_long_error">Veuillez saisir un nom plus court</string>
<string name="activity_edit_closed_group_not_enough_group_members_error">Les groupes doivent avoir au moins un membre</string>
<string name="activity_edit_closed_group_invalid_session_id_error">Un des membres du group a un Session ID invalide</string>
<string name="activity_edit_closed_group_confirm_removal">Êtes vous sûr de vouloir suprimer ce membre du groupe?</string>
<string name="activity_edit_closed_group_member_removed">Membre supprimé du groupe</string>
<string name="fragment_edit_group_bottom_sheet_remove">Supprimer le membre du groupe</string>
<string name="activity_select_contacts_title">Contacts</string>
<string name="dialog_ui_mode_title">Thème</string>
<string name="dialog_ui_mode_option_day">Jour</string>
<string name="dialog_ui_mode_option_night">Nuit</string>
<string name="dialog_ui_mode_option_system_default">Système</string>
<string name="activity_conversation_menu_copy_session_id">Copier le Session ID</string>
<string name="attachment">Pièce jointe</string>
<string name="attachment_type_voice_message">Message vocal</string>
<string name="details">Détails</string>
</resources>

View File

@ -493,6 +493,16 @@
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.</string>
<string name="MessageRecord_left_group">You have left the group.</string>
<string name="MessageRecord_you_updated_group">You updated the group.</string>
<string name="MessageRecord_you_created_a_new_group">You created a new group.</string>
<string name="MessageRecord_s_added_you_to_the_group">%1$s added you to the group.</string>
<string name="MessageRecord_you_renamed_the_group_to_s">You renamed the group to \'%1$s\'</string>
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s renamed the group to \'%2$s\'</string>
<string name="MessageRecord_you_added_s_to_the_group">You added %1$s to the group.</string>
<string name="MessageRecord_s_added_s_to_the_group">%1$s added %2$s to the group.</string>
<string name="MessageRecord_you_removed_s_from_the_group">You removed %1$s from the group.</string>
<string name="MessageRecord_s_removed_s_from_the_group">%1$s removed %2$s from the group.</string>
<string name="MessageRecord_you_were_removed_from_the_group">You were removed from the group.</string>
<string name="MessageRecord_you">You</string>
<string name="MessageRecord_you_called">You called</string>
<string name="MessageRecord_called_you">Contact called</string>
<string name="MessageRecord_missed_call">Missed call</string>
@ -505,6 +515,8 @@
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s took a screenshot.</string>
<string name="MessageRecord_media_saved_by_s">Media saved by %1$s.</string>
<string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">You marked your safety number with %s verified</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string>
@ -703,6 +715,8 @@
<string name="ThreadRecord_s_is_on_signal">%s is on Session!</string>
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
<string name="ThreadRecord_s_took_a_screenshot">%s took a screenshot.</string>
<string name="ThreadRecord_media_saved_by_s">Media saved by %s.</string>
<string name="ThreadRecord_safety_number_changed">Safety number changed</string>
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="ThreadRecord_you_marked_verified">You marked verified</string>
@ -1884,5 +1898,6 @@
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
<!-- LinkDeviceActivity -->
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
<string name="activity_join_public_chat_join_rooms">Or join one of these...</string>
</resources>

View File

@ -1,5 +1,6 @@
package org.session.libsession.database
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI
@ -10,7 +11,8 @@ import java.io.InputStream
interface MessageDataProvider {
fun getMessageID(serverID: Long): Long?
fun deleteMessage(messageID: Long)
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
fun deleteMessage(messageID: Long, isSms: Boolean)
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
@ -30,7 +32,6 @@ interface MessageDataProvider {
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
// Quotes
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
fun getMessageBodyFor(timestamp: Long, author: String): String
@ -38,4 +39,5 @@ interface MessageDataProvider {
fun getAttachmentIDsFor(messageID: Long): List<Long>
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
fun getOpenGroup(threadID: Long): OpenGroup?
}

View File

@ -1,25 +0,0 @@
package org.session.libsession.messaging
import android.content.Context
import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
class MessagingConfiguration(
val context: Context,
val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider,
val sessionProtocol: SessionProtocol)
{
companion object {
lateinit var shared: MessagingConfiguration
fun configure(context: Context,
storage: StorageProtocol,
messageDataProvider: MessageDataProvider,
sessionProtocol: SessionProtocol
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingConfiguration(context, storage, messageDataProvider, sessionProtocol)
}
}
}

View File

@ -0,0 +1,26 @@
package org.session.libsession.messaging
import android.content.Context
import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
class MessagingModuleConfiguration(
val context: Context,
val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider,
val sessionProtocol: SessionProtocol)
{
companion object {
lateinit var shared: MessagingModuleConfiguration
fun configure(context: Context,
storage: StorageProtocol,
messageDataProvider: MessageDataProvider,
sessionProtocol: SessionProtocol
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, sessionProtocol)
}
}
}

View File

@ -9,10 +9,12 @@ import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.opengroups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.GroupRecord
@ -20,7 +22,6 @@ import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSe
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
interface StorageProtocol {
@ -34,6 +35,7 @@ interface StorageProtocol {
fun setUserProfilePictureUrl(newProfilePicture: String)
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
// Signal Protocol
@ -51,16 +53,18 @@ interface StorageProtocol {
fun isJobCanceled(job: Job): Boolean
// Authorization
fun getAuthToken(server: String): String?
fun setAuthToken(server: String, newValue: String?)
fun removeAuthToken(server: String)
fun getAuthToken(room: String, server: String): String?
fun setAuthToken(room: String, server: String, newValue: String)
fun removeAuthToken(room: String, server: String)
// Open Groups
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
fun getV2OpenGroup(threadId: String): OpenGroupV2?
// Open Groups
fun getOpenGroup(threadID: String): OpenGroup?
fun getThreadID(openGroupID: String): String?
fun getAllOpenGroups(): Map<Long, OpenGroup>
fun addOpenGroup(server: String, channel: Long)
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun addOpenGroup(serverUrl: String, channel: Long)
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
// Open Group Public Keys
@ -68,31 +72,30 @@ interface StorageProtocol {
fun setOpenGroupPublicKey(server: String, newValue: String)
// Open Group User Info
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String)
fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String?
// Open Group Metadata
fun setUserCount(group: Long, server: String, newValue: Int)
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray)
fun setUserCount(room: String, server: String, newValue: Int)
// Last Message Server ID
fun getLastMessageServerID(group: Long, server: String): Long?
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
fun removeLastMessageServerID(group: Long, server: String)
fun getLastMessageServerId(room: String, server: String): Long?
fun setLastMessageServerId(room: String, server: String, newValue: Long)
fun removeLastMessageServerId(room: String, server: String)
// Last Deletion Server ID
fun getLastDeletionServerID(group: Long, server: String): Long?
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
fun removeLastDeletionServerID(group: Long, server: String)
fun getLastDeletionServerId(room: String, server: String): Long?
fun setLastDeletionServerId(room: String, server: String, newValue: Long)
fun removeLastDeletionServerId(room: String, server: String)
// Message Handling
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
fun getReceivedMessageTimestamps(): Set<Long>
fun addReceivedMessageTimestamp(timestamp: Long)
// fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
// Returns the IDs of the saved attachments.
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
@ -107,8 +110,10 @@ interface StorageProtocol {
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
fun isGroupActive(groupPublicKey: String): Boolean
fun setActive(groupID: String, value: Boolean)
fun getZombieMember(groupID: String): Set<String>
fun removeMember(groupID: String, member: Address)
fun updateMembers(groupID: String, members: List<Address>)
fun updateZombieMembers(groupID: String, members: List<Address>)
// Closed Group
fun getAllClosedGroupPublicKeys(): Set<String>
fun getAllActiveClosedGroupPublicKeys(): Set<String>
@ -116,9 +121,9 @@ interface StorageProtocol {
fun removeClosedGroupPublicKey(groupPublicKey: String)
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,
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
fun isClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
@ -134,6 +139,7 @@ interface StorageProtocol {
fun getOrCreateThreadIdFor(address: Address): Long
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
fun getThreadIdFor(address: Address): Long?
fun getThreadIdForMms(mmsId: Long): Long
// Session Request
fun getSessionRequestSentTimestamp(publicKey: String): Long?
@ -158,4 +164,31 @@ interface StorageProtocol {
// Message Handling
/// Returns the ID of the `TSIncomingMessage` that was constructed.
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
}
// Data Extraction Notification
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
// DEPRECATED
fun getAuthToken(server: String): String?
fun setAuthToken(server: String, newValue: String?)
fun removeAuthToken(server: String)
fun getLastMessageServerID(group: Long, server: String): Long?
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
fun removeLastMessageServerID(group: Long, server: String)
fun getLastDeletionServerID(group: Long, server: String): Long?
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
fun removeLastDeletionServerID(group: Long, server: String)
fun getOpenGroup(threadID: String): OpenGroup?
fun getAllOpenGroups(): Map<Long, OpenGroup>
fun setUserCount(group: Long, server: String, newValue: Int)
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
}

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars;
import android.content.Context;
import androidx.annotation.NonNull;

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars;
import android.content.Context;
import android.net.Uri;
@ -19,5 +18,4 @@ public interface ContactPhoto extends Key {
@Nullable Uri getUri(@NonNull Context context);
boolean isProfilePhoto();
}

View File

@ -7,5 +7,4 @@ public interface FallbackContactPhoto {
public Drawable asDrawable(Context context, int color);
public Drawable asDrawable(Context context, int color, boolean inverted);
}

View File

@ -19,7 +19,6 @@ import org.session.libsession.utilities.ViewUtil;
import java.util.regex.Pattern;
public class GeneratedContactPhoto implements FallbackContactPhoto {
private static final Pattern PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+");

View File

@ -1,13 +1,12 @@
package org.session.libsession.messaging.avatars;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.MessagingConfiguration;
import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.StorageProtocol;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.GroupRecord;
@ -32,7 +31,7 @@ public class GroupRecordContactPhoto implements ContactPhoto {
@Override
public InputStream openInputStream(Context context) throws IOException {
StorageProtocol groupDatabase = MessagingConfiguration.shared.getStorage();
StorageProtocol groupDatabase = MessagingModuleConfiguration.shared.getStorage();
Optional<GroupRecord> groupRecord = Optional.of(groupDatabase.getGroup(address.toGroupString()));
if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) {

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars;
import android.content.Context;
import android.net.Uri;

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars;
import android.content.Context;
import android.net.Uri;

View File

@ -1,17 +1,16 @@
package org.session.libsession.messaging.fileserver
package org.session.libsession.messaging.file_server
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.Request
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.*
import java.net.URL
import java.util.concurrent.ConcurrentHashMap
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {

View File

@ -0,0 +1,106 @@
package org.session.libsession.messaging.file_server
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.service.loki.HTTP
import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.logging.Log
object FileServerAPIV2 {
const val DEFAULT_SERVER = "http://88.99.175.227"
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
sealed class Error : Exception() {
object PARSING_FAILED : Error()
object INVALID_URL : Error()
fun errorDescription() = when (this) {
PARSING_FAILED -> "Invalid response."
INVALID_URL -> "Invalid URL."
}
}
data class Request(
val verb: HTTP.Verb,
val endpoint: String,
val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null,
val headers: Map<String, String> = mapOf(),
// Always `true` under normal circumstances. You might want to disable
// this when running over Lokinet.
val useOnionRouting: Boolean = true
)
private fun createBody(parameters: Any?): RequestBody? {
if (parameters == null) return null
val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
}
private fun send(request: Request): Promise<Map<*, *>, Exception> {
val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.INVALID_URL)
val urlBuilder = HttpUrl.Builder()
.scheme(parsed.scheme())
.host(parsed.host())
.port(parsed.port())
.addPathSegments(request.endpoint)
if (request.verb == HTTP.Verb.GET) {
for ((key, value) in request.queryParameters) {
urlBuilder.addQueryParameter(key, value)
}
}
val requestBuilder = okhttp3.Request.Builder()
.url(urlBuilder.build())
.headers(Headers.of(request.headers))
when (request.verb) {
HTTP.Verb.GET -> requestBuilder.get()
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
}
if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
.fail { e ->
Log.e("Loki", "FileServerV2 failed with error",e)
}
} else {
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
}
}
// region Sending
fun upload(file: ByteArray): Promise<Long, Exception> {
val base64EncodedFile = Base64.encodeBytes(file)
val parameters = mapOf("file" to base64EncodedFile)
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
return send(request).map { json ->
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.PARSING_FAILED
}
}
fun download(file: Long): Promise<ByteArray, Exception> {
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
return send(request).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
}
}
}

View File

@ -1,23 +1,24 @@
package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.fileserver.FileServerAPI
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsession.utilities.DownloadUtilities
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log
import java.io.File
import java.io.FileInputStream
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long): Job {
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
// Error
internal sealed class Error(val description: String) : Exception(description) {
object NoAttachment : Error("No such attachment.")
@ -25,44 +26,53 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
// Settings
override val maxFailureCount: Int = 20
companion object {
val KEY: String = "AttachmentDownloadJob"
//keys used for database storage purpose
// Keys used for database storage
private val KEY_ATTACHMENT_ID = "attachment_id"
private val KEY_TS_INCOMING_MESSAGE_ID = "tsIncoming_message_id"
}
override fun execute() {
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
if(exception is Error && exception == Error.NoAttachment) {
MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
if (exception == Error.NoAttachment) {
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception)
} else if (exception is DotNetAPI.Error && exception == DotNetAPI.Error.ParsingFailed) {
} else if (exception == DotNetAPI.Error.ParsingFailed) {
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
// got a "Cannot GET ..." error from the file server.
MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception)
} else {
this.handleFailure(exception)
}
}
try {
val messageDataProvider = MessagingConfiguration.shared.messageDataProvider
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment)
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
?: return handleFailure(Error.NoAttachment)
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
val tempFile = createTempFile()
FileServerAPI.shared.downloadFile(tempFile, attachment.url, MAX_ATTACHMENT_SIZE, null)
// DECRYPTION
// Assume we're retrieving an attachment for an open group server if the digest is not set
val stream = if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
val stream = if (openGroupV2 == null) {
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
// Assume we're retrieving an attachment for an open group server if the digest is not set
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
} else {
val url = HttpUrl.parse(attachment.url)!!
val fileId = url.pathSegments().last()
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let {
tempFile.writeBytes(it)
}
FileInputStream(tempFile)
}
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
tempFile.delete()
handleSuccess()
} catch (e: Exception) {
@ -84,24 +94,22 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
}
private fun createTempFile(): File {
val file = File.createTempFile("push-attachment", "tmp", MessagingConfiguration.shared.context.cacheDir)
val file = File.createTempFile("push-attachment", "tmp", MessagingModuleConfiguration.shared.context.cacheDir)
file.deleteOnExit()
return file
}
//database functions
override fun serialize(): Data {
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
.putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID)
.build();
.putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID)
.build();
}
override fun getFactoryKey(): String {
return KEY
}
class Factory: Job.Factory<AttachmentDownloadJob> {
class Factory : Job.Factory<AttachmentDownloadJob> {
override fun create(data: Data): AttachmentDownloadJob {
return AttachmentDownloadJob(data.getLong(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID))
}

View File

@ -3,9 +3,10 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.fileserver.FileServerAPI
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
@ -14,11 +15,10 @@ 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.service.loki.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.logging.Log
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
@ -34,9 +34,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val TAG = AttachmentUploadJob::class.simpleName
val KEY: String = "AttachmentUploadJob"
val maxFailureCount: Int = 20
//keys used for database storage purpose
// Keys used for database storage
private val KEY_ATTACHMENT_ID = "attachment_id"
private val KEY_THREAD_ID = "thread_id"
private val KEY_MESSAGE = "message"
@ -45,17 +43,18 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
override fun execute() {
try {
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment)
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment)
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
}
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
val server = openGroup?.let {
it.server
} ?: openGroupV2?.let {
it.server
} ?: FileServerAPI.shared.server
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
val attachmentKey = Util.getSecretBytes(64)
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
@ -65,11 +64,14 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
val uploadResult = if (openGroupV2 == null) FileServerAPI.shared.uploadAttachment(server, attachmentData) else {
val dataBytes = attachmentData.data.readBytes()
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
}
handleSuccess(attachment, attachmentKey, uploadResult)
} catch (e: java.lang.Exception) {
if (e is Error && e == Error.NoAttachment) {
if (e == Error.NoAttachment) {
this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e)
@ -77,33 +79,32 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
this.handleFailure(e)
}
}
}
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)
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
}
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)
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
failAssociatedMessageSendJob(e)
}
private fun handleFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed due to error: $this.")
delegate?.handleJobFailed(this, e)
if (failureCount + 1 == AttachmentUploadJob.maxFailureCount) {
if (failureCount + 1 == maxFailureCount) {
failAssociatedMessageSendJob(e)
}
}
private fun failAssociatedMessageSendJob(e: Exception) {
val storage = MessagingConfiguration.shared.storage
val storage = MessagingModuleConfiguration.shared.storage
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
MessageSender.handleFailedMessageSend(this.message, e)
if (messageSendJob != null) {
@ -111,10 +112,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
}
//database functions
override fun serialize(): Data {
//serialize Message property
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
@ -122,10 +120,10 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
kryo.writeObject(output, message)
output.close()
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
.putString(KEY_THREAD_ID, threadID)
.putByteArray(KEY_MESSAGE, serializedMessage)
.putString(KEY_MESSAGE_SEND_JOB_ID, messageSendJobID)
.build();
.putString(KEY_THREAD_ID, threadID)
.putByteArray(KEY_MESSAGE, serializedMessage)
.putString(KEY_MESSAGE_SEND_JOB_ID, messageSendJobID)
.build();
}
override fun getFactoryKey(): String {
@ -133,9 +131,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
class Factory: Job.Factory<AttachmentUploadJob> {
override fun create(data: Data): AttachmentUploadJob {
val serializedMessage = data.getByteArray(KEY_MESSAGE)
//deserialize Message property
val kryo = Kryo()
val input = Input(serializedMessage)
val message: Message = kryo.readObject(input, Message::class.java)

View File

@ -12,7 +12,6 @@ import org.session.libsession.utilities.ParcelableUtil;
import java.util.HashMap;
import java.util.Map;
// TODO AC: For now parcelable objects utilize byteArrays field to store their data into.
// Introduce a dedicated Map<String, byte[]> field specifically for parcelable needs.
public class Data {

View File

@ -8,15 +8,13 @@ interface Job {
val maxFailureCount: Int
companion object {
//keys used for database storage purpose
// Keys used for database storage
private val KEY_ID = "id"
private val KEY_FAILURE_COUNT = "failure_count"
}
fun execute()
//database functions
fun serialize(): Data
/**

Some files were not shown because too many files have changed in this diff Show More