diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 45f421a325..36bf9c87ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -134,6 +134,7 @@ public class ConversationUpdateItem extends LinearLayout else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord); else if (messageRecord.isIdentityVerified() || messageRecord.isIdentityDefault()) setIdentityVerifyUpdate(messageRecord); + else if (messageRecord.isProfileChange()) setProfileNameChangeRecord(messageRecord); else throw new AssertionError("Neither group nor log nor joined."); if (batchSelected.contains(messageRecord)) setSelected(true); @@ -195,6 +196,16 @@ public class ConversationUpdateItem extends LinearLayout date.setVisibility(GONE); } + private void setProfileNameChangeRecord(MessageRecord messageRecord) { + icon.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_outline_20)); + icon.setColorFilter(getIconTintFilter()); + body.setText(messageRecord.getDisplayBody(getContext())); + + title.setVisibility(GONE); + body.setVisibility(VISIBLE); + date.setVisibility(GONE); + } + private void setGroupRecord(MessageRecord messageRecord) { icon.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.menu_group_icon)); icon.clearColorFilter(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 4aeaa03617..e17211c536 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; 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 172ab8f0f8..5320c872c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.SqlUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -224,12 +225,22 @@ public final class GroupDatabase extends Database { @WorkerThread public @NonNull List getPushGroupsContainingMember(@NonNull RecipientId recipientId) { + return getGroupsContainingMember(recipientId, true); + } + + @WorkerThread + public @NonNull List getGroupsContainingMember(@NonNull RecipientId recipientId, boolean pushOnly) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); String table = TABLE_NAME + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID; - String query = MEMBERS + " LIKE ? AND " + MMS + " = ?"; - String[] args = new String[]{"%" + recipientId.serialize() + "%", "0"}; + String query = MEMBERS + " LIKE ?"; + String[] args = new String[]{"%" + recipientId.serialize() + "%"}; String orderBy = ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " DESC"; + if (pushOnly) { + query += " AND " + MMS + " = ?"; + args = SqlUtil.appendArg(args, "0"); + } + List groups = new LinkedList<>(); try (Cursor cursor = database.query(table, null, query, args, null, null, orderBy)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index f1f4b52df6..64bcae9236 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -38,6 +38,7 @@ public interface MmsSmsColumns { protected static final long JOINED_TYPE = 4; protected static final long UNSUPPORTED_MESSAGE_TYPE = 5; protected static final long INVALID_MESSAGE_TYPE = 6; + protected static final long PROFILE_CHANGE_TYPE = 7; protected static final long BASE_INBOX_TYPE = 20; protected static final long BASE_OUTBOX_TYPE = 21; @@ -255,6 +256,10 @@ public interface MmsSmsColumns { (type & ENCRYPTION_REMOTE_BIT) != 0; } + public static boolean isProfileChange(long type) { + return type == PROFILE_CHANGE_TYPE; + } + public static long translateFromSystemBaseType(long theirType) { // public static final int NONE_TYPE = 0; // public static final int INBOX_TYPE = 1; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 799cdb5370..205e279d2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; +import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.logging.Log; @@ -45,6 +46,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; @@ -631,6 +633,57 @@ public class SmsDatabase extends MessagingDatabase { return new Pair<>(messageId, threadId); } + public void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName) { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + List groupRecords = DatabaseFactory.getGroupDatabase(context).getGroupsContainingMember(recipient.getId(), false); + List threadIdsToUpdate = new LinkedList<>(); + + byte[] profileChangeDetails = ProfileChangeDetails.newBuilder() + .setProfileNameChange(ProfileChangeDetails.StringChange.newBuilder() + .setNew(newProfileName) + .setPrevious(previousProfileName)) + .build() + .toByteArray(); + + String body = Base64.encodeBytes(profileChangeDetails); + + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.beginTransaction(); + + try { + threadIdsToUpdate.add(threadDatabase.getThreadIdFor(recipient.getId())); + for (GroupDatabase.GroupRecord groupRecord : groupRecords) { + threadIdsToUpdate.add(threadDatabase.getThreadIdFor(groupRecord.getRecipientId())); + } + + Stream.of(threadIdsToUpdate) + .withoutNulls() + .forEach(threadId -> { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipient.getId().serialize()); + values.put(ADDRESS_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.PROFILE_CHANGE_TYPE); + values.put(THREAD_ID, threadId); + values.put(BODY, body); + + db.insert(TABLE_NAME, null, values); + }); + + for (long threadId : threadIdsToUpdate) { + DatabaseFactory.getThreadDatabase(context).update(threadId, true); + notifyConversationListeners(threadId); + ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId)); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + protected Optional insertMessageInbox(IncomingTextMessage message, long type) { if (message.isJoined()) { type = (type & (Types.TOTAL_MASK - Types.BASE_TYPE_MASK)) | Types.JOINED_TYPE; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 4820fd794c..4b4736bf1f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -175,14 +175,16 @@ public class ThreadDatabase extends Database { } } - ContentValues contentValues = new ContentValues(7); - contentValues.put(DATE, date - date % 1000); + ContentValues contentValues = new ContentValues(); + if (!MmsSmsColumns.Types.isProfileChange(type)) { + contentValues.put(DATE, date - date % 1000); + contentValues.put(SNIPPET, body); + contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString()); + contentValues.put(SNIPPET_TYPE, type); + contentValues.put(SNIPPET_CONTENT_TYPE, contentType); + contentValues.put(SNIPPET_EXTRAS, extraSerialized); + } contentValues.put(MESSAGE_COUNT, count); - contentValues.put(SNIPPET, body); - contentValues.put(SNIPPET_URI, attachment == null ? null : attachment.toString()); - contentValues.put(SNIPPET_TYPE, type); - contentValues.put(SNIPPET_CONTENT_TYPE, contentType); - contentValues.put(SNIPPET_EXTRAS, extraSerialized); contentValues.put(STATUS, status); contentValues.put(DELIVERY_RECEIPT_COUNT, deliveryReceiptCount); contentValues.put(READ_RECEIPT_COUNT, readReceiptCount); @@ -202,8 +204,11 @@ public class ThreadDatabase extends Database { } public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) { - ContentValues contentValues = new ContentValues(4); + if (MmsSmsColumns.Types.isProfileChange(type)) { + return; + } + ContentValues contentValues = new ContentValues(); contentValues.put(DATE, date - date % 1000); contentValues.put(SNIPPET, snippet); contentValues.put(SNIPPET_TYPE, type); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 7a4765905e..ef915fc60f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -156,6 +156,10 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isIdentityDefault(type) || SmsDatabase.Types.isIdentityVerified(type); } + public boolean isProfileChange() { + return SmsDatabase.Types.isProfileChange(type); + } + public int getDeliveryStatus() { return deliveryStatus; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 41802f69b0..c5b40e0a1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -31,7 +31,9 @@ import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; +import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.Base64; @@ -142,6 +144,8 @@ public abstract class MessageRecord extends DisplayRecord { } else if (isIdentityDefault()) { if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().getDisplayName(context))); else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().getDisplayName(context))); + } else if (isProfileChange()) { + return new SpannableString(getProfileChangeDescription(context)); } return new SpannableString(getBody()); @@ -177,6 +181,29 @@ public abstract class MessageRecord extends DisplayRecord { } } + private @NonNull String getProfileChangeDescription(@NonNull Context context) { + try { + byte[] decoded = Base64.decode(getBody()); + ProfileChangeDetails profileChangeDetails = ProfileChangeDetails.parseFrom(decoded); + + if (profileChangeDetails.hasProfileNameChange()) { + String displayName = getIndividualRecipient().getDisplayName(context); + String newName = ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getNew()).toString(); + String previousName = ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getPrevious()).toString(); + + if (getIndividualRecipient().isSystemContact()) { + return context.getString(R.string.MessageRecord_changed_their_profile_name_from_to, displayName, previousName, newName); + } else { + return context.getString(R.string.MessageRecord_changed_their_profile_name_to, previousName, newName); + } + } + } catch (IOException e) { + Log.w(TAG, "Profile name change details could not be read", e); + } + + return context.getString(R.string.MessageRecord_changed_their_profile, getIndividualRecipient().getDisplayName(context)); + } + /** * Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}. */ @@ -254,7 +281,7 @@ public abstract class MessageRecord extends DisplayRecord { public boolean isUpdate() { return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || - isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault(); + isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isProfileChange(); } public boolean isMediaPending() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 388b195a89..8f440e0ba6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -377,8 +377,16 @@ public class RetrieveProfileJob extends BaseJob { String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName); if (!Objects.equals(plaintextProfileName, recipient.getProfileName().serialize())) { + String newProfileName = TextUtils.isEmpty(plaintextProfileName) ? ProfileName.EMPTY.serialize() : plaintextProfileName; + String previousProfileName = recipient.getProfileName().serialize(); + Log.i(TAG, "Profile name updated. Writing new value."); DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), ProfileName.fromSerialized(plaintextProfileName)); + + if (!recipient.isGroup() && !recipient.isLocalNumber()) { + //noinspection ConstantConditions + DatabaseFactory.getSmsDatabase(context).insertProfileNameChangeMessages(recipient, newProfileName, previousProfileName); + } } if (TextUtils.isEmpty(plaintextProfileName)) { diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 5da6e253bf..5bbbfbd770 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -46,3 +46,12 @@ message AudioWaveFormData { int64 durationUs = 1; bytes waveForm = 2; } + +message ProfileChangeDetails { + message StringChange { + string previous = 1; + string new = 2; + } + + StringChange profileNameChange = 1; +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fb52ff219..29fb9ecbfc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -368,7 +368,7 @@ Problem setting profile Profile photo Set up your profile - Your profile is end-to-end encrypted. It will be visible to your contacts, when you initiate or accept new conversations, and when you join new groups. + Your profile is end-to-end encrypted. Your profile and changes to it will be visible to your contacts, when you initiate or accept new conversations, and when you join new groups. Set avatar @@ -762,6 +762,11 @@ %1$s set the disappearing message timer to %2$s. The disappearing message timer has been set to %1$s. + + %1$s changed their profile name to %2$s. + %1$s changed their profile name from %2$s to %3$s. + %1$s changed their profile. + You created the group. Group updated.