diff --git a/build.gradle b/build.gradle index 07a2b292a9..bf4c2a5328 100644 --- a/build.gradle +++ b/build.gradle @@ -182,7 +182,7 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 111 +def canonicalVersionCode = 115 def canonicalVersionName = "1.6.2" def postFixSize = 10 diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 29a668776d..44531d4c4b 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule; @@ -305,7 +306,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this); LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this); - publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB); + GroupDatabase groupDB = DatabaseFactory.getGroupDatabase(this); + publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB, groupDB); return publicChatAPI; } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 886d603999..12c8e337ce 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -213,6 +213,7 @@ import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.ExpirationUtil; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -227,6 +228,7 @@ import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.loki.api.opengroups.PublicChat; +import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI; import org.whispersystems.signalservice.loki.protocol.mentions.Mention; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol; @@ -458,7 +460,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); if (publicChat != null) { - ApplicationContext.getInstance(this).getPublicChatAPI().getChannelInfo(publicChat.getChannel(), publicChat.getServer()).success(displayName -> { + PublicChatAPI publicChatAPI = ApplicationContext.getInstance(this).getPublicChatAPI(); + publicChatAPI.getChannelInfo(publicChat.getChannel(), publicChat.getServer()).success(info -> { + String groupId = GroupUtil.getEncodedOpenGroupId(publicChat.getId().getBytes()); + + publicChatAPI.updateProfileIfNeeded( + publicChat.getChannel(), + publicChat.getServer(), + groupId, + info, + false); + runOnUiThread(ConversationActivity.this::updateSubtitleTextView); return Unit.INSTANCE; }); diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index c14aff87d5..c5bd65e903 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -6,9 +6,10 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.annimon.stream.Stream; @@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.loki.database.LokiOpenGroupDatabaseProtocol; import java.io.Closeable; import java.io.IOException; @@ -29,7 +31,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -public class GroupDatabase extends Database { +public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProtocol { @SuppressWarnings("unused") private static final String TAG = GroupDatabase.class.getSimpleName(); @@ -240,35 +242,37 @@ public class GroupDatabase extends Database { notifyConversationListListeners(); } - public void updateTitle(String groupId, String title) { + @Override + public void updateTitle(String groupID, String newValue) { ContentValues contentValues = new ContentValues(); - contentValues.put(TITLE, title); + contentValues.put(TITLE, newValue); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", - new String[] {groupId}); + new String[] {groupID}); - Recipient recipient = Recipient.from(context, Address.fromSerialized(groupId), false); - recipient.setName(title); + Recipient recipient = Recipient.from(context, Address.fromSerialized(groupID), false); + recipient.setName(newValue); } - public void updateAvatar(String groupId, Bitmap avatar) { - updateAvatar(groupId, BitmapUtil.toByteArray(avatar)); + public void updateProfilePicture(String groupID, Bitmap newValue) { + updateProfilePicture(groupID, BitmapUtil.toByteArray(newValue)); } - public void updateAvatar(String groupId, byte[] avatar) { + @Override + public void updateProfilePicture(String groupID, byte[] newValue) { long avatarId; - if (avatar != null) avatarId = Math.abs(new SecureRandom().nextLong()); + if (newValue != null) avatarId = Math.abs(new SecureRandom().nextLong()); else avatarId = 0; ContentValues contentValues = new ContentValues(2); - contentValues.put(AVATAR, avatar); + contentValues.put(AVATAR, newValue); contentValues.put(AVATAR_ID, avatarId); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", - new String[] {groupId}); + new String[] {groupID}); - Recipient.applyCached(Address.fromSerialized(groupId), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId)); + Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(avatarId == 0 ? null : avatarId)); } public void updateMembers(String groupId, List
members) { diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index de227acdc1..f1e7ff8c0d 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -92,9 +92,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV13 = 34; private static final int lokiV14_BACKUP_FILES = 35; private static final int lokiV15 = 36; - private static final int lokiV16_AUDIO_ATTACHMENT_EXTRAS = 37; + private static final int lokiV16 = 37; - private static final int DATABASE_VERSION = lokiV16_AUDIO_ATTACHMENT_EXTRAS; + private static final int DATABASE_VERSION = lokiV16; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -156,6 +156,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand()); db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand()); @@ -633,9 +634,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand()); } - if (oldVersion < lokiV16_AUDIO_ATTACHMENT_EXTRAS) { - db.execSQL("ALTER TABLE part ADD COLUMN audio_visual_samples BLOB"); - db.execSQL("ALTER TABLE part ADD COLUMN audio_duration INTEGER"); + if (oldVersion < lokiV16) { + db.execSQL("ALTER TABLE part ADD COLUMN audio_visual_samples BLOB"); + db.execSQL("ALTER TABLE part ADD COLUMN audio_duration INTEGER"); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand()); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 520761b536..a7449ca58d 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -159,6 +159,7 @@ public class SignalCommunicationModule { DatabaseFactory.getLokiPreKeyBundleDatabase(context), new SessionResetImplementation(context), DatabaseFactory.getLokiUserDatabase(context), + DatabaseFactory.getGroupDatabase(context), ((ApplicationContext)context.getApplicationContext()).broadcaster); } else { this.messageSender.setMessagePipe(IncomingMessageObserver.getPipe(), IncomingMessageObserver.getUnidentifiedPipe()); diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 81f6feec33..694627f629 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -85,7 +85,7 @@ public class GroupManager { groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses)); if (!mms) { - groupDatabase.updateAvatar(groupId, avatarBytes); + groupDatabase.updateProfilePicture(groupId, avatarBytes); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); } else { @@ -125,7 +125,7 @@ public class GroupManager { memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>()); - groupDatabase.updateAvatar(groupId, avatarBytes); + groupDatabase.updateProfilePicture(groupId, avatarBytes); long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadID); @@ -148,7 +148,7 @@ public class GroupManager { groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses)); groupDatabase.updateTitle(groupId, name); - groupDatabase.updateAvatar(groupId, avatarBytes); + groupDatabase.updateProfilePicture(groupId, avatarBytes); if (!GroupUtil.isMmsGroup(groupId)) { return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 0b2db43157..f2b95a46f0 100644 --- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -95,7 +95,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType { InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); - database.updateAvatar(groupId, avatar); + database.updateProfilePicture(groupId, avatar); inputStream.close(); } } catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index b23313bb0e..8a5cb4ae5c 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -342,20 +342,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val threadID = thread.threadId val recipient = thread.recipient val threadDB = DatabaseFactory.getThreadDatabase(this) - val deleteThread = object : Runnable { - - override fun run() { - AsyncTask.execute { - val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID) - if (publicChat != null) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(this@HomeActivity) - apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) - apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) - ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server) - } - threadDB.deleteConversation(threadID) - ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity) + val deleteThread = Runnable { + AsyncTask.execute { + val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID) + if (publicChat != null) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(this@HomeActivity) + apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) + apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) + apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) + ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server) } + threadDB.deleteConversation(threadID) + ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity) } } val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message diff --git a/src/org/thoughtcrime/securesms/loki/activities/PathActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/PathActivity.kt index fa96d6b2a1..9de15d852b 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/PathActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/PathActivity.kt @@ -82,7 +82,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() { private fun update(isAnimated: Boolean) { pathRowsContainer.removeAllViews() - if (OnionRequestAPI.paths.count() >= OnionRequestAPI.pathCount) { + if (OnionRequestAPI.paths.isNotEmpty()) { val path = OnionRequestAPI.paths.firstOrNull() ?: return finish() val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000 val pathRows = path.mapIndexed { index, snode -> diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt index 1cf97b3c64..7081cefe46 100644 --- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt +++ b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollJob.kt @@ -50,13 +50,16 @@ class BackgroundPollJob private constructor(parameters: Parameters) : BaseJob(pa Log.d("Loki", "Performing background poll.") val userPublicKey = TextSecurePreferences.getLocalNumber(context) val promises = mutableListOf>() - val promise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes -> - envelopes.forEach { - PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) + if (!TextSecurePreferences.isUsingFCM(context)) { + Log.d("Loki", "Not using FCM; polling for contacts and closed groups.") + val promise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes -> + envelopes.forEach { + PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) + } } + promises.add(promise) + promises.addAll(ClosedGroupPoller.shared.pollOnce()) } - promises.add(promise) - promises.addAll(ClosedGroupPoller.shared.pollOnce()) val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value } for (openGroup in openGroups) { val poller = PublicChatPoller(context, openGroup) diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt index 8095731318..6b710c5ea4 100644 --- a/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.api import android.content.Context import android.database.ContentObserver +import android.graphics.Bitmap import android.text.TextUtils import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind @@ -10,8 +11,10 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.loki.api.opengroups.PublicChatInfo import org.whispersystems.signalservice.loki.api.opengroups.PublicChat class PublicChatManager(private val context: Context) { @@ -24,8 +27,8 @@ class PublicChatManager(private val context: Context) { var areAllCaughtUp = true refreshChatsAndPollers() for ((threadID, chat) in chats) { - val poller = pollers[threadID] ?: PublicChatPoller(context, chat) - areAllCaughtUp = areAllCaughtUp && poller.isCaughtUp + val poller = pollers[threadID] + areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true } return areAllCaughtUp } @@ -56,7 +59,8 @@ class PublicChatManager(private val context: Context) { } public fun addChat(server: String, channel: Long): Promise { - val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!")) + val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI + ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!")) return groupChatAPI.getAuthToken(server).bind { groupChatAPI.getChannelInfo(channel, server) }.map { @@ -64,12 +68,18 @@ class PublicChatManager(private val context: Context) { } } - public fun addChat(server: String, channel: Long, name: String): PublicChat { - val chat = PublicChat(channel, server, name, true) + public fun addChat(server: String, channel: Long, info: PublicChatInfo): PublicChat { + val chat = PublicChat(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 if (threadID < 0) { - val result = GroupManager.createOpenGroup(chat.id, context, null, chat.displayName) + if (info.profilePictureURL.isNotEmpty()) { + val profilePictureAsByteArray = ApplicationContext.getInstance(context).publicChatAPI + ?.downloadOpenGroupProfilePicture(server, info.profilePictureURL) + profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray) + } + val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, chat.displayName) threadID = result.threadId } DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID) diff --git a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt index 374f8b68c6..ca73a77b3e 100644 --- a/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt @@ -46,7 +46,8 @@ class PublicChatPoller(private val context: Context, private val group: PublicCh val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context) val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context) - PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase) + val openGroupDatabase = DatabaseFactory.getGroupDatabase(context) + PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase, openGroupDatabase) }() // endregion diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index 18fae93b87..fc7c0b44e8 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -6,7 +6,6 @@ import android.util.Log import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.utilities.* -import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.api.Snode import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLink @@ -71,6 +70,10 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( // Open group public keys private val openGroupPublicKeyTable = "open_group_public_keys" @JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);" + // Open group profile picture cache + private val openGroupProfilePictureTable = "open_group_avatar_cache" + private val openGroupProfilePicture = "open_group_avatar" + @JvmStatic val createOpenGroupProfilePictureTableCommand = "CREATE TABLE $openGroupProfilePictureTable ($publicChatID STRING PRIMARY KEY, $openGroupProfilePicture TEXT NULLABLE DEFAULT NULL);" // region Deprecated private val deviceLinkCache = "loki_pairing_authorisation_cache" @@ -114,6 +117,30 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(snodePoolTable, row, "${Companion.dummyKey} = ?", wrap("dummy_key")) } + override fun setOnionRequestPaths(newValue: List>) { + // FIXME: This approach assumes either 1 or 2 paths of length 3 each. We should do better than this. + val database = databaseHelper.writableDatabase + fun set(indexPath: String, snode: Snode) { + var snodeAsString = "${snode.address}-${snode.port}" + val keySet = snode.publicKeySet + if (keySet != null) { + snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" + } + val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString )) + database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) + } + Log.d("Loki", "Persisting onion request paths to database.") + clearOnionRequestPaths() + if (newValue.count() < 1) { return } + val path0 = newValue[0] + if (path0.count() != 3) { return } + set("0-0", path0[0]); set("0-1", path0[1]); set("0-2", path0[2]) + if (newValue.count() < 2) { return } + val path1 = newValue[1] + if (path1.count() != 3) { return } + set("1-0", path1[0]); set("1-1", path1[1]); set("1-2", path1[2]) + } + override fun getOnionRequestPaths(): List> { val database = databaseHelper.readableDatabase fun get(indexPath: String): Snode? { @@ -131,10 +158,16 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } } } - val path0Snode0 = get("0-0") ?: return listOf(); val path0Snode1 = get("0-1") ?: return listOf() - val path0Snode2 = get("0-2") ?: return listOf(); val path1Snode0 = get("1-0") ?: return listOf() - val path1Snode1 = get("1-1") ?: return listOf(); val path1Snode2 = get("1-2") ?: return listOf() - return listOf( listOf( path0Snode0, path0Snode1, path0Snode2 ), listOf( path1Snode0, path1Snode1, path1Snode2 ) ) + val result = mutableListOf>() + val path0Snode0 = get("0-0"); val path0Snode1 = get("0-1"); val path0Snode2 = get("0-2") + if (path0Snode0 != null && path0Snode1 != null && path0Snode2 != null) { + result.add(listOf( path0Snode0, path0Snode1, path0Snode2 )) + } + val path1Snode0 = get("1-0"); val path1Snode1 = get("1-1"); val path1Snode2 = get("1-2") + if (path1Snode0 != null && path1Snode1 != null && path1Snode2 != null) { + result.add(listOf( path1Snode0, path1Snode1, path1Snode2 )) + } + return result } override fun clearOnionRequestPaths() { @@ -147,28 +180,6 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( delete("1-1"); delete("1-2") } - override fun setOnionRequestPaths(newValue: List>) { - // TODO: Make this work with arbitrary paths - if (newValue.count() != 2) { return } - val path0 = newValue[0] - val path1 = newValue[1] - if (path0.count() != 3 || path1.count() != 3) { return } - Log.d("Loki", "Persisting onion request paths to database.") - val database = databaseHelper.writableDatabase - fun set(indexPath: String, snode: Snode) { - var snodeAsString = "${snode.address}-${snode.port}" - val keySet = snode.publicKeySet - if (keySet != null) { - snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" - } - val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString )) - database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) - } - set("0-0", path0[0]); set("0-1", path0[1]) - set("0-2", path0[2]); set("1-0", path1[0]) - set("1-1", path1[1]); set("1-2", path1[2]) - } - override fun getSwarm(publicKey: String): Set? { val database = databaseHelper.readableDatabase return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> @@ -343,6 +354,27 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(openGroupPublicKeyTable, row, "${LokiAPIDatabase.server} = ?", wrap(server)) } + override fun getOpenGroupProfilePictureURL(group: Long, server: String): String? { + val database = databaseHelper.readableDatabase + val index = "$server.$group" + return database.get(openGroupProfilePictureTable, "$publicChatID = ?", wrap(index)) { cursor -> + cursor.getString(openGroupProfilePicture) + }?.toString() + } + + override fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) { + val database = databaseHelper.writableDatabase + val index = "$server.$group" + val row = wrap(mapOf(publicChatID to index, openGroupProfilePicture to newValue)) + database.insertOrUpdate(openGroupProfilePictureTable, row, "$publicChatID = ?", wrap(index)) + } + + fun clearOpenGroupProfilePictureURL(group: Long, server: String): Boolean { + val database = databaseHelper.writableDatabase + val index = "$server.$group" + return database.delete(openGroupProfilePictureTable, "$publicChatID = ?", arrayOf(index)) > 0 + } + // region Deprecated override fun getDeviceLinks(publicKey: String): Set { return setOf() diff --git a/src/org/thoughtcrime/securesms/loki/views/PathStatusView.kt b/src/org/thoughtcrime/securesms/loki/views/PathStatusView.kt index e65630c144..77cfbe2505 100644 --- a/src/org/thoughtcrime/securesms/loki/views/PathStatusView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/PathStatusView.kt @@ -85,7 +85,7 @@ class PathStatusView : View { private fun handlePathsBuiltEvent() { update() } private fun update() { - if (OnionRequestAPI.paths.count() >= OnionRequestAPI.pathCount) { + if (OnionRequestAPI.paths.isNotEmpty()) { setBackgroundResource(R.drawable.accent_dot) mainColor = resources.getColorWithID(R.color.accent, context.theme) sessionShadowColor = resources.getColorWithID(R.color.accent, context.theme) diff --git a/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt index 311739d44d..85105609c8 100644 --- a/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/src/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt @@ -68,32 +68,30 @@ class ProfilePictureView : RelativeLayout { return result ?: publicKey } } - if (recipient.isGroupRecipient) { - if ("Session Public Chat" == recipient.name) { - publicKey = "" - displayName = "" - additionalPublicKey = null - isRSSFeed = true - } else { - val users = MentionsManager.shared.userPublicKeyCache[threadID]?.toMutableList() ?: mutableListOf() - users.remove(TextSecurePreferences.getLocalNumber(context)) - val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - if (masterPublicKey != null) { - users.remove(masterPublicKey) - } - val randomUsers = users.sorted().toMutableList() // Sort to provide a level of stability - if (users.count() == 1) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - randomUsers.add(0, userPublicKey) // Ensure the current user is at the back visually - } - val pk = randomUsers.getOrNull(0) ?: "" - publicKey = pk - displayName = getUserDisplayName(pk) - val apk = randomUsers.getOrNull(1) ?: "" - additionalPublicKey = apk - additionalDisplayName = getUserDisplayName(apk) - isRSSFeed = recipient.name == "Loki News" || recipient.name == "Session Updates" + fun isOpenGroupWithProfilePicture(recipient: Recipient): Boolean { + return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null + } + if (recipient.isGroupRecipient && !isOpenGroupWithProfilePicture(recipient)) { + val users = MentionsManager.shared.userPublicKeyCache[threadID]?.toMutableList() ?: mutableListOf() + users.remove(TextSecurePreferences.getLocalNumber(context)) + val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + if (masterPublicKey != null) { + users.remove(masterPublicKey) } + val randomUsers = users.sorted().toMutableList() // Sort to provide a level of stability + if (users.count() == 1) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + randomUsers.add(0, userPublicKey) // Ensure the current user is at the back visually + } + val pk = randomUsers.getOrNull(0) ?: "" + publicKey = pk + displayName = getUserDisplayName(pk) + val apk = randomUsers.getOrNull(1) ?: "" + additionalPublicKey = apk + additionalDisplayName = getUserDisplayName(apk) + isRSSFeed = recipient.name == "Loki News" || + recipient.name == "Session Updates" || + recipient.name == "Session Public Chat" } else { publicKey = recipient.address.toString() displayName = getUserDisplayName(publicKey) diff --git a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 7fd5b3517c..7237cf9795 100644 --- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -84,13 +84,17 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil ContactPhoto contactPhoto = recipient.getContactPhoto(); if (contactPhoto != null) { try { - setLargeIcon(GlideApp.with(context.getApplicationContext()) - .load(contactPhoto) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .circleCrop() - .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), - context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)) - .get()); + // AC: For some reason, if not use ".asBitmap()" method, the returned BitmapDrawable + // wraps a recycled bitmap and leads to a crash. + Bitmap iconBitmap = GlideApp.with(context.getApplicationContext()) + .asBitmap() + .load(contactPhoto) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .circleCrop() + .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), + context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)) + .get(); + setLargeIcon(iconBitmap); } catch (InterruptedException | ExecutionException e) { Log.w(TAG, e); setLargeIcon(getPlaceholderDrawable(context, recipient)); diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 530aa6ecb1..24c44b19ac 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -419,6 +419,10 @@ public class Recipient implements RecipientModifiedListener { return address.isGroup(); } + public boolean isOpenGroupRecipient() { + return address.isOpenGroup(); + } + public boolean isMmsGroupRecipient() { return address.isMmsGroup(); } @@ -505,6 +509,11 @@ public class Recipient implements RecipientModifiedListener { if (notify) notifyListeners(); } + @Nullable + public synchronized Long getGroupAvatarId() { + return groupAvatarId; + } + public synchronized @Nullable Uri getMessageRingtone() { if (messageRingtone != null && messageRingtone.getScheme() != null && messageRingtone.getScheme().startsWith("file")) { return null;