Merge pull request #368 from loki-project/open-group-profile-pictures

Open Group Profile Pictures
This commit is contained in:
Niels Andriesse 2020-10-28 14:58:40 +11:00 committed by GitHub
commit ced392a555
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 136 additions and 71 deletions

View file

@ -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;
}

View file

@ -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;
});

View file

@ -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<Address> members) {

View file

@ -155,6 +155,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());
@ -629,6 +630,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < lokiV15) {
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand());
}

View file

@ -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());

View file

@ -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);

View file

@ -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) {

View file

@ -340,20 +340,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

View file

@ -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) {
@ -56,7 +59,8 @@ class PublicChatManager(private val context: Context) {
}
public fun addChat(server: String, channel: Long): Promise<PublicChat, Exception> {
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)

View file

@ -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

View file

@ -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"
@ -351,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<DeviceLink> {
return setOf()

View file

@ -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 isOpenGroupWithAvatar(recipient: Recipient): Boolean {
return recipient.isOpenGroupRecipient && recipient.groupAvatarId != null
}
if (recipient.isGroupRecipient && !isOpenGroupWithAvatar(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)

View file

@ -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));

View file

@ -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;