From bb30535afb79c8570fc7aa75b56d03892be6b70f Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 20 Dec 2023 11:48:02 -0500 Subject: [PATCH] Respect the phoneNumberSharing setting on the profile. --- .../PhoneNumberPrivacySettingsViewModel.kt | 2 + .../preferences/BioTextPreference.kt | 6 ++- .../contacts/paged/ContactSearchAdapter.kt | 4 +- .../conversation/v2/ConversationAdapterV2.kt | 2 +- .../securesms/database/RecipientTable.kt | 42 +++++++++++++++---- .../database/RecipientTableCursorUtil.kt | 3 +- .../helpers/SignalDatabaseMigrations.kt | 6 ++- .../V214_PhoneNumberSharingColumn.kt | 19 +++++++++ .../database/model/RecipientRecord.kt | 4 +- .../securesms/jobs/RefreshOwnProfileJob.java | 40 ++++++++++++++++++ .../securesms/jobs/RetrieveProfileJob.java | 31 ++++++++++++++ .../WhoCanFindMeByPhoneNumberRepository.kt | 4 ++ .../securesms/recipients/Recipient.java | 18 +++++++- .../securesms/recipients/RecipientDetails.kt | 10 +++-- .../recipients/RecipientExporter.java | 2 +- .../recipients/ui/about/AboutSheet.kt | 4 +- .../RecipientBottomSheetDialogFragment.java | 2 +- .../securesms/util/ProfileUtil.java | 14 ++++++- .../database/RecipientDatabaseTestUtils.kt | 3 +- .../recipients/RecipientExporterTest.java | 19 +++++++++ .../api/SignalServiceAccountManager.java | 17 ++++---- .../api/crypto/ProfileCipher.java | 18 ++++++++ .../api/profiles/SignalServiceProfile.java | 7 ++++ .../profiles/SignalServiceProfileWrite.java | 24 ++++++----- 24 files changed, 257 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V214_PhoneNumberSharingColumn.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/pnp/PhoneNumberPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/pnp/PhoneNumberPrivacySettingsViewModel.kt index 1d5893adb3..d6ac569cab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/pnp/PhoneNumberPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/pnp/PhoneNumberPrivacySettingsViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.ProfileUploadJob import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberListingMode @@ -45,6 +46,7 @@ class PhoneNumberPrivacySettingsViewModel : ViewModel() { SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = if (phoneNumberSharingEnabled) PhoneNumberSharingMode.EVERYBODY else PhoneNumberSharingMode.NOBODY SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() + ApplicationDependencies.getJobManager().add(ProfileUploadJob()) refresh() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt index 25f650c3d4..3b228710d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt @@ -70,7 +70,11 @@ object BioTextPreference { } } - override fun getSubhead2Text(): String? = recipient.e164.map(PhoneNumberFormatter::prettyPrint).orElse(null) + override fun getSubhead2Text(): String? = if (recipient.shouldShowE164()) { + recipient.e164.map(PhoneNumberFormatter::prettyPrint).orElse(null) + } else { + null + } override fun areContentsTheSame(newItem: RecipientModel): Boolean { return super.areContentsTheSame(newItem) && newItem.recipient.hasSameContent(recipient) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt index ff1cbbcf85..a8d2118483 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchAdapter.kt @@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode import org.thoughtcrime.securesms.database.model.StoryViewState import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter @@ -437,8 +436,7 @@ open class ContactSearchAdapter( number.text = recipient.combinedAboutAndEmoji number.visible = true } else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164()) { - number.text = PhoneNumberFormatter.prettyPrint(recipient.requireE164()) - number.visible = true + number.visible = false } else { super.bindNumberField(model) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt index 2fa7cd25f1..07e1e14b35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt @@ -581,7 +581,7 @@ class ConversationAdapterV2( } else if (isSelf) { conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation)) } else { - val subtitle: String? = recipient.e164.map { e164: String? -> PhoneNumberFormatter.prettyPrint(e164!!) }.orElse(null) + val subtitle: String? = recipient.takeIf { it.shouldShowE164() }?.e164?.map { e164: String? -> PhoneNumberFormatter.prettyPrint(e164!!) }?.orElse(null) if (subtitle == null || subtitle == title) { conversationBanner.hideSubtitle() } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 7a168d01ba..2b7e551b26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -177,6 +177,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da const val BADGES = "badges" const val NEEDS_PNI_SIGNATURE = "needs_pni_signature" const val REPORTING_TOKEN = "reporting_token" + const val PHONE_NUMBER_SHARING = "phone_number_sharing" const val SEARCH_PROFILE_NAME = "search_signal_profile" const val SORT_NAME = "sort_name" @@ -242,7 +243,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da $CUSTOM_CHAT_COLORS_ID INTEGER DEFAULT 0, $BADGES BLOB DEFAULT NULL, $NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0, - $REPORTING_TOKEN BLOB DEFAULT NULL + $REPORTING_TOKEN BLOB DEFAULT NULL, + $PHONE_NUMBER_SHARING INTEGER DEFAULT ${PhoneNumberSharingState.UNKNOWN.id} ) """ @@ -301,7 +303,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da CUSTOM_CHAT_COLORS_ID, BADGES, NEEDS_PNI_SIGNATURE, - REPORTING_TOKEN + REPORTING_TOKEN, + PHONE_NUMBER_SHARING ) private val ID_PROJECTION = arrayOf(ID) @@ -1804,6 +1807,15 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } } + fun setPhoneNumberSharing(id: RecipientId, phoneNumberSharing: PhoneNumberSharingState) { + val contentValues = contentValuesOf( + PHONE_NUMBER_SHARING to phoneNumberSharing.id + ) + if (update(id, contentValues)) { + ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) + } + } + fun resetAllWallpaper() { val database = writableDatabase val selection = SqlUtil.buildArgs(ID, WALLPAPER_URI) @@ -3339,7 +3351,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da ( $SORT_NAME GLOB ? OR $USERNAME GLOB ? OR - $E164 GLOB ? OR + ${ContactSearchSelection.E164_SEARCH} OR $EMAIL GLOB ? ) """ @@ -3360,7 +3372,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da ( $SORT_NAME GLOB ? OR $USERNAME GLOB ? OR - $E164 GLOB ? OR + ${ContactSearchSelection.E164_SEARCH} OR $EMAIL GLOB ? )) """ @@ -3381,7 +3393,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da AND ( $SORT_NAME GLOB ? OR $USERNAME GLOB ? OR - $E164 GLOB ? OR + ${ContactSearchSelection.E164_SEARCH} OR $EMAIL GLOB ? ) """ @@ -4401,16 +4413,17 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da WHERE ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID} = $TABLE_NAME.$ID AND ${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} = 1 AND ${GroupTable.TABLE_NAME}.${GroupTable.MMS} = 0 ) """ + val E164_SEARCH = "(($PHONE_NUMBER_SHARING != ${PhoneNumberSharingState.DISABLED.id} OR $SYSTEM_CONTACT_URI NOT NULL) AND $E164 GLOB ?)" const val FILTER_GROUPS = " AND $GROUP_ID IS NULL" const val FILTER_ID = " AND $ID != ?" const val FILTER_BLOCKED = " AND $BLOCKED = ?" const val FILTER_HIDDEN = " AND $HIDDEN = ?" const val NON_SIGNAL_CONTACT = "$REGISTERED != ? AND $SYSTEM_CONTACT_URI NOT NULL AND ($E164 NOT NULL OR $EMAIL NOT NULL)" - const val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($E164 GLOB ? OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)" + val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($E164_SEARCH OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)" const val SIGNAL_CONTACT = "$REGISTERED = ? AND (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)" - const val QUERY_SIGNAL_CONTACT = "$SIGNAL_CONTACT AND ($E164 GLOB ? OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)" + val QUERY_SIGNAL_CONTACT = "$SIGNAL_CONTACT AND ($E164_SEARCH OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)" val GROUP_MEMBER_CONTACT = "$REGISTERED = ? AND $HAS_GROUP_IN_COMMON AND NOT (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)" - val QUERY_GROUP_MEMBER_CONTACT = "$GROUP_MEMBER_CONTACT AND ($E164 GLOB ? OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)" + val QUERY_GROUP_MEMBER_CONTACT = "$GROUP_MEMBER_CONTACT AND ($E164_SEARCH OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)" } } @@ -4500,6 +4513,19 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } } + enum class PhoneNumberSharingState(val id: Int) { + UNKNOWN(0), ENABLED(1), DISABLED(2); + + val enabled + get() = this == ENABLED || this == UNKNOWN + + companion object { + fun fromId(id: Int): PhoneNumberSharingState { + return values()[id] + } + } + } + data class CdsV2Result( val pni: PNI, val aci: ACI? diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt index f68ffe81a2..502d8d490b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt @@ -164,7 +164,8 @@ object RecipientTableCursorUtil { badges = parseBadgeList(cursor.requireBlob(RecipientTable.BADGES)), needsPniSignature = cursor.requireBoolean(RecipientTable.NEEDS_PNI_SIGNATURE), hiddenState = Recipient.HiddenState.deserialize(cursor.requireInt(RecipientTable.HIDDEN)), - callLinkRoomId = cursor.requireString(RecipientTable.CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) } + callLinkRoomId = cursor.requireString(RecipientTable.CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) }, + phoneNumberSharing = cursor.requireInt(RecipientTable.PHONE_NUMBER_SHARING).let { RecipientTable.PhoneNumberSharingState.fromId(it) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index d4fba06012..f732038a79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V210_FixPniPossible import org.thoughtcrime.securesms.database.helpers.migration.V211_ReceiptColumnRenames import org.thoughtcrime.securesms.database.helpers.migration.V212_RemoveDistributionListUniqueConstraint import org.thoughtcrime.securesms.database.helpers.migration.V213_FixUsernameInE164Column +import org.thoughtcrime.securesms.database.helpers.migration.V214_PhoneNumberSharingColumn /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -144,10 +145,11 @@ object SignalDatabaseMigrations { 210 to V210_FixPniPossibleColumns, 211 to V211_ReceiptColumnRenames, 212 to V212_RemoveDistributionListUniqueConstraint, - 213 to V213_FixUsernameInE164Column + 213 to V213_FixUsernameInE164Column, + 214 to V214_PhoneNumberSharingColumn ) - const val DATABASE_VERSION = 213 + const val DATABASE_VERSION = 214 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V214_PhoneNumberSharingColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V214_PhoneNumberSharingColumn.kt new file mode 100644 index 0000000000..fcc18257d5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V214_PhoneNumberSharingColumn.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Adds a phone_number_sharing column to the recipient table. + */ +@Suppress("ClassName") +object V214_PhoneNumberSharingColumn : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE recipient ADD COLUMN phone_number_sharing INTEGER DEFAULT 0") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index e279f3479b..7b4cae3c91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting +import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode import org.thoughtcrime.securesms.database.RecipientTable.VibrateState @@ -76,7 +77,8 @@ data class RecipientRecord( @get:JvmName("needsPniSignature") val needsPniSignature: Boolean, val hiddenState: Recipient.HiddenState, - val callLinkRoomId: CallLinkRoomId? + val callLinkRoomId: CallLinkRoomId?, + val phoneNumberSharing: PhoneNumberSharingState ) { fun e164Only(): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index bf531e4df1..691d3abc02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -148,6 +148,7 @@ public class RefreshOwnProfileJob extends BaseJob { setProfileCapabilities(profile.getCapabilities()); setProfileBadges(profile.getBadges()); ensureUnidentifiedAccessCorrect(profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess()); + ensurePhoneNumberSharingIsCorrect(profile.getPhoneNumberSharing()); profileAndCredential.getExpiringProfileKeyCredential() .ifPresent(expiringProfileKeyCredential -> setExpiringProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), expiringProfileKeyCredential)); @@ -256,6 +257,45 @@ public class RefreshOwnProfileJob extends BaseJob { } } + /** + * Checks to make sure that our phone number sharing setting matches what's on our profile. If there's a mismatch, we first sync with storage service + * (to limit race conditions between devices) and then upload our profile. + */ + private void ensurePhoneNumberSharingIsCorrect(@Nullable String phoneNumberSharingCiphertext) { + if (phoneNumberSharingCiphertext == null) { + Log.w(TAG, "No phone number sharing is set remotely! Syncing with storage service, then uploading our profile."); + syncWithStorageServiceThenUploadProfile(); + return; + } + + ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey(); + ProfileCipher cipher = new ProfileCipher(profileKey); + + try { + RecipientTable.PhoneNumberSharingState remotePhoneNumberSharing = cipher.decryptBoolean(Base64.decode(phoneNumberSharingCiphertext)) + .map(value -> value ? RecipientTable.PhoneNumberSharingState.ENABLED : RecipientTable.PhoneNumberSharingState.DISABLED) + .orElse(RecipientTable.PhoneNumberSharingState.UNKNOWN); + + if (remotePhoneNumberSharing == RecipientTable.PhoneNumberSharingState.UNKNOWN || remotePhoneNumberSharing.getEnabled() != SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled()) { + Log.w(TAG, "Phone number sharing setting did not match! Syncing with storage service, then uploading our profile."); + syncWithStorageServiceThenUploadProfile(); + } + } catch (IOException e) { + Log.w(TAG, "Failed to decode phone number sharing! Syncing with storage service, then uploading our profile.", e); + syncWithStorageServiceThenUploadProfile(); + } catch (InvalidCiphertextException e) { + Log.w(TAG, "Failed to decrypt phone number sharing! Syncing with storage service, then uploading our profile.", e); + syncWithStorageServiceThenUploadProfile(); + } + } + + private void syncWithStorageServiceThenUploadProfile() { + ApplicationDependencies.getJobManager() + .startChain(new StorageSyncJob()) + .then(new ProfileUploadJob()) + .enqueue(); + } + static void checkUsernameIsInSync() { boolean validated = false; 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 152c838146..fbd24d4b9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -21,6 +21,7 @@ import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.util.Pair; +import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.badges.Badges; @@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.RecipientTable; +import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState; import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.RecipientRecord; @@ -411,6 +413,14 @@ public class RetrieveProfileJob extends BaseJob { return true; } + PhoneNumberSharingState remotePhoneNumberSharing = ProfileUtil.decryptBoolean(profileKey, remoteProfile.getPhoneNumberSharing()) + .map(value -> value ? PhoneNumberSharingState.ENABLED : PhoneNumberSharingState.DISABLED) + .orElse(PhoneNumberSharingState.UNKNOWN); + + if (localRecipientRecord.getPhoneNumberSharing() != remotePhoneNumberSharing) { + return true; + } + return false; } @@ -425,6 +435,7 @@ public class RetrieveProfileJob extends BaseJob { setProfileBadges(recipient, profile.getBadges()); setProfileCapabilities(recipient, profile.getCapabilities()); setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess()); + setPhoneNumberSharingMode(recipient, profile.getPhoneNumberSharing()); if (recipientProfileKey != null) { profileAndCredential.getExpiringProfileKeyCredential() @@ -611,6 +622,26 @@ public class RetrieveProfileJob extends BaseJob { SignalDatabase.recipients().setCapabilities(recipient.getId(), capabilities); } + private void setPhoneNumberSharingMode(Recipient recipient, String phoneNumberSharing) { + ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); + if (profileKey == null) { + return; + } + + try { + PhoneNumberSharingState remotePhoneNumberSharing = ProfileUtil.decryptBoolean(profileKey, phoneNumberSharing) + .map(value -> value ? PhoneNumberSharingState.ENABLED : PhoneNumberSharingState.DISABLED) + .orElse(PhoneNumberSharingState.UNKNOWN); + + if (recipient.getPhoneNumberSharing() != remotePhoneNumberSharing) { + Log.i(TAG, "Updating phone number sharing state for " + recipient.getId() + " to " + remotePhoneNumberSharing); + SignalDatabase.recipients().setPhoneNumberSharing(recipient.getId(), remotePhoneNumberSharing); + } + } catch (InvalidCiphertextException | IOException e) { + Log.w(TAG, "Failed to set the phone number sharing setting!", e); + } + } + /** * Collective state as responses are processed as they come in. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanFindMeByPhoneNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanFindMeByPhoneNumberRepository.kt index c9867f8c80..f16fadcfac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanFindMeByPhoneNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/pnp/WhoCanFindMeByPhoneNumberRepository.kt @@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.profiles.edit.pnp import io.reactivex.rxjava3.core.Completable import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.ProfileUploadJob import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.storage.StorageSyncHelper /** * Manages the current phone-number listing state. @@ -31,6 +33,8 @@ class WhoCanFindMeByPhoneNumberRepository { } ApplicationDependencies.getJobManager().add(RefreshAttributesJob()) + StorageSyncHelper.scheduleSyncForDataChange() + ApplicationDependencies.getJobManager().add(ProfileUploadJob()) } } } 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 c021f3260c..70937099cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting; +import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState; import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState; import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.RecipientTable.VibrateState; @@ -137,6 +138,7 @@ public class Recipient { private final boolean needsPniSignature; private final CallLinkRoomId callLinkRoomId; private final Optional groupRecord; + private final PhoneNumberSharingState phoneNumberSharing; /** * Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be @@ -426,6 +428,7 @@ public class Recipient { this.isActiveGroup = false; this.callLinkRoomId = null; this.groupRecord = Optional.empty(); + this.phoneNumberSharing = PhoneNumberSharingState.UNKNOWN; } public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { @@ -481,6 +484,7 @@ public class Recipient { this.isActiveGroup = details.isActiveGroup; this.callLinkRoomId = details.callLinkRoomId; this.groupRecord = details.groupRecord; + this.phoneNumberSharing = details.phoneNumberSharing; } public @NonNull RecipientId getId() { @@ -679,6 +683,13 @@ public class Recipient { return Optional.ofNullable(e164); } + /** + * Whether or not we should show this user's e164 in the interface. + */ + public boolean shouldShowE164() { + return hasE164() && (isSystemContact() || getPhoneNumberSharing() != PhoneNumberSharingState.DISABLED); + } + public @NonNull Optional getEmail() { return Optional.ofNullable(email); } @@ -1230,6 +1241,10 @@ public class Recipient { return Objects.requireNonNull(callLinkRoomId); } + public PhoneNumberSharingState getPhoneNumberSharing() { + return phoneNumberSharing; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -1386,7 +1401,8 @@ public class Recipient { hasGroupsInCommon == other.hasGroupsInCommon && Objects.equals(badges, other.badges) && isActiveGroup == other.isActiveGroup && - Objects.equals(callLinkRoomId, other.callLinkRoomId); + Objects.equals(callLinkRoomId, other.callLinkRoomId) && + phoneNumberSharing == other.phoneNumberSharing; } private static boolean allContentsAreTheSame(@NonNull List a, @NonNull List b) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.kt index 039a07ffd0..4244012a2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.kt @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting +import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode import org.thoughtcrime.securesms.database.RecipientTable.VibrateState @@ -79,7 +80,8 @@ class RecipientDetails private constructor( @JvmField val isReleaseChannel: Boolean, @JvmField val needsPniSignature: Boolean, @JvmField val callLinkRoomId: CallLinkRoomId?, - @JvmField val groupRecord: Optional + @JvmField val groupRecord: Optional, + @JvmField val phoneNumberSharing: PhoneNumberSharingState ) { @VisibleForTesting @@ -143,7 +145,8 @@ class RecipientDetails private constructor( isReleaseChannel = isReleaseChannel, needsPniSignature = record.needsPniSignature, callLinkRoomId = record.callLinkRoomId, - groupRecord = groupRecord + groupRecord = groupRecord, + phoneNumberSharing = record.phoneNumberSharing ) companion object { @@ -271,7 +274,8 @@ class RecipientDetails private constructor( needsPniSignature = false, isActiveGroup = false, callLinkRoomId = null, - groupRecord = Optional.empty() + groupRecord = Optional.empty(), + phoneNumberSharing = PhoneNumberSharingState.UNKNOWN ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java index 63935e5915..8a935504bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java @@ -34,7 +34,7 @@ public final class RecipientExporter { } private static void addAddressToIntent(Intent intent, Recipient recipient) { - if (recipient.getE164().isPresent()) { + if (recipient.getE164().isPresent() && recipient.shouldShowE164()) { intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipient.requireE164()); } else if (recipient.getEmail().isPresent()) { intent.putExtra(ContactsContract.Intents.Insert.EMAIL, recipient.requireEmail()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt index 87b4621389..5b88c608a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt @@ -154,7 +154,7 @@ private fun AboutSheetContent( modifier = Modifier.fillMaxWidth() ) - if (recipient.about != null) { + if (!recipient.about.isNullOrBlank()) { AboutRow( startIcon = painterResource(R.drawable.symbol_edit_24), text = { @@ -190,7 +190,7 @@ private fun AboutSheetContent( ) } - if (recipient.e164.isPresent) { + if (recipient.e164.isPresent && recipient.shouldShowE164()) { val e164 = remember(recipient.e164.get()) { PhoneNumberFormatter.get(context).prettyPrintFormat(recipient.e164.get()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index 788e28500a..277816dc8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -213,7 +213,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF about.setVisibility(View.GONE); } - String usernameNumberString = recipient.hasAUserSetDisplayName(requireContext()) && !recipient.isSelf() + String usernameNumberString = recipient.hasAUserSetDisplayName(requireContext()) && !recipient.isSelf() && recipient.shouldShowE164() ? recipient.getSmsAddress().map(PhoneNumberFormatter::prettyPrint).orElse("").trim() : ""; usernameNumber.setText(usernameNumberString); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 32f9a7f395..88cd485c52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -150,6 +150,17 @@ public final class ProfileUtil { return decryptString(profileKey, Base64.decode(encryptedStringBase64)); } + public static Optional decryptBoolean(@NonNull ProfileKey profileKey, @Nullable String encryptedBooleanBase64) + throws InvalidCiphertextException, IOException + { + if (encryptedBooleanBase64 == null) { + return Optional.empty(); + } + + ProfileCipher profileCipher = new ProfileCipher(profileKey); + return profileCipher.decryptBoolean(Base64.decode(encryptedBooleanBase64)); + } + @WorkerThread public static @NonNull MobileCoinPublicAddress getAddressForRecipient(@NonNull Recipient recipient) throws IOException, PaymentsAddressException @@ -347,7 +358,8 @@ public final class ProfileUtil { aboutEmoji, Optional.ofNullable(paymentsAddress), avatar, - badgeIds).orElse(null); + badgeIds, + SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled()).orElse(null); SignalStore.registrationValues().markHasUploadedProfile(); if (!avatar.keepTheSame) { SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath); diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 9ddf2d377f..b34dd5f3cf 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -146,7 +146,8 @@ object RecipientDatabaseTestUtils { badges = badges, needsPniSignature = false, hiddenState = Recipient.HiddenState.NOT_HIDDEN, - callLinkRoomId = null + callLinkRoomId = null, + phoneNumberSharing = RecipientTable.PhoneNumberSharingState.UNKNOWN ), participantIds = participants, isReleaseChannel = isReleaseChannel, diff --git a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java index 88807d652f..dad926bb1b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java @@ -37,6 +37,19 @@ public final class RecipientExporterTest { assertNull(intent.getStringExtra(EMAIL)); } + @Test + public void asAddContactIntent_with_phone_number_should_not_show_number() { + Recipient recipient = givenPhoneRecipient(ProfileName.fromParts("Alice", null), "+1555123456", false); + + Intent intent = RecipientExporter.export(recipient).asAddContactIntent(); + + assertEquals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction()); + assertEquals(ContactsContract.Contacts.CONTENT_ITEM_TYPE, intent.getType()); + assertEquals("Alice", intent.getStringExtra(NAME)); + assertNull(intent.getStringExtra(PHONE)); + assertNull(intent.getStringExtra(EMAIL)); + } + @Test public void asAddContactIntent_with_email() { Recipient recipient = givenEmailRecipient(ProfileName.fromParts("Bob", null), "bob@signal.org"); @@ -50,13 +63,19 @@ public final class RecipientExporterTest { assertNull(intent.getStringExtra(PHONE)); } + private Recipient givenPhoneRecipient(ProfileName profileName, String phone) { + return givenPhoneRecipient(profileName, phone, true); + } + + private Recipient givenPhoneRecipient(ProfileName profileName, String phone, boolean shouldShowPhoneNumber) { Recipient recipient = mock(Recipient.class); when(recipient.getProfileName()).thenReturn(profileName); when(recipient.requireE164()).thenReturn(phone); when(recipient.getE164()).thenAnswer(i -> Optional.of(phone)); when(recipient.getEmail()).thenAnswer(i -> Optional.empty()); + when(recipient.shouldShowE164()).thenAnswer(i -> shouldShowPhoneNumber); return recipient; } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index f840a6657a..b06ae36eb0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -722,17 +722,19 @@ public class SignalServiceAccountManager { String aboutEmoji, Optional paymentsAddress, AvatarUploadParams avatar, - List visibleBadgeIds) + List visibleBadgeIds, + boolean phoneNumberSharing) throws IOException { if (name == null) name = ""; - ProfileCipher profileCipher = new ProfileCipher(profileKey); - byte[] ciphertextName = profileCipher.encryptString(name, ProfileCipher.getTargetNameLength(name)); - byte[] ciphertextAbout = profileCipher.encryptString(about, ProfileCipher.getTargetAboutLength(about)); - byte[] ciphertextEmoji = profileCipher.encryptString(aboutEmoji, ProfileCipher.EMOJI_PADDED_LENGTH); - byte[] ciphertextMobileCoinAddress = paymentsAddress.map(address -> profileCipher.encryptWithLength(address.encode(), ProfileCipher.PAYMENTS_ADDRESS_CONTENT_SIZE)).orElse(null); - ProfileAvatarData profileAvatarData = null; + ProfileCipher profileCipher = new ProfileCipher(profileKey); + byte[] ciphertextName = profileCipher.encryptString(name, ProfileCipher.getTargetNameLength(name)); + byte[] ciphertextAbout = profileCipher.encryptString(about, ProfileCipher.getTargetAboutLength(about)); + byte[] ciphertextEmoji = profileCipher.encryptString(aboutEmoji, ProfileCipher.EMOJI_PADDED_LENGTH); + byte[] ciphertextMobileCoinAddress = paymentsAddress.map(address -> profileCipher.encryptWithLength(address.encode(), ProfileCipher.PAYMENTS_ADDRESS_CONTENT_SIZE)).orElse(null); + byte[] cipherTextPhoneNumberSharing = profileCipher.encryptBoolean(phoneNumberSharing); + ProfileAvatarData profileAvatarData = null; if (avatar.stream != null && !avatar.keepTheSame) { profileAvatarData = new ProfileAvatarData(avatar.stream.getStream(), @@ -746,6 +748,7 @@ public class SignalServiceAccountManager { ciphertextAbout, ciphertextEmoji, ciphertextMobileCoinAddress, + cipherTextPhoneNumberSharing, avatar.hasAvatar, avatar.keepTheSame, profileKey.getCommitment(aci.getLibSignalAci()).serialize(), diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java index fd318416c2..6e07ff3508 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java @@ -14,7 +14,9 @@ import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; +import java.util.Optional; +import javax.annotation.Nullable; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -127,6 +129,22 @@ public class ProfileCipher { return new String(plaintext); } + public byte[] encryptBoolean(boolean input) { + byte[] value = new byte[1]; + value[0] = (byte) (input ? 1 : 0); + + return encrypt(value, value.length); + } + + public Optional decryptBoolean(@Nullable byte[] input) throws InvalidCiphertextException { + if (input == null) { + return Optional.empty(); + } + + byte[] paddedPlaintext = decrypt(input); + return Optional.of(paddedPlaintext[0] != 0); + } + /** * Encodes the length, and adds padding. *

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 71d2643416..f8310b7ba8 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 @@ -63,6 +63,9 @@ public class SignalServiceProfile { @JsonProperty private List badges; + @JsonProperty + private String phoneNumberSharing; + @JsonIgnore private RequestType requestType; @@ -96,6 +99,10 @@ public class SignalServiceProfile { return unidentifiedAccess; } + public String getPhoneNumberSharing() { + return phoneNumberSharing; + } + public boolean isUnrestrictedUnidentifiedAccess() { return unrestrictedUnidentifiedAccess; } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java index 8abf5bdf1e..227b42452a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java @@ -22,6 +22,9 @@ public class SignalServiceProfileWrite { @JsonProperty private byte[] paymentAddress; + @JsonProperty + private byte[] phoneNumberSharing; + @JsonProperty private boolean avatar; @@ -38,16 +41,17 @@ public class SignalServiceProfileWrite { public SignalServiceProfileWrite(){ } - public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, byte[] paymentAddress, boolean hasAvatar, boolean sameAvatar, byte[] commitment, List badgeIds) { - this.version = version; - this.name = name; - this.about = about; - this.aboutEmoji = aboutEmoji; - this.paymentAddress = paymentAddress; - this.avatar = hasAvatar; - this.sameAvatar = sameAvatar; - this.commitment = commitment; - this.badgeIds = badgeIds; + public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, byte[] paymentAddress, byte[] phoneNumberSharing, boolean hasAvatar, boolean sameAvatar, byte[] commitment, List badgeIds) { + this.version = version; + this.name = name; + this.about = about; + this.aboutEmoji = aboutEmoji; + this.paymentAddress = paymentAddress; + this.phoneNumberSharing = phoneNumberSharing; + this.avatar = hasAvatar; + this.sameAvatar = sameAvatar; + this.commitment = commitment; + this.badgeIds = badgeIds; } public boolean hasAvatar() {