From 44f5684b219c12744cf21fffbe5beb585c9f124d Mon Sep 17 00:00:00 2001 From: ceokot Date: Wed, 15 Dec 2021 08:11:55 +0200 Subject: [PATCH] feat: Update open group avatars periodically (#807) * feat: Update open group avatars periodically * Updated timestamp * Existing job check * Refresh avatar on the conversation * Remove println statement * Update profile picture on recipient modified event --- .../conversation/v2/ConversationActivityV2.kt | 1 + .../securesms/database/GroupDatabase.java | 18 ++++++- .../securesms/database/SessionJobDatabase.kt | 17 ++++-- .../securesms/database/Storage.kt | 9 ++++ .../database/helpers/SQLCipherOpenHelper.java | 8 ++- .../securesms/groups/OpenGroupManager.kt | 14 +---- .../libsession/database/StorageProtocol.kt | 3 ++ .../messaging/jobs/GroupAvatarDownloadJob.kt | 54 +++++++++++++++++++ .../libsession/messaging/jobs/JobQueue.kt | 16 ++++-- .../jobs/SessionJobManagerFactories.kt | 3 +- .../pollers/OpenGroupPollerV2.kt | 14 ++++- .../libsession/utilities/GroupRecord.kt | 5 +- 12 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 0c4751d805..267403ff14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -501,6 +501,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } updateSubtitle() showOrHideInputIfNeeded() + profilePictureView.update(recipient, threadID) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 78cc9b405d..a9042ed399 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -53,6 +53,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt private static final String TIMESTAMP = "timestamp"; private static final String ACTIVE = "active"; private static final String MMS = "mms"; + private static final String UPDATED = "updated"; // Loki private static final String AVATAR_URL = "avatar_url"; @@ -83,11 +84,16 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt private static final String[] GROUP_PROJECTION = { GROUP_ID, TITLE, MEMBERS, ZOMBIE_MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, - TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS + TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS, UPDATED }; static final List TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList(); + public static String getCreateUpdatedTimestampCommand() { + return "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + UPDATED + " INTEGER DEFAULT 0;"; + } + public GroupDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); } @@ -330,6 +336,13 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); } + public void updateTimestampUpdated(String groupId, Long updatedTimestamp) { + ContentValues contents = new ContentValues(); + contents.put(UPDATED, updatedTimestamp); + + databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId}); + } + public void removeMember(String groupId, Address source) { List
currentMembers = getCurrentMembers(groupId, false); currentMembers.remove(source); @@ -439,7 +452,8 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1, cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)), cursor.getString(cursor.getColumnIndexOrThrow(ADMINS)), - cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP))); + cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)), + cursor.getLong(cursor.getColumnIndexOrThrow(UPDATED))); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt index 49729241a5..95d7e5e3d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt @@ -3,13 +3,17 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import net.sqlcipher.Cursor -import org.session.libsession.messaging.jobs.* +import org.session.libsession.messaging.jobs.AttachmentUploadJob +import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob +import org.session.libsession.messaging.jobs.Job +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.messaging.jobs.MessageSendJob +import org.session.libsession.messaging.jobs.SessionJobInstantiator +import org.session.libsession.messaging.jobs.SessionJobManagerFactories import org.session.libsession.messaging.utilities.Data import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.database.* import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer -import org.thoughtcrime.securesms.util.* class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { @@ -78,6 +82,13 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } + fun getGroupAvatarDownloadJob(server: String, room: String): GroupAvatarDownloadJob? { + val database = databaseHelper.readableDatabase + return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(GroupAvatarDownloadJob.KEY)) { + jobFromCursor(it) as GroupAvatarDownloadJob? + }.filterNotNull().find { it.server == server && it.room == room } + } + fun cancelPendingMessageSendJobs(threadID: Long) { val database = databaseHelper.writableDatabase val attachmentUploadJobKeys = mutableListOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 196bffcdb6..1dfc1f0b25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -185,6 +185,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).sessionJobDatabase().getMessageReceiveJob(messageReceiveJobID) } + override fun getGroupAvatarDownloadJob(server: String, room: String): GroupAvatarDownloadJob? { + return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room) + } + override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return JobQueue.shared.resumePendingSendMessage(job) @@ -468,6 +472,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .updateFormationTimestamp(groupID, formationTimestamp) } + override fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long) { + DatabaseComponent.get(context).groupDatabase() + .updateTimestampUpdated(groupID, updatedTimestamp) + } + override fun setExpirationTimer(groupID: String, duration: Int) { val recipient = Recipient.from(context, fromSerialized(groupID), false) DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index e563e6e5c3..b0a02041ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -61,9 +61,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV27 = 48; private static final int lokiV28 = 49; private static final int lokiV29 = 50; + private static final int lokiV30 = 51; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV29; + private static final int DATABASE_VERSION = lokiV30; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -136,6 +137,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand()); db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand()); db.execSQL(ThreadDatabase.getCreatePinnedCommand()); + db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -314,6 +316,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(ThreadDatabase.getCreatePinnedCommand()); } + if (oldVersion < lokiV30) { + db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index 3545e2fd2a..e18f1a8e8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.groups import android.content.Context -import android.graphics.Bitmap import androidx.annotation.WorkerThread import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration @@ -72,18 +71,9 @@ object OpenGroupManager { OpenGroupAPIV2.getAuthToken(room, server).get() // Get group info val info = OpenGroupAPIV2.getInfo(room, server).get() - // Download the group image - // FIXME: Don't wait for the image to download - val image: Bitmap? + // Create the group locally if not available already if (threadID < 0) { - val profilePictureAsByteArray = try { - OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id, server).get() - } catch (e: Exception) { - null - } - image = BitmapUtil.fromByteArray(profilePictureAsByteArray) - // Create the group locally - threadID = GroupManager.createOpenGroup(openGroupID, context, image, info.name).threadId + threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId } val openGroup = OpenGroupV2(server, room, info.name, publicKey) threadDB.setOpenGroupChat(openGroup, threadID) diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 09a3ba5e4c..c983f524a6 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.AttachmentUploadJob +import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.messages.control.ConfigurationMessage @@ -43,6 +44,7 @@ interface StorageProtocol { fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? fun getMessageSendJob(messageSendJobID: String): MessageSendJob? fun getMessageReceiveJob(messageReceiveJobID: String): Job? + fun getGroupAvatarDownloadJob(server: String, room: String): Job? fun resumeMessageSendJobIfNeeded(messageSendJobID: String) fun isJobCanceled(job: Job): Boolean @@ -117,6 +119,7 @@ interface StorageProtocol { fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? fun updateFormationTimestamp(groupID: String, formationTimestamp: Long) + fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long) fun setExpirationTimer(groupID: String, duration: Int) // Groups diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt new file mode 100644 index 0000000000..227944a889 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt @@ -0,0 +1,54 @@ +package org.session.libsession.messaging.jobs + +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.session.libsession.messaging.utilities.Data +import org.session.libsession.utilities.GroupUtil + +class GroupAvatarDownloadJob(val room: String, val server: String) : Job { + + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + override val maxFailureCount: Int = 10 + + override fun execute() { + val storage = MessagingModuleConfiguration.shared.storage + try { + val info = OpenGroupAPIV2.getInfo(room, server).get() + val bytes = OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id, server).get() + val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) + storage.updateProfilePicture(groupId, bytes) + storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) + delegate?.handleJobSucceeded(this) + } catch (e: Exception) { + delegate?.handleJobFailed(this, e) + } + } + + override fun serialize(): Data { + return Data.Builder() + .putString(ROOM, room) + .putString(SERVER, server) + .build() + } + + override fun getFactoryKey(): String = KEY + + companion object { + const val KEY = "GroupAvatarDownloadJob" + + private const val ROOM = "room" + private const val SERVER = "server" + } + + class Factory : Job.Factory { + + override fun create(data: Data): GroupAvatarDownloadJob { + return GroupAvatarDownloadJob( + data.getString(ROOM), + data.getString(SERVER) + ) + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 88510fb278..016fb27e05 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -45,9 +45,16 @@ class JobQueue : JobDelegate { while (isActive) { for (job in queue) { when (job) { - is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> txQueue.send(job) - is MessageReceiveJob, is TrimThreadJob, is BatchMessageReceiveJob, is AttachmentDownloadJob-> rxQueue.send(job) - else -> throw IllegalStateException("Unexpected job type.") + is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> { + txQueue.send(job) + } + is MessageReceiveJob, is TrimThreadJob, is BatchMessageReceiveJob, + is AttachmentDownloadJob, is GroupAvatarDownloadJob -> { + rxQueue.send(job) + } + else -> { + throw IllegalStateException("Unexpected job type.") + } } } } @@ -123,7 +130,8 @@ class JobQueue : JobDelegate { MessageReceiveJob.KEY, MessageSendJob.KEY, NotifyPNServerJob.KEY, - BatchMessageReceiveJob.KEY + BatchMessageReceiveJob.KEY, + GroupAvatarDownloadJob.KEY ) allJobTypes.forEach { type -> resumePendingJobs(type) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt index cc22c59dbf..9a3a97401c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt @@ -12,7 +12,8 @@ class SessionJobManagerFactories { MessageSendJob.KEY to MessageSendJob.Factory(), NotifyPNServerJob.KEY to NotifyPNServerJob.Factory(), TrimThreadJob.KEY to TrimThreadJob.Factory(), - BatchMessageReceiveJob.KEY to BatchMessageReceiveJob.Factory() + BatchMessageReceiveJob.KEY to BatchMessageReceiveJob.Factory(), + GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory() ) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt index 194072e543..c604fb7f9b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt @@ -40,6 +40,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService: fun poll(isBackgroundPoll: Boolean = false): Promise { val storage = MessagingModuleConfiguration.shared.storage val rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room } + rooms.forEach { downloadGroupAvatarIfNeeded(it) } return OpenGroupAPIV2.compactPoll(rooms, server).successBackground { responses -> responses.forEach { (room, response) -> val openGroupID = "$server.$room" @@ -50,7 +51,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService: } } }.always { - executorService?.schedule(this@OpenGroupPollerV2::poll, OpenGroupPollerV2.pollInterval, TimeUnit.MILLISECONDS) + executorService?.schedule(this@OpenGroupPollerV2::poll, pollInterval, TimeUnit.MILLISECONDS) }.map { } } @@ -103,4 +104,15 @@ class OpenGroupPollerV2(private val server: String, private val executorService: storage.setLastDeletionServerID(room, server, latestMax) } } + + private fun downloadGroupAvatarIfNeeded(room: String) { + val storage = MessagingModuleConfiguration.shared.storage + if (storage.getGroupAvatarDownloadJob(server, room) != null) return + val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) + storage.getGroup(groupId)?.let { + if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) { + JobQueue.shared.add(GroupAvatarDownloadJob(room, server)) + } + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt index 348b6bcae9..630e6a89ef 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupRecord.kt @@ -1,15 +1,14 @@ package org.session.libsession.utilities import android.text.TextUtils -import org.session.libsession.utilities.Address import java.io.IOException -import java.util.* +import java.util.LinkedList class GroupRecord( val encodedId: String, val title: String, members: String?, val avatar: ByteArray?, val avatarId: Long?, val avatarKey: ByteArray?, val avatarContentType: String?, val relay: String?, val isActive: Boolean, val avatarDigest: ByteArray?, val isMms: Boolean, - val url: String?, admins: String?, val formationTimestamp: Long + val url: String?, admins: String?, val formationTimestamp: Long, val updatedTimestamp: Long ) { var members: List
= LinkedList
() var admins: List
= LinkedList
()