Respect the phoneNumberSharing setting on the profile.

This commit is contained in:
Greyson Parrelli 2023-12-20 11:48:02 -05:00 committed by Clark Chen
parent 624f863da4
commit bb30535afb
24 changed files with 257 additions and 44 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
*/

View file

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

View file

@ -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> 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<String> 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<Recipient> a, @NonNull List<Recipient> b) {

View file

@ -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<GroupRecord>
@JvmField val groupRecord: Optional<GroupRecord>,
@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
)
}
}

View file

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

View file

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

View file

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

View file

@ -150,6 +150,17 @@ public final class ProfileUtil {
return decryptString(profileKey, Base64.decode(encryptedStringBase64));
}
public static Optional<Boolean> 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);

View file

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

View file

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

View file

@ -722,17 +722,19 @@ public class SignalServiceAccountManager {
String aboutEmoji,
Optional<PaymentAddress> paymentsAddress,
AvatarUploadParams avatar,
List<String> visibleBadgeIds)
List<String> 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(),

View file

@ -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<Boolean> 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.
* <p>

View file

@ -63,6 +63,9 @@ public class SignalServiceProfile {
@JsonProperty
private List<Badge> 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;
}

View file

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