diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index e2bde579ab..17fd512b39 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -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 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() { diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index 2d39b63fb7..cf73ae580f 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -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 groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChatServers(); + for (String server : groupChatServers) { + chatAPI.setDisplayName(name, server); + } + } // Loki - Original code // ======== diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index 1f2801ef9d..b667eb6484 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -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 } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 40c91565f5..fa1e4cb365 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -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() }; - 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 f = (SettableFuture) future[0]; f.set(Unit.INSTANCE); diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index 798d843ab0..f8fef22029 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -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); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index efbe89544a..c92b054706 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -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(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index c7e6291eff..8793474558 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -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
getGroupMessageRecipients(String groupId, long messageId) { ArrayList
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; /* diff --git a/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt b/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt index 41613af87c..8f252feaaf 100644 --- a/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt @@ -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)) } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt index 3685a2ad92..0fbcc18b55 100644 --- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt @@ -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) } } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt index 9d53000c5d..3be416b0f8 100644 --- a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt @@ -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): Set { diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index 166cfe3aed..9ac5dc0ca0 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -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 { val database = databaseHelper.readableDatabase return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor -> diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt new file mode 100644 index 0000000000..bec1b1f1f1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -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() + private val pollers = mutableMapOf() + private val observers = mutableMapOf() + 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 { + 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 + } + } + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index 1d1158caa3..a19acf580f 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -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 { + val database = databaseHelper.readableDatabase + var cursor: Cursor? = null + + try { + val map = mutableMapOf() + 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 { + return getAllGroupChats().values.fold(setOf()) { 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) diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index f43c973b35..6676dfcf5f 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -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; }