Added public chat manager.

Replace hard coded public chat server with dynamic one.
This commit is contained in:
Mikunj 2019-10-10 11:38:43 +11:00
parent 8b92932b6d
commit 13d42f542c
14 changed files with 267 additions and 79 deletions

View File

@ -25,6 +25,7 @@ import android.database.ContentObserver;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.multidex.MultiDexApplication;
import com.crashlytics.android.Crashlytics;
@ -61,8 +62,9 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.loki.BackgroundPollWorker;
import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.LokiGroupChatPoller;
import org.thoughtcrime.securesms.loki.LokiPublicChatManager;
import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
@ -98,6 +100,7 @@ import java.security.Security;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -132,9 +135,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
// Loki
private LokiLongPoller lokiLongPoller = null;
private LokiGroupChatPoller lokiPublicChatPoller = null;
private LokiRSSFeedPoller lokiNewsFeedPoller = null;
private LokiRSSFeedPoller lokiMessengerUpdatesFeedPoller = null;
private LokiPublicChatManager lokiPublicChatManager = null;
private LokiGroupChatAPI lokiGroupChatAPI = null;
public SignalCommunicationModule communicationModule;
public MixpanelAPI mixpanel;
@ -147,7 +151,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
@Override
public void onCreate() {
super.onCreate();
LokiGroupChatAPI.Companion.setDebugMode(BuildConfig.DEBUG); // Loki - Set debug mode if needed
startKovenant();
Log.i(TAG, "onCreate()");
initializeSecurityProvider();
@ -183,6 +186,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
mixpanel.trackMap(event, properties);
return Unit.INSTANCE;
};
lokiPublicChatManager = new LokiPublicChatManager(this);
}
@Override
@ -204,6 +209,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
MessageNotifier.setVisibleThread(-1);
// Loki - Stop long polling if needed
if (lokiLongPoller != null) { lokiLongPoller.stopIfNeeded(); }
if (lokiPublicChatManager != null) { lokiPublicChatManager.stopPollers(); }
}
@Override
@ -243,6 +249,22 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
return persistentLogger;
}
public LokiPublicChatManager getLokiPublicChatManager() {
return lokiPublicChatManager;
}
public @Nullable LokiGroupChatAPI getLokiGroupChatAPI() {
if (lokiGroupChatAPI == null && TextSecurePreferences.isPushRegistered(this)) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(this);
LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(this);
lokiGroupChatAPI = new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase);
}
return lokiGroupChatAPI;
}
private void initializeSecurityProvider() {
try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
@ -471,10 +493,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (lokiLongPoller != null) { lokiLongPoller.startIfNeeded(); }
}
private LokiGroupChat lokiPublicChat() {
return new LokiGroupChat(LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), "Loki Public Chat", true);
}
private LokiRSSFeed lokiNewsFeed() {
return new LokiRSSFeed("loki.network.feed", "https://loki.network/feed/", "Loki News", true);
}
@ -484,11 +502,21 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
public void createGroupChatsIfNeeded() {
LokiGroupChat publicChat = lokiPublicChat();
boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId());
if (!isChatSetUp || !publicChat.isDeletable()) {
GroupManager.GroupActionResult result = GroupManager.createGroup(publicChat.getId(), this, new HashSet<>(), null, publicChat.getDisplayName(), false);
TextSecurePreferences.markChatSetUp(this, publicChat.getId());
List<LokiGroupChat> defaultChats = LokiGroupChat.Companion.defaultChats(BuildConfig.DEBUG);
for (LokiGroupChat chat : defaultChats) {
long threadID = GroupManager.getThreadId(chat.getId(), this);
String migrationKey = chat.getId() + "_migrated";
boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false);
boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, chat.getId());
if (!isChatSetUp || !chat.isDeletable()) {
lokiPublicChatManager.addChat(chat.getServer(), chat.getChannel(), chat.getDisplayName());
TextSecurePreferences.markChatSetUp(this, chat.getId());
TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
} else if (threadID > -1 && !isChatMigrated) {
// Migrate the old public chats.
DatabaseFactory.getLokiThreadDatabase(this).setGroupChat(chat, threadID);
TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
}
}
}
@ -505,20 +533,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
}
private void createGroupChatPollersIfNeeded() {
// Only create the group chat pollers if their threads aren't deleted
LokiGroupChat publicChat = lokiPublicChat();
long threadID = GroupManager.getThreadId(publicChat.getId(), this);
if (threadID >= 0 && lokiPublicChatPoller == null) {
lokiPublicChatPoller = new LokiGroupChatPoller(this, publicChat);
// Set up deletion listeners if needed
setUpThreadDeletionListeners(threadID, () -> {
if (lokiPublicChatPoller != null) lokiPublicChatPoller.stop();
lokiPublicChatPoller = null;
});
}
}
private void createRSSFeedPollersIfNeeded() {
// Only create the RSS feed pollers if their threads aren't deleted
LokiRSSFeed lokiNewsFeed = lokiNewsFeed();
@ -559,8 +573,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
public void startGroupChatPollersIfNeeded() {
createGroupChatPollersIfNeeded();
if (lokiPublicChatPoller != null) lokiPublicChatPoller.startIfNeeded();
lokiPublicChatManager.startPollersIfNeeded();
}
public void startRSSFeedPollersIfNeeded() {

View File

@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import org.whispersystems.signalservice.loki.utilities.Analytics;
@ -68,6 +69,8 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
@ -377,12 +380,13 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
Analytics.Companion.getShared().track("Display Name Updated");
TextSecurePreferences.setProfileName(context, name);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).getPrivateKey().serialize();
LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(context);
LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(context);
new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.getPublicChatServer());
LokiGroupChatAPI chatAPI = ApplicationContext.getInstance(context).getLokiGroupChatAPI();
if (chatAPI != null) {
Set<String> groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChatServers();
for (String server : groupChatServers) {
chatAPI.setDisplayName(name, server);
}
}
// Loki - Original code
// ========

