diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index b2d9aa9cd8..94b4b3015f 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -43,6 +43,8 @@ import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseContentProviders; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule; @@ -75,6 +77,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.IncomingMessageObserver; @@ -207,6 +210,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc checkNeedsRevocation(); } } + updatePublicChatProfileAvatarIfNeeded(); } @Override @@ -597,6 +601,26 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded(); if (lokiMessengerUpdatesFeedPoller != null) lokiMessengerUpdatesFeedPoller.startIfNeeded(); } + + public void updatePublicChatProfileAvatarIfNeeded() { + AsyncTask.execute(() -> { + LokiPublicChatAPI publicChatAPI = getLokiPublicChatAPI(); + if (publicChatAPI != null) { + byte[] profileKey = ProfileKeyUtil.getProfileKey(this); + String url = TextSecurePreferences.getProfileAvatarUrl(this); + String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(this); + if (ourMasterDevice != null) { + Recipient masterDevice = Recipient.from(this, Address.fromSerialized(ourMasterDevice), false).resolve(); + profileKey = masterDevice.getProfileKey(); + url = masterDevice.getProfileAvatar(); + } + Set servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers(); + for (String server : servers) { + publicChatAPI.setProfileAvatar(server, profileKey, url); + } + } + }); + } // endregion public void checkNeedsRevocation() { diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index 5fdbce2c8e..adca4f3936 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -67,6 +67,7 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.security.SecureRandom; +import java.util.Collections; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -421,7 +422,9 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje // Upload was successful with this new profile key, we should set it so the other users know to re-fetch profiles ProfileKeyUtil.setEncodedProfileKey(context, newProfileKey); - // TODO: Update profile key in public chats here + + // Update profile key on the public chat server + ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded(); } catch (Exception e) { Log.d("Loki", "Failed to upload profile photo: " + e); return false; diff --git a/src/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/org/thoughtcrime/securesms/components/AvatarImageView.java index 294657c121..73a44ee5ef 100644 --- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -120,24 +120,26 @@ public class AvatarImageView extends AppCompatImageView { public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { if (recipient != null) { - RecipientContactPhoto photo = new RecipientContactPhoto(recipient); - if (!photo.equals(recipientContactPhoto)) { - requestManager.clear(this); - recipientContactPhoto = photo; + if (recipient.isLocalNumber()) { + setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted)); + } else { + RecipientContactPhoto photo = new RecipientContactPhoto(recipient); + if (!photo.equals(recipientContactPhoto)) { + requestManager.clear(this); + recipientContactPhoto = photo; - Drawable fallbackContactPhotoDrawable = recipient.isLocalNumber() - ? new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted) - : photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted); + Drawable fallbackContactPhotoDrawable = photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted); - if (photo.contactPhoto != null) { - requestManager.load(photo.contactPhoto) - .fallback(fallbackContactPhotoDrawable) - .error(fallbackContactPhotoDrawable) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .circleCrop() - .into(this); - } else { - setImageDrawable(fallbackContactPhotoDrawable); + if (photo.contactPhoto != null) { + requestManager.load(photo.contactPhoto) + .fallback(fallbackContactPhotoDrawable) + .error(fallbackContactPhotoDrawable) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .circleCrop() + .into(this); + } else { + setImageDrawable(fallbackContactPhotoDrawable); + } } } } else { diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 8465d82f10..fee75e62b5 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -130,6 +130,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.loki.api.DeviceLinkingSession; import org.whispersystems.signalservice.loki.api.LokiAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.PairingAuthorisation; import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; @@ -146,6 +147,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Set; import javax.inject.Inject; @@ -1442,6 +1444,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN); String url = content.senderProfileAvatarUrl.or(""); ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(recipient, url)); + + // Loki - If the recipient is our master device then we need to go and update our avatar mappings on the public chats + if (recipient.isOurMasterDevice()) { + ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded(); + } } } diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt index c99f480a75..ab091d25c3 100644 --- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt @@ -54,6 +54,7 @@ class DisplayNameActivity : BaseActionBarActivity() { application.startRSSFeedPollersIfNeeded() val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() servers.forEach { publicChatAPI.setDisplayName(name, it) } + application.updatePublicChatProfileAvatarIfNeeded() } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt index 756a7ebc6b..b1af5cc08c 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt @@ -6,9 +6,14 @@ import android.util.Log import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.then +import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.jobs.PushDecryptJob +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer @@ -21,7 +26,9 @@ import org.whispersystems.signalservice.loki.api.LokiPublicChat import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage import org.whispersystems.signalservice.loki.api.LokiStorageAPI +import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.utilities.successBackground +import java.security.MessageDigest import java.util.* class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) { @@ -155,6 +162,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})" DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName) } + val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey val serviceDataMessage = getDataMessage(message) val serviceContent = SignalServiceContent(serviceDataMessage, senderPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false) @@ -163,6 +171,25 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki } else { PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) } + + // Update profile avatar if needed + val senderRecipient = Recipient.from(context, Address.fromSerialized(senderPublicKey), false) + if (message.avatar != null && message.avatar!!.url.isNotEmpty()) { + val profileKey = message.avatar!!.profileKey + val url = message.avatar!!.url + if (senderRecipient.profileKey == null || !MessageDigest.isEqual(senderRecipient.profileKey, profileKey)) { + val database = DatabaseFactory.getRecipientDatabase(context) + database.setProfileKey(senderRecipient, profileKey) + ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, url)) + } + } else if (senderRecipient.profileAvatar.orEmpty().isNotEmpty()) { + // Unset the avatar if we had an avatar before and we're not friends with the person + val threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(senderRecipient) + val friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId) + if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { + ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, "")) + } + } } fun processOutgoingMessage(message: LokiPublicChatMessage) { val messageServerID = message.serverID ?: return @@ -178,6 +205,19 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki } else { PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript) } + + // Loki - If we got a message from our master device then make sure our mappings stay in sync + val recipient = Recipient.from(context, Address.fromSerialized(message.hexEncodedPublicKey), false) + if (recipient.isOurMasterDevice && message.avatar != null) { + val profileKey = message.avatar!!.profileKey + val url = message.avatar!!.url + if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profileKey)) { + val database = DatabaseFactory.getRecipientDatabase(context) + database.setProfileKey(recipient, profileKey) + database.setProfileAvatar(recipient, url) + ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded() + } + } } var userDevices = setOf() var uniqueDevices = setOf()