diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index cb0a0d3fe..c46e6b897 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -8,6 +8,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java index c23b855cf..cf78973be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java @@ -12,12 +12,13 @@ public final class AppCapabilities { private static final boolean GV1_MIGRATION = true; private static final boolean ANNOUNCEMENT_GROUPS = true; private static final boolean SENDER_KEY = true; + private static final boolean CHANGE_NUMBER = true; /** * @param storageCapable Whether or not the user can use storage service. This is another way of * asking if the user has set a Signal PIN or not. */ public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) { - return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS); + return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 59b3cbd17..5ce5c952c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -87,6 +87,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onPlayInlineContent(ConversationMessage conversationMessage); void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord); void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted); + void onChangeNumberUpdateContact(@NonNull Recipient recipient); /** @return true if handled, false if you want to let the normal url handling continue */ boolean onUrlClicked(@NonNull String url); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt index 9b8a633dc..c7cfa277f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/AccountSettingsFragment.kt @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity import org.thoughtcrime.securesms.lock.v2.KbsConstants import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.ThemeUtil @@ -104,7 +105,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag sectionHeaderPref(R.string.AccountSettingsFragment__account) - if (FeatureFlags.changeNumber()) { + if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED) { clickPref( title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number), onClick = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 485bd9f35..446e1fbe7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -86,7 +86,6 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity; import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener; import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder; -import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.conversation.colors.Colorizer; import org.thoughtcrime.securesms.conversation.colors.ColorizerView; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemAnimator; @@ -136,6 +135,7 @@ import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment; import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientExporter; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment; import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity; @@ -1718,6 +1718,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect GroupDescriptionDialog.show(getChildFragmentManager(), groupId, description, isMessageRequestAccepted); } } + + @Override + public void onChangeNumberUpdateContact(@NonNull Recipient recipient) { + startActivity(RecipientExporter.export(recipient).asAddContactIntent()); + } } public void refreshList() { 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 82b05f952..1f860b4e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -408,6 +408,14 @@ public final class ConversationUpdateItem extends FrameLayout eventListener.onBadDecryptLearnMoreClicked(conversationMessage.getMessageRecord().getRecipient().getId()); } }); + } else if (conversationMessage.getMessageRecord().isChangeNumber() && conversationMessage.getMessageRecord().getIndividualRecipient().isSystemContact()) { + actionButton.setText(R.string.ConversationUpdateItem_update_contact); + actionButton.setVisibility(VISIBLE); + actionButton.setOnClickListener(v -> { + if (batchSelected.isEmpty() && eventListener != null) { + eventListener.onChangeNumberUpdateContact(conversationMessage.getMessageRecord().getIndividualRecipient()); + } + }); } else { actionButton.setVisibility(GONE); actionButton.setOnClickListener(null); @@ -480,10 +488,11 @@ public final class ConversationUpdateItem extends FrameLayout } private static boolean isSameType(@NonNull MessageRecord current, @NonNull MessageRecord candidate) { - return (current.isGroupUpdate() && candidate.isGroupUpdate()) || - (current.isProfileChange() && candidate.isProfileChange()) || - (current.isGroupCall() && candidate.isGroupCall()) || - (current.isExpirationTimerUpdate() && candidate.isExpirationTimerUpdate()); + return (current.isGroupUpdate() && candidate.isGroupUpdate()) || + (current.isProfileChange() && candidate.isProfileChange()) || + (current.isGroupCall() && candidate.isGroupCall()) || + (current.isExpirationTimerUpdate() && candidate.isExpirationTimerUpdate()) || + (current.isChangeNumber() && candidate.isChangeNumber()); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java index 49fbb2f11..f50e1d568 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -7,7 +7,6 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.FeatureFlags; import java.util.Set; import java.util.stream.Collectors; @@ -188,7 +187,8 @@ final class MenuState { messageRecord.isProfileChange() || messageRecord.isGroupV1MigrationEvent() || messageRecord.isChatSessionRefresh() || - messageRecord.isInMemoryMessageRecord(); + messageRecord.isInMemoryMessageRecord() || + messageRecord.isChangeNumber(); } private final static class Builder { 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 e7f0fc9cf..16b830486 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -491,6 +491,8 @@ public final class ConversationListItem extends ConstraintLayout return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint); } else if (SmsDatabase.Types.isProfileChange(thread.getType())) { return emphasisAdded(context, "", defaultTint); + } else if (SmsDatabase.Types.isChangeNumber(thread.getType())) { + return emphasisAdded(context, "", defaultTint); } else if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) { return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 05a551818..b58d0eb7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -167,6 +167,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException; public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName); public abstract void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, @NonNull GroupMigrationMembershipChange membershipChange); + public abstract void insertNumberChangeMessages(@NonNull Recipient recipient); public abstract boolean deleteMessage(long messageId); abstract void deleteThread(long threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index f5f700c73..e57b859d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -503,6 +503,11 @@ public class MmsDatabase extends MessageDatabase { throw new UnsupportedOperationException(); } + @Override + public void insertNumberChangeMessages(@NonNull Recipient recipient) { + throw new UnsupportedOperationException(); + } + @Override public void endTransaction(SQLiteDatabase database) { database.endTransaction(); 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 079796fcc..a5343da4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -77,6 +77,7 @@ public interface MmsSmsColumns { protected static final long OUTGOING_VIDEO_CALL_TYPE = 11; protected static final long GROUP_CALL_TYPE = 12; protected static final long BAD_DECRYPT_TYPE = 13; + protected static final long CHANGE_NUMBER_TYPE = 14; protected static final long BASE_INBOX_TYPE = 20; protected static final long BASE_OUTBOX_TYPE = 21; @@ -334,6 +335,10 @@ public interface MmsSmsColumns { return type == GV1_MIGRATION_TYPE; } + public static boolean isChangeNumber(long type) { + return type == CHANGE_NUMBER_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/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index d8982c649..f8103c531 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -108,7 +108,7 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.RECEIPT_TIMESTAMP}; private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " + - "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ") " + + "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ") " + "UNION ALL " + "SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsDatabase.MESSAGE_BOX + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + " " + "WHERE " + MmsSmsColumns.THREAD_ID + " = ? " + diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 377252ffc..8341a7307 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -29,9 +29,9 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore; -import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor; import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; @@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; +import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; @@ -59,6 +60,7 @@ import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.StringUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.wallpaper.ChatWallpaper; import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory; @@ -167,6 +169,7 @@ public class RecipientDatabase extends Database { static final int GROUPS_V1_MIGRATION = 1; static final int SENDER_KEY = 2; static final int ANNOUNCEMENT_GROUPS = 3; + static final int CHANGE_NUMBER = 4; } private static final String[] RECIPIENT_PROJECTION = new String[] { @@ -421,12 +424,17 @@ public class RecipientDatabase extends Database { } public @NonNull RecipientId getAndPossiblyMerge(@Nullable UUID uuid, @Nullable String e164, boolean highTrust) { + return getAndPossiblyMerge(uuid, e164, highTrust, false); + } + + public @NonNull RecipientId getAndPossiblyMerge(@Nullable UUID uuid, @Nullable String e164, boolean highTrust, boolean changeSelf) { if (uuid == null && e164 == null) { throw new IllegalArgumentException("Must provide a UUID or E164!"); } RecipientId recipientNeedingRefresh = null; Pair remapped = null; + RecipientId recipientChangedNumber = null; boolean transactionSuccessful = false; SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); @@ -485,9 +493,15 @@ public class RecipientDatabase extends Database { } else if (!byE164.isPresent() && byUuid.isPresent()) { if (e164 != null) { if (highTrust) { - Log.i(TAG, String.format(Locale.US, "Found out about an E164 (%s) for a known UUID user (%s). High-trust, so updating.", e164, byUuid.get()), true); - setPhoneNumberOrThrow(byUuid.get(), e164); - finalId = byUuid.get(); + if (Objects.equals(uuid, TextSecurePreferences.getLocalUuid(context)) && !changeSelf) { + Log.w(TAG, String.format(Locale.US, "Found out about an E164 (%s) for our own UUID user (%s). High-trust but not change self, doing nothing.", e164, byUuid.get()), true); + finalId = byUuid.get(); + } else { + Log.i(TAG, String.format(Locale.US, "Found out about an E164 (%s) for a known UUID user (%s). High-trust, so updating.", e164, byUuid.get()), true); + setPhoneNumberOrThrow(byUuid.get(), e164); + finalId = byUuid.get(); + recipientChangedNumber = finalId; + } } else { Log.i(TAG, String.format(Locale.US, "Found out about an E164 (%s) for a known UUID user (%s). Low-trust, so doing nothing.", e164, byUuid.get()), true); finalId = byUuid.get(); @@ -552,6 +566,10 @@ public class RecipientDatabase extends Database { StorageSyncHelper.scheduleSyncForDataChange(); RecipientId.clearCache(); } + + if (recipientChangedNumber != null) { + ApplicationDependencies.getJobManager().add(new RecipientChangedNumberJob(recipientChangedNumber)); + } } } } @@ -1616,6 +1634,7 @@ public class RecipientDatabase extends Database { value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize()); value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey()).serialize()); value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup()).serialize()); + value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber()).serialize()); ContentValues values = new ContentValues(1); values.put(CAPABILITIES, value); @@ -2040,7 +2059,7 @@ public class RecipientDatabase extends Database { try { RecipientId id = Recipient.self().getId(); - RecipientId newId = getAndPossiblyMerge(Recipient.self().requireUuid(), e164, true); + RecipientId newId = getAndPossiblyMerge(Recipient.self().requireUuid(), e164, true, true); if (id.equals(newId)) { Log.i(TAG, "[updateSelfPhone] Phone updated for self"); @@ -3159,6 +3178,7 @@ public class RecipientDatabase extends Database { private final Recipient.Capability groupsV1MigrationCapability; private final Recipient.Capability senderKeyCapability; private final Recipient.Capability announcementGroupCapability; + private final Recipient.Capability changeNumberCapability; private final InsightsBannerTier insightsBannerTier; private final byte[] storageId; private final MentionSetting mentionSetting; @@ -3251,6 +3271,7 @@ public class RecipientDatabase extends Database { this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH)); this.senderKeyCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH)); this.announcementGroupCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH)); + this.changeNumberCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH)); this.insightsBannerTier = insightsBannerTier; this.storageId = storageId; this.mentionSetting = mentionSetting; @@ -3408,6 +3429,10 @@ public class RecipientDatabase extends Database { return announcementGroupCapability; } + public @NonNull Recipient.Capability getChangeNumberCapability() { + return changeNumberCapability; + } + public @Nullable byte[] getStorageId() { return storageId; } 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 c16dfd73e..c22eba806 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -73,6 +73,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -270,8 +271,8 @@ public class SmsDatabase extends MessageDatabase { } private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) { - String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND TYPE != ?)"; - return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE); + String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND TYPE != ? AND TYPE != ?)"; + return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE); } @Override @@ -1027,6 +1028,53 @@ public class SmsDatabase extends MessageDatabase { databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); } + @Override + public void insertNumberChangeMessages(@NonNull Recipient recipient) { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + List groupRecords = DatabaseFactory.getGroupDatabase(context).getGroupsContainingMember(recipient.getId(), false); + List threadIdsToUpdate = new LinkedList<>(); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + db.beginTransaction(); + + try { + threadIdsToUpdate.add(threadDatabase.getThreadIdFor(recipient.getId())); + for (GroupDatabase.GroupRecord groupRecord : groupRecords) { + if (groupRecord.isActive()) { + threadIdsToUpdate.add(threadDatabase.getThreadIdFor(groupRecord.getRecipientId())); + } + } + + threadIdsToUpdate.stream() + .filter(Objects::nonNull) + .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.CHANGE_NUMBER_TYPE); + values.put(THREAD_ID, threadId); + values.putNull(BODY); + + db.insert(TABLE_NAME, null, values); + }); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + threadIdsToUpdate.stream() + .filter(Objects::nonNull) + .forEach(threadId -> { + TrimThreadJob.enqueueAsync(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); + notifyConversationListeners(threadId); + }); + } + @Override public Optional insertMessageInbox(IncomingTextMessage message, long type) { if (message.isJoined()) { 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 d4edd296b..4f3eb244c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -1514,7 +1514,8 @@ public class ThreadDatabase extends Database { private boolean isSilentType(long type) { return MmsSmsColumns.Types.isProfileChange(type) || - MmsSmsColumns.Types.isGroupV1MigrationEvent(type); + MmsSmsColumns.Types.isGroupV1MigrationEvent(type) || + MmsSmsColumns.Types.isChangeNumber(type); } public Reader readerFor(Cursor cursor) { 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 f578309ec..af9311273 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 @@ -178,6 +178,10 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isProfileChange(type); } + public boolean isChangeNumber() { + return SmsDatabase.Types.isChangeNumber(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 544b1475f..e6f62c6bb 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 @@ -187,6 +187,8 @@ public abstract class MessageRecord extends DisplayRecord { else return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, r.getDisplayName(context)), R.drawable.ic_update_info_16); } else if (isProfileChange()) { return staticUpdateDescription(getProfileChangeDescription(context), R.drawable.ic_update_profile_16); + } else if (isChangeNumber()) { + return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_changed_their_number_to_a_new_number, r.getDisplayName(context)), R.drawable.ic_phone_16); } else if (isEndSession()) { if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset), R.drawable.ic_update_info_16); else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_16); @@ -484,7 +486,8 @@ public abstract class MessageRecord extends DisplayRecord { public boolean isUpdate() { return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || - isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType(); + isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() || + isChangeNumber(); } public boolean isMediaPending() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 0a6cdcb7b..e8ede0681 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -131,6 +131,7 @@ public final class JobManagerFactories { put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory()); put(PushTextSendJob.KEY, new PushTextSendJob.Factory()); put(ReactionSendJob.KEY, new ReactionSendJob.Factory()); + put(RecipientChangedNumberJob.KEY, new RecipientChangedNumberJob.Factory()); put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory()); put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RecipientChangedNumberJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RecipientChangedNumberJob.kt new file mode 100644 index 000000000..3ef284e53 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RecipientChangedNumberJob.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId + +/** + * Insert change number update items in all threads (1:1 and group) with [recipientId]. + */ +class RecipientChangedNumberJob(parameters: Parameters, private val recipientId: RecipientId) : BaseJob(parameters) { + + constructor(recipientId: RecipientId) : this( + Parameters.Builder().setQueue("RecipientChangedNumberJob_${recipientId.toQueueKey()}").build(), + recipientId + ) + + override fun serialize(): Data { + return Data.Builder() + .putString(KEY_RECIPIENT_ID, recipientId.serialize()) + .build() + } + + override fun getFactoryKey(): String { + return KEY + } + + override fun onRun() { + val recipient: Recipient = Recipient.resolved(recipientId) + + if (!recipient.isBlocked && !recipient.isGroup && !recipient.isSelf) { + Log.i(TAG, "Writing a number change event.") + DatabaseFactory.getSmsDatabase(context).insertNumberChangeMessages(recipient) + } else { + Log.i(TAG, "Number changed but not relevant. blocked: ${recipient.isBlocked} isGroup: ${recipient.isGroup} isSelf: ${recipient.isSelf}") + } + } + + override fun onShouldRetry(e: Exception): Boolean = false + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data): RecipientChangedNumberJob { + return RecipientChangedNumberJob(parameters, RecipientId.from(data.getString(KEY_RECIPIENT_ID))) + } + } + + companion object { + const val KEY = "RecipientChangedNumberJob" + + private val TAG = Log.tag(RecipientChangedNumberJob::class.java) + private const val KEY_RECIPIENT_ID = "recipient_id" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index db472c319..236470011 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -103,6 +103,7 @@ public class RefreshAttributesJob extends BaseJob { "\n GV1 Migration? " + capabilities.isGv1Migration() + "\n Sender Key? " + capabilities.isSenderKey() + "\n Announcement Groups? " + capabilities.isAnnouncementGroup() + + "\n Change Number? " + capabilities.isChangeNumber() + "\n UUID? " + capabilities.isUuid()); SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java index 2c0029f04..47d7e2fb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java @@ -35,11 +35,13 @@ public final class LogSectionCapabilities implements LogSection { .append("GV1 Migration : ").append(capabilities.isGv1Migration()).append("\n") .append("Sender Key : ").append(capabilities.isSenderKey()).append("\n") .append("Announcement Groups: ").append(capabilities.isAnnouncementGroup()).append("\n") + .append("Change Number : ").append(capabilities.isChangeNumber()).append("\n") .append("\n") .append("-- Global").append("\n") .append("GV2 : ").append(self.getGroupsV2Capability()).append("\n") .append("GV1 Migration : ").append(self.getGroupsV1MigrationCapability()).append("\n") .append("Sender Key : ").append(self.getSenderKeyCapability()).append("\n") - .append("Announcement Groups: ").append(self.getAnnouncementGroupCapability()).append("\n"); + .append("Announcement Groups: ").append(self.getAnnouncementGroupCapability()).append("\n") + .append("Change Number : ").append(self.getChangeNumberCapability()).append("\n"); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index fef68986a..1c26ab4bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.migrations; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -40,7 +41,8 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - private static final class Version { + @VisibleForTesting + static final class Version { static final int LEGACY = 1; static final int RECIPIENT_ID = 2; static final int RECIPIENT_SEARCH = 3; @@ -84,9 +86,10 @@ public class ApplicationMigrations { static final int STICKER_MY_DAILY_LIFE = 42; static final int SENDER_KEY_3 = 43; static final int CHANGE_NUMBER_SYNC = 44; + static final int CHANGE_NUMBER_CAPABILITY = 45; } - public static final int CURRENT_VERSION = 43; + public static final int CURRENT_VERSION = 45; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -372,6 +375,10 @@ public class ApplicationMigrations { jobs.put(Version.CHANGE_NUMBER_SYNC, new AccountRecordMigrationJob()); } + if (lastSeenVersion < Version.CHANGE_NUMBER_CAPABILITY) { + jobs.put(Version.CHANGE_NUMBER_CAPABILITY, new AttributesMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 7441c5c8b..741498dd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.recipients; -import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier; - import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -63,6 +61,8 @@ import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; +import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier; + public class Recipient { private static final String TAG = Log.tag(Recipient.class); @@ -110,6 +110,7 @@ public class Recipient { private final Capability groupsV1MigrationCapability; private final Capability senderKeyCapability; private final Capability announcementGroupCapability; + private final Capability changeNumberCapability; private final InsightsBannerTier insightsBannerTier; private final byte[] storageId; private final MentionSetting mentionSetting; @@ -362,6 +363,7 @@ public class Recipient { this.groupsV1MigrationCapability = Capability.UNKNOWN; this.senderKeyCapability = Capability.UNKNOWN; this.announcementGroupCapability = Capability.UNKNOWN; + this.changeNumberCapability = Capability.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; @@ -414,6 +416,7 @@ public class Recipient { this.groupsV1MigrationCapability = details.groupsV1MigrationCapability; this.senderKeyCapability = details.senderKeyCapability; this.announcementGroupCapability = details.announcementGroupCapability; + this.changeNumberCapability = details.changeNumberCapability; this.storageId = details.storageId; this.mentionSetting = details.mentionSetting; this.wallpaper = details.wallpaper; @@ -918,6 +921,10 @@ public class Recipient { return announcementGroupCapability; } + public @NonNull Capability getChangeNumberCapability() { + return changeNumberCapability; + } + /** * True if this recipient supports the message retry system, or false if we should use the legacy session reset system. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index 1f962ff96..628309911 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -65,6 +65,7 @@ public class RecipientDetails { final Recipient.Capability groupsV1MigrationCapability; final Recipient.Capability senderKeyCapability; final Recipient.Capability announcementGroupCapability; + final Recipient.Capability changeNumberCapability; final InsightsBannerTier insightsBannerTier; final byte[] storageId; final MentionSetting mentionSetting; @@ -121,6 +122,7 @@ public class RecipientDetails { this.groupsV1MigrationCapability = settings.getGroupsV1MigrationCapability(); this.senderKeyCapability = settings.getSenderKeyCapability(); this.announcementGroupCapability = settings.getAnnouncementGroupCapability(); + this.changeNumberCapability = settings.getChangeNumberCapability(); this.insightsBannerTier = settings.getInsightsBannerTier(); this.storageId = settings.getStorageId(); this.mentionSetting = settings.getMentionSetting(); @@ -177,6 +179,7 @@ public class RecipientDetails { this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN; this.senderKeyCapability = Recipient.Capability.UNKNOWN; this.announcementGroupCapability = Recipient.Capability.UNKNOWN; + this.changeNumberCapability = Recipient.Capability.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; diff --git a/app/src/main/res/drawable-night/ic_phone_16.xml b/app/src/main/res/drawable-night/ic_phone_16.xml new file mode 100644 index 000000000..1244512b1 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_phone_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_phone_16.xml b/app/src/main/res/drawable/ic_phone_16.xml new file mode 100644 index 000000000..5af70338b --- /dev/null +++ b/app/src/main/res/drawable/ic_phone_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fb18baf9..bbd02b0dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1209,6 +1209,7 @@ You marked your safety number with %s unverified You marked your safety number with %s unverified from another device A message from %s couldn\'t be delivered + %1$s changed their number to a new number. %1$s started a group call ยท %2$s @@ -1950,6 +1951,7 @@ Call is full Invite friends Enable Call Notifications + Update contact No groups in common. Review requests carefully. No contacts in this group. Review requests carefully. View @@ -3505,7 +3507,7 @@ New phone number The phone number you entered doesn\'t match your account\'s. You must specify your old number\'s country code - You must specify your old number + You must specify your old phone number You must specify your new number\'s country code You must specify your new phone number diff --git a/app/src/test/java/org/thoughtcrime/securesms/migrations/ApplicationMigrationsTest.kt b/app/src/test/java/org/thoughtcrime/securesms/migrations/ApplicationMigrationsTest.kt new file mode 100644 index 000000000..2c8c68e1c --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/migrations/ApplicationMigrationsTest.kt @@ -0,0 +1,19 @@ +package org.thoughtcrime.securesms.migrations + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.lang.reflect.Field +import java.lang.reflect.Modifier + +class ApplicationMigrationsTest { + @Test + fun `ensure ApplicationMigration CURRENT_VERSION matches max version`() { + val fields: Array = ApplicationMigrations.Version::class.java.declaredFields + + val maxField: Int? = fields.filter { Modifier.isStatic(it.modifiers) && it.type == Int::class.java } + .map { it.getInt(null) } + .maxOrNull() + + assertEquals(ApplicationMigrations.CURRENT_VERSION, maxField) + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java index 3e17c77d7..78f579877 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java @@ -9,8 +9,6 @@ package org.whispersystems.signalservice.api.account; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; - public class AccountAttributes { @JsonProperty @@ -134,16 +132,20 @@ public class AccountAttributes { @JsonProperty private boolean announcementGroup; + @JsonProperty + private boolean changeNumber; + @JsonCreator public Capabilities() {} - public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup) { + public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber) { this.uuid = uuid; this.gv2 = gv2; this.storage = storage; this.gv1Migration = gv1Migration; this.senderKey = senderKey; this.announcementGroup = announcementGroup; + this.changeNumber = changeNumber; } public boolean isUuid() { @@ -169,5 +171,9 @@ public class AccountAttributes { public boolean isAnnouncementGroup() { return announcementGroup; } + + public boolean isChangeNumber() { + return changeNumber; + } } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index 544e6eadd..7f5b0fdf3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -127,6 +127,9 @@ public class SignalServiceProfile { @JsonProperty private boolean announcementGroup; + @JsonProperty + private boolean changeNumber; + @JsonCreator public Capabilities() {} @@ -149,6 +152,10 @@ public class SignalServiceProfile { public boolean isAnnouncementGroup() { return announcementGroup; } + + public boolean isChangeNumber() { + return changeNumber; + } } public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java index 0091caf06..276bee4a3 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java @@ -16,7 +16,7 @@ public final class AccountAttributesTest { "reglock1234", new byte[10], false, - new AccountAttributes.Capabilities(true, true, true, true, true, true), + new AccountAttributes.Capabilities(true, true, true, true, true, true, true), false)); assertEquals("{\"signalingKey\":\"skey\"," + "\"registrationId\":123," + @@ -28,18 +28,18 @@ public final class AccountAttributesTest { "\"unidentifiedAccessKey\":\"AAAAAAAAAAAAAA==\"," + "\"unrestrictedUnidentifiedAccess\":false," + "\"discoverableByPhoneNumber\":false," + - "\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json); + "\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"changeNumber\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json); } @Test public void gv2_true() { - String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false)); - assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"gv2-3\":true,\"gv1-migration\":false}", json); + String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false, false)); + assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"gv2-3\":true,\"gv1-migration\":false}", json); } @Test public void gv2_false() { - String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false)); - assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"gv2-3\":false,\"gv1-migration\":false}", json); + String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false, false)); + assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"gv2-3\":false,\"gv1-migration\":false}", json); } } \ No newline at end of file