View File

@ -21,6 +21,7 @@ import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.Database;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import java.util.List;
@ -194,18 +196,14 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress());
String quoteeDisplayName = author.toShortString();
if (quoteeDisplayName.equals(author.getAddress().toString())) {
quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(LokiGroupChatAPI.getPublicChatServer() + "." + LokiGroupChatAPI.getPublicChatServerID(), author.getAddress().toString());
}
// If we're in a group then try and use the display name in the group
if (conversationRecipient.isGroupRecipient()) {
try {
String serverId = GroupUtil.getDecodedStringId(conversationRecipient.getAddress().serialize());
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(serverId, author.getAddress().serialize());
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient);
LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId);
if (chat != null) {
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(chat.getId(), author.getAddress().serialize());
if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; }
} catch (Exception e) {
// Do nothing
}
}

View File

@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHol
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.Database;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@ -105,6 +106,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture;
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import java.io.IOException;
@ -408,14 +410,15 @@ public class ConversationFragment extends Fragment
boolean isGroupChat = recipient.isGroupRecipient();
if (isGroupChat) {
boolean isLokiPublicChat = recipient.getName() != null && recipient.getName().equals("Loki Public Chat");
LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId);
boolean isPublicChat = groupChat != null;
int selectedMessageCount = messageRecords.size();
boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing();
menu.findItem(R.id.menu_context_copy_public_key).setVisible(isLokiPublicChat && selectedMessageCount == 1 && !isSentByUser);
menu.findItem(R.id.menu_context_reply).setVisible(isLokiPublicChat && selectedMessageCount == 1);
menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !isSentByUser);
menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
boolean userCanModerate = LokiGroupChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer());
boolean isDeleteOptionVisible = isLokiPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate);
boolean userCanModerate = groupChat != null && LokiGroupChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, groupChat.getChannel(), groupChat.getServer());
boolean isDeleteOptionVisible = isPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate);
menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible);
} else {
menu.findItem(R.id.menu_context_copy_public_key).setVisible(false);
@ -509,8 +512,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);
// Loki - The delete option is only visible to the user in a group chat if it's the Loki Public Chat
boolean isLokiPublicChat = this.recipient.isGroupRecipient();
// Loki - The delete option is only visible to the user in a group chat
LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
@ -524,19 +527,16 @@ public class ConversationFragment extends Fragment
for (MessageRecord messageRecord : messageRecords) {
boolean isThreadDeleted;
if (isLokiPublicChat) {
if (groupChat != null) {
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
LokiAPIDatabase lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(getContext());
LokiUserDatabase lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(getContext());
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey().serialize();
LokiGroupChatAPI chatAPI = ApplicationContext.getInstance(getContext()).getLokiGroupChatAPI();
boolean isSentByUser = messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
if (serverID != null) {
new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase)
.deleteMessage(serverID, LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), isSentByUser)
if (chatAPI != null && serverID != null) {
chatAPI
.deleteMessage(serverID, groupChat.getChannel(), groupChat.getServer(), isSentByUser)
.success(l -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>) future[0];
f.set(Unit.INSTANCE);

View File

@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.components.StickerView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.Database;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@ -112,6 +113,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import java.util.Collections;
@ -934,13 +936,15 @@ public class ConversationItem extends LinearLayout
if (!next.isPresent() || next.get().isUpdate() || !current.getRecipient().getAddress().equals(next.get().getRecipient().getAddress())) {
contactPhoto.setVisibility(VISIBLE);
int visibility;
if (conversationRecipient.getName() != null && conversationRecipient.getName().equals("Loki Public Chat")) {
boolean isModerator = LokiGroupChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer());
int visibility = View.GONE;
// If we have a chat then use that to determine mod status
LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(messageRecord.getThreadId());
if (groupChat != null) {
boolean isModerator = LokiGroupChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), groupChat.getChannel(), groupChat.getServer());
visibility = isModerator ? View.VISIBLE : View.GONE;
} else {
visibility = View.GONE;
}
moderatorIconImageView.setVisibility(visibility);
} else {
contactPhoto.setVisibility(GONE);

View File

@ -71,7 +71,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV2 = 23;
private static final int lokiV3 = 24;
private static final int DATABASE_VERSION = lokiV2; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV3; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -499,8 +499,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV3) {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiThreadDatabase.getCreateGroupChatMappingTableCommand());
// TODO: Map old public chat threads to new manager format
}
db.setTransactionSuccessful();

View File

@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
@ -44,6 +45,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import java.io.IOException;
@ -285,7 +287,14 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) {
ArrayList<Address> result = new ArrayList<>();
result.add(Address.fromSerialized(LokiGroupChatAPI.getPublicChatServer())); // Loki - All group messages should be directed to the Loki Public Chat for now
// Loki - All group messages should be directed to their servers
long threadID = GroupManager.getThreadIdFromGroupId(groupId, context);
LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(threadID);
if (chat != null) {
result.add(Address.fromSerialized(chat.getServer()));
}
return result;
/*

View File

@ -50,6 +50,10 @@ fun Cursor.getString(columnName: String): String {
return getString(getColumnIndexOrThrow(columnName))
}
fun Cursor.getLong(columnName: String): Long {
return getLong(getColumnIndexOrThrow(columnName))
}
fun Cursor.getBase64EncodedData(columnName: String): ByteArray {
return Base64.decode(getString(columnName))
}

View File

@ -45,12 +45,11 @@ class DisplayNameActivity : BaseActionBarActivity() {
application.setUpStorageAPIIfNeeded()
startActivity(Intent(this, ConversationListActivity::class.java))
finish()
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).privateKey.serialize()
val apiDatabase = DatabaseFactory.getLokiAPIDatabase(this)
val userDatabase = DatabaseFactory.getLokiUserDatabase(this)
if (name != null) {
LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.publicChatServer)
val chatAPI = ApplicationContext.getInstance(this).lokiGroupChatAPI
if (chatAPI != null && name != null) {
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllGroupChatServers()
servers.forEach { chatAPI.setDisplayName(name, it) }
}
}
}

View File

@ -24,8 +24,8 @@ fun toPx(dp: Int, resources: Resources): Int {
return (dp * scale).roundToInt()
}
fun isGroupRecipient(recipient: String): Boolean {
return (LokiGroupChatAPI.publicChatServer == recipient)
fun isGroupRecipient(context: Context, recipient: String): Boolean {
return DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChats().values.map { it.server }.contains(recipient)
}
fun getFriendPublicKeys(context: Context, devicePublicKeys: Set<String>): Set<String> {

View File

@ -137,6 +137,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastMessageServerIDCache, row, "$lastMessageServerIDCacheIndex = ?", wrap(index))
}
fun removeLastMessageServerID(group: Long, server: String) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
database.delete(lastMessageServerIDCache,"$lastMessageServerIDCacheIndex = ?", wrap(index))
}
override fun getLastDeletionServerID(group: Long, server: String): Long? {
val database = databaseHelper.readableDatabase
val index = "$server.$group"
@ -152,6 +158,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index))
}
fun removeLastDeletionServerID(group: Long, server: String) {
val database = databaseHelper.writableDatabase
val index = "$server.$group"
database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index))
}
override fun getPairingAuthorisations(hexEncodedPublicKey: String): List<PairingAuthorisation> {
val database = databaseHelper.readableDatabase
return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor ->

View File

@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.database.ContentObserver
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiGroupChat
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import java.util.HashSet
class LokiPublicChatManager(private val context: Context) {
private var chats = mutableMapOf<Long, LokiGroupChat>()
private val pollers = mutableMapOf<Long, LokiGroupChatPoller>()
private val observers = mutableMapOf<Long, ContentObserver>()
private var isPolling = false
public fun startPollersIfNeeded() {
refreshChatsAndPollers()
for ((threadId, chat) in chats) {
val poller = pollers[threadId] ?: LokiGroupChatPoller(context, chat)
poller.startIfNeeded()
listenToThreadDeletion(threadId)
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
}
isPolling = true
}
public fun stopPollers() {
pollers.values.forEach { it.stop() }
isPolling = false
}
public fun addChat(server: String, channel: Long): Promise<LokiGroupChat, Exception> {
val groupChatAPI = ApplicationContext.getInstance(context).lokiGroupChatAPI ?: return Promise.ofFail(IllegalStateException())
return groupChatAPI.getAuthToken(server).bind {
groupChatAPI.getChannelInfo(channel, server)
}.map {
addChat(server, channel, it)
}
}
public fun addChat(server: String, channel: Long, name: String): LokiGroupChat {
val chat = LokiGroupChat(channel, server, name, true)
var threadID = GroupManager.getThreadId(chat.id, context)
// Create the group if we don't have one
if (threadID < 0) {
val result = GroupManager.createGroup(chat.id, context, HashSet(), null, chat.displayName, false)
threadID = result.threadId
}
DatabaseFactory.getLokiThreadDatabase(context).setGroupChat(chat, threadID)
startPollersIfNeeded()
// Set our name on the server
ApplicationContext.getInstance(context).lokiGroupChatAPI?.setDisplayName(server, TextSecurePreferences.getProfileName(context))
return chat
}
private fun refreshChatsAndPollers() {
val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChats()
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.getThreadId(it.value.id, context) > -1 }.toMutableMap()
}
private fun listenToThreadDeletion(threadID: Long) {
if (threadID < 0 || observers[threadID] != null) { return }
val observer = createDeletionObserver(threadID, Runnable {
val chat = chats[threadID]
// Reset last message cache
if (chat != null) {
val apiDatabase = DatabaseFactory.getLokiAPIDatabase(context)
apiDatabase.removeLastDeletionServerID(chat.channel, chat.server)
apiDatabase.removeLastMessageServerID(chat.channel, chat.server)
}
DatabaseFactory.getLokiThreadDatabase(context).removeGroupChat(threadID)
pollers.remove(threadID)?.stop()
observers.remove(threadID)
startPollersIfNeeded()
})
observers[threadID] = observer
context.applicationContext.contentResolver.registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer)
}
private fun createDeletionObserver(threadID: Long, onDelete: Runnable): ContentObserver {
return object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
// Stop the poller if thread is deleted
try {
if (!DatabaseFactory.getThreadDatabase(context).hasThread(threadID)) {
onDelete.run()
context.applicationContext.contentResolver.unregisterContentObserver(this)
}
} catch (e: Exception) {
// TODO: Handle
}
}
}
}
}

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
@ -12,6 +13,7 @@ import org.whispersystems.signalservice.loki.api.LokiGroupChat
import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus
import java.lang.Exception
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol {
var delegate: LokiThreadDatabaseDelegate? = null
@ -19,11 +21,11 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
companion object {
private val friendRequestTableName = "loki_thread_friend_request_database"
private val sessionResetTableName = "loki_thread_session_reset_database"
private val groupChatMappingTableName = "loki_group_chat_mapping_database"
private val threadID = "thread_id"
public val groupChatMappingTableName = "loki_group_chat_mapping_database"
public val threadID = "thread_id"
private val friendRequestStatus = "friend_request_status"
private val sessionResetStatus = "session_reset_status"
private val groupChatJSON = "group_chat_json"
public val groupChatJSON = "group_chat_json"
@JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
@JvmStatic val createGroupChatMappingTableCommand = "CREATE TABLE $groupChatMappingTableName ($threadID INTEGER PRIMARY KEY, $groupChatJSON TEXT);"
@ -90,7 +92,35 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
notifyConversationListeners(threadID)
}
fun getAllGroupChats(): Map<Long, LokiGroupChat> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
try {
val map = mutableMapOf<Long, LokiGroupChat>()
cursor = database.rawQuery("select * from $groupChatMappingTableName", null)
while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(Companion.threadID)
val string = cursor.getString(groupChatJSON)
val chat = LokiGroupChat.fromJSON(string)
if (chat != null) { map[threadID] = chat }
}
return map
} catch (e: Exception) {
} finally {
cursor?.close()
}
return mapOf()
}
fun getAllGroupChatServers(): Set<String> {
return getAllGroupChats().values.fold(setOf<String>()) { set, chat -> set.plus(chat.server) }
}
override fun getGroupChat(threadID: Long): LokiGroupChat? {
if (threadID < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
val string = cursor.getString(groupChatJSON)
@ -99,6 +129,8 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
}
override fun setGroupChat(groupChat: LokiGroupChat, threadID: Long) {
if (threadID < 0) { return }
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)

View File

@ -213,7 +213,7 @@ public class MessageSender {
// Just send the message normally if it's a group message
String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) {
if (GeneralUtilitiesKt.isGroupRecipient(context, recipientPublicKey)) {
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
return;
}
@ -243,7 +243,7 @@ public class MessageSender {
// Just send the message normally if it's a group message
String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) {
if (GeneralUtilitiesKt.isGroupRecipient(context, recipientPublicKey)) {
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
return;
}