mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add GV2 accept by PNI invite.
This commit is contained in:
@@ -77,6 +77,9 @@ object ContactDiscoveryRefreshV2 {
|
||||
)
|
||||
stopwatch.split("recipient-db")
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList())
|
||||
stopwatch.split("update-registered")
|
||||
|
||||
stopwatch.stop(TAG)
|
||||
|
||||
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
||||
@@ -127,6 +130,9 @@ object ContactDiscoveryRefreshV2 {
|
||||
)
|
||||
stopwatch.split("recipient-db")
|
||||
|
||||
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList())
|
||||
stopwatch.split("update-registered")
|
||||
|
||||
stopwatch.stop(TAG)
|
||||
|
||||
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
||||
|
||||
@@ -311,6 +311,7 @@ import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -1267,7 +1268,7 @@ public class ConversationParentFragment extends Fragment
|
||||
AttachmentManager.selectLocation(this, PICK_LOCATION, getSendButtonColor(sendButton.getSelectedSendType()));
|
||||
break;
|
||||
case PAYMENT:
|
||||
if (recipient.get().hasProfileKeyCredential()) {
|
||||
if (ExpiringProfileCredentialUtil.isValid(recipient.get().getExpiringProfileKeyCredential())) {
|
||||
AttachmentManager.selectPayment(this, recipient.getId());
|
||||
} else {
|
||||
CanNotSendPaymentDialog.show(requireActivity());
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.annotation.Nullable;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -63,18 +62,6 @@ public final class ProfileKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable ProfileKeyCredential profileKeyCredentialOrNull(@Nullable byte[] profileKeyCredential) {
|
||||
if (profileKeyCredential != null) {
|
||||
try {
|
||||
return new ProfileKeyCredential(profileKeyCredential);
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, String.format(Locale.US, "Seen non-null profile key credential of wrong length %d", profileKeyCredential.length), e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) {
|
||||
try {
|
||||
return new ProfileKey(profileKey);
|
||||
|
||||
@@ -83,6 +83,9 @@ public class GroupDatabase extends Database {
|
||||
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
|
||||
private static final String DISTRIBUTION_ID = "distribution_id";
|
||||
private static final String DISPLAY_AS_STORY = "display_as_story";
|
||||
|
||||
/** Was temporarily used for PNP accept by pni but is no longer needed/updated */
|
||||
@Deprecated
|
||||
private static final String AUTH_SERVICE_ID = "auth_service_id";
|
||||
|
||||
|
||||
@@ -125,7 +128,7 @@ public class GroupDatabase extends Database {
|
||||
|
||||
private static final String[] GROUP_PROJECTION = {
|
||||
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
|
||||
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, AUTH_SERVICE_ID
|
||||
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
|
||||
};
|
||||
|
||||
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
|
||||
@@ -477,25 +480,23 @@ public class GroupDatabase extends Database {
|
||||
if (groupExists(groupId.deriveV2MigrationGroupId())) {
|
||||
throw new LegacyGroupInsertException(groupId);
|
||||
}
|
||||
create(null, groupId, title, members, avatar, relay, null, null);
|
||||
create(groupId, title, members, avatar, relay, null, null);
|
||||
}
|
||||
|
||||
public void create(@NonNull GroupId.Mms groupId,
|
||||
@Nullable String title,
|
||||
@NonNull Collection<RecipientId> members)
|
||||
{
|
||||
create(null, groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null);
|
||||
create(groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null);
|
||||
}
|
||||
|
||||
public GroupId.V2 create(@Nullable ServiceId authServiceId,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull DecryptedGroup groupState)
|
||||
{
|
||||
return create(authServiceId, groupMasterKey, groupState, false);
|
||||
return create(groupMasterKey, groupState, false);
|
||||
}
|
||||
|
||||
public GroupId.V2 create(@Nullable ServiceId authServiceId,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull DecryptedGroup groupState,
|
||||
boolean force)
|
||||
{
|
||||
@@ -507,7 +508,7 @@ public class GroupDatabase extends Database {
|
||||
Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!");
|
||||
}
|
||||
|
||||
create(authServiceId, groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
|
||||
create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState);
|
||||
|
||||
return groupId;
|
||||
}
|
||||
@@ -537,8 +538,8 @@ public class GroupDatabase extends Database {
|
||||
|
||||
if (updated < 1) {
|
||||
Log.w(TAG, "No group entry. Creating restore placeholder for " + groupId);
|
||||
create(authServiceId,
|
||||
groupMasterKey,
|
||||
create(
|
||||
groupMasterKey,
|
||||
DecryptedGroup.newBuilder()
|
||||
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
|
||||
.build(),
|
||||
@@ -559,8 +560,7 @@ public class GroupDatabase extends Database {
|
||||
/**
|
||||
* @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version).
|
||||
*/
|
||||
private void create(@Nullable ServiceId authServiceId,
|
||||
@NonNull GroupId groupId,
|
||||
private void create(@NonNull GroupId groupId,
|
||||
@Nullable String title,
|
||||
@NonNull Collection<RecipientId> memberCollection,
|
||||
@Nullable SignalServiceAttachmentPointer avatar,
|
||||
@@ -575,7 +575,6 @@ public class GroupDatabase extends Database {
|
||||
Collections.sort(members);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(AUTH_SERVICE_ID, authServiceId != null ? authServiceId.toString() : null);
|
||||
contentValues.put(RECIPIENT_ID, groupRecipientId.serialize());
|
||||
contentValues.put(GROUP_ID, groupId.toString());
|
||||
contentValues.put(TITLE, title);
|
||||
@@ -987,13 +986,6 @@ public class GroupDatabase extends Database {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setAuthServiceId(@Nullable ServiceId authServiceId, @NonNull GroupId groupId) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(AUTH_SERVICE_ID, authServiceId == null ? null : authServiceId.toString());
|
||||
|
||||
getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId));
|
||||
}
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
|
||||
public final Cursor cursor;
|
||||
@@ -1038,8 +1030,7 @@ public class GroupDatabase extends Database {
|
||||
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
|
||||
CursorUtil.requireInt(cursor, V2_REVISION),
|
||||
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
|
||||
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null),
|
||||
CursorUtil.requireString(cursor, AUTH_SERVICE_ID));
|
||||
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1065,7 +1056,6 @@ public class GroupDatabase extends Database {
|
||||
private final boolean mms;
|
||||
@Nullable private final V2GroupProperties v2GroupProperties;
|
||||
private final DistributionId distributionId;
|
||||
@Nullable private final String authServiceId;
|
||||
|
||||
public GroupRecord(@NonNull GroupId id,
|
||||
@NonNull RecipientId recipientId,
|
||||
@@ -1082,8 +1072,7 @@ public class GroupDatabase extends Database {
|
||||
@Nullable byte[] groupMasterKeyBytes,
|
||||
int groupRevision,
|
||||
@Nullable byte[] decryptedGroupBytes,
|
||||
@Nullable DistributionId distributionId,
|
||||
@Nullable String authServiceId)
|
||||
@Nullable DistributionId distributionId)
|
||||
{
|
||||
this.id = id;
|
||||
this.recipientId = recipientId;
|
||||
@@ -1096,7 +1085,6 @@ public class GroupDatabase extends Database {
|
||||
this.active = active;
|
||||
this.mms = mms;
|
||||
this.distributionId = distributionId;
|
||||
this.authServiceId = authServiceId;
|
||||
|
||||
V2GroupProperties v2GroupProperties = null;
|
||||
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
|
||||
@@ -1285,10 +1273,6 @@ public class GroupDatabase extends Database {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @Nullable ServiceId getAuthServiceId() {
|
||||
return ServiceId.parseOrNull(authServiceId);
|
||||
}
|
||||
}
|
||||
|
||||
public static class V2GroupProperties {
|
||||
|
||||
@@ -33,8 +33,8 @@ import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.InvalidKeyException
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.toDatabaseBadge
|
||||
@@ -66,7 +66,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ExpiringProfileKeyCredentialColumnData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -154,7 +154,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
private const val SYSTEM_CONTACT_URI = "system_contact_uri"
|
||||
private const val SYSTEM_INFO_PENDING = "system_info_pending"
|
||||
private const val PROFILE_KEY = "profile_key"
|
||||
private const val PROFILE_KEY_CREDENTIAL = "profile_key_credential"
|
||||
const val EXPIRING_PROFILE_KEY_CREDENTIAL = "profile_key_credential"
|
||||
private const val SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"
|
||||
const val PROFILE_SHARING = "profile_sharing"
|
||||
private const val LAST_PROFILE_FETCH = "last_profile_fetch"
|
||||
@@ -214,7 +214,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
$SYSTEM_CONTACT_URI TEXT DEFAULT NULL,
|
||||
$SYSTEM_INFO_PENDING INTEGER DEFAULT 0,
|
||||
$PROFILE_KEY TEXT DEFAULT NULL,
|
||||
$PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL,
|
||||
$EXPIRING_PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL,
|
||||
$PROFILE_GIVEN_NAME TEXT DEFAULT NULL,
|
||||
$PROFILE_FAMILY_NAME TEXT DEFAULT NULL,
|
||||
$PROFILE_JOINED_NAME TEXT DEFAULT NULL,
|
||||
@@ -269,7 +269,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
MESSAGE_EXPIRATION_TIME,
|
||||
REGISTERED,
|
||||
PROFILE_KEY,
|
||||
PROFILE_KEY_CREDENTIAL,
|
||||
EXPIRING_PROFILE_KEY_CREDENTIAL,
|
||||
SYSTEM_JOINED_NAME,
|
||||
SYSTEM_GIVEN_NAME,
|
||||
SYSTEM_FAMILY_NAME,
|
||||
@@ -925,7 +925,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
val recipientId = getByStorageKeyOrThrow(update.new.id.raw)
|
||||
if (StorageSyncHelper.profileKeyChanged(update)) {
|
||||
val clearValues = ContentValues(1).apply {
|
||||
putNull(PROFILE_KEY_CREDENTIAL)
|
||||
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
|
||||
}
|
||||
db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId))
|
||||
}
|
||||
@@ -986,7 +986,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
|
||||
Log.i(TAG, "Creating restore placeholder for $groupId")
|
||||
groups.create(
|
||||
null,
|
||||
masterKey,
|
||||
DecryptedGroup.newBuilder()
|
||||
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
|
||||
@@ -1560,7 +1559,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
val valuesToSet = ContentValues(3).apply {
|
||||
put(PROFILE_KEY, encodedProfileKey)
|
||||
putNull(PROFILE_KEY_CREDENTIAL)
|
||||
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
|
||||
put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode)
|
||||
}
|
||||
|
||||
@@ -1592,7 +1591,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
val args = arrayOf(id.serialize())
|
||||
val valuesToSet = ContentValues(3).apply {
|
||||
put(PROFILE_KEY, Base64.encodeBytes(profileKey.serialize()))
|
||||
putNull(PROFILE_KEY_CREDENTIAL)
|
||||
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
|
||||
put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode)
|
||||
}
|
||||
|
||||
@@ -1611,16 +1610,16 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
fun setProfileKeyCredential(
|
||||
id: RecipientId,
|
||||
profileKey: ProfileKey,
|
||||
profileKeyCredential: ProfileKeyCredential
|
||||
expiringProfileKeyCredential: ExpiringProfileKeyCredential
|
||||
): Boolean {
|
||||
val selection = "$ID = ? AND $PROFILE_KEY = ?"
|
||||
val args = arrayOf(id.serialize(), Base64.encodeBytes(profileKey.serialize()))
|
||||
val columnData = ProfileKeyCredentialColumnData.newBuilder()
|
||||
val columnData = ExpiringProfileKeyCredentialColumnData.newBuilder()
|
||||
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
|
||||
.setProfileKeyCredential(ByteString.copyFrom(profileKeyCredential.serialize()))
|
||||
.setExpiringProfileKeyCredential(ByteString.copyFrom(expiringProfileKeyCredential.serialize()))
|
||||
.build()
|
||||
val values = ContentValues(1).apply {
|
||||
put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray()))
|
||||
put(EXPIRING_PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray()))
|
||||
}
|
||||
val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values)
|
||||
|
||||
@@ -1634,11 +1633,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
|
||||
fun clearProfileKeyCredential(id: RecipientId) {
|
||||
val values = ContentValues(1)
|
||||
values.putNull(PROFILE_KEY_CREDENTIAL)
|
||||
values.putNull(PROFILE_KEY)
|
||||
values.put(PROFILE_SHARING, 0)
|
||||
values.putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
|
||||
if (update(id, values)) {
|
||||
rotateStorageId(id)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
|
||||
}
|
||||
}
|
||||
@@ -2101,11 +2097,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
|
||||
db.beginTransaction()
|
||||
try {
|
||||
for ((recipientId, aci) in registered) {
|
||||
for ((recipientId, serviceId) in registered) {
|
||||
val values = ContentValues(2).apply {
|
||||
put(REGISTERED, RegisteredState.REGISTERED.id)
|
||||
if (aci != null) {
|
||||
put(SERVICE_ID, aci.toString().lowercase())
|
||||
if (serviceId != null) {
|
||||
put(SERVICE_ID, serviceId.toString().lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2117,7 +2113,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
} catch (e: SQLiteConstraintException) {
|
||||
Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update $recipientId. Possibly merging.")
|
||||
val e164 = getRecord(recipientId).e164
|
||||
val newId = getAndPossiblyMerge(aci, e164)
|
||||
val newId = getAndPossiblyMerge(serviceId, e164)
|
||||
Log.w(TAG, "[bulkUpdateRegisteredStatus] Merged into $newId")
|
||||
}
|
||||
}
|
||||
@@ -2173,7 +2169,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes CDSv2 results, merging recipients as necessary.
|
||||
* Processes CDSv2 results, merging recipients as necessary. Does not mark users as
|
||||
* registered.
|
||||
*
|
||||
* Important: This is under active development and is not suitable for actual use.
|
||||
*
|
||||
@@ -3489,6 +3486,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
.run()
|
||||
|
||||
ApplicationDependencies.getRecipientCache().clear()
|
||||
RecipientId.clearCache()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3499,7 +3499,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
PROFILE_KEY to null,
|
||||
PROFILE_KEY_CREDENTIAL to null,
|
||||
EXPIRING_PROFILE_KEY_CREDENTIAL to null,
|
||||
PROFILE_GIVEN_NAME to null,
|
||||
PROFILE_FAMILY_NAME to null,
|
||||
PROFILE_JOINED_NAME to null,
|
||||
@@ -3514,6 +3514,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
.run()
|
||||
|
||||
ApplicationDependencies.getRecipientCache().clear()
|
||||
RecipientId.clearCache()
|
||||
}
|
||||
|
||||
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
|
||||
@@ -3522,9 +3525,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
|
||||
fun getRecord(context: Context, cursor: Cursor, idColumnName: String): RecipientRecord {
|
||||
val profileKeyString = cursor.requireString(PROFILE_KEY)
|
||||
val profileKeyCredentialString = cursor.requireString(PROFILE_KEY_CREDENTIAL)
|
||||
val expiringProfileKeyCredentialString = cursor.requireString(EXPIRING_PROFILE_KEY_CREDENTIAL)
|
||||
var profileKey: ByteArray? = null
|
||||
var profileKeyCredential: ProfileKeyCredential? = null
|
||||
var expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null
|
||||
|
||||
if (profileKeyString != null) {
|
||||
try {
|
||||
@@ -3533,12 +3536,12 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
|
||||
if (profileKeyCredentialString != null) {
|
||||
if (expiringProfileKeyCredentialString != null) {
|
||||
try {
|
||||
val columnDataBytes = Base64.decode(profileKeyCredentialString)
|
||||
val columnData = ProfileKeyCredentialColumnData.parseFrom(columnDataBytes)
|
||||
val columnDataBytes = Base64.decode(expiringProfileKeyCredentialString)
|
||||
val columnData = ExpiringProfileKeyCredentialColumnData.parseFrom(columnDataBytes)
|
||||
if (Arrays.equals(columnData.profileKey.toByteArray(), profileKey)) {
|
||||
profileKeyCredential = ProfileKeyCredential(columnData.profileKeyCredential.toByteArray())
|
||||
expiringProfileKeyCredential = ExpiringProfileKeyCredential(columnData.expiringProfileKeyCredential.toByteArray())
|
||||
} else {
|
||||
Log.i(TAG, "Out of date profile key credential data ignored on read")
|
||||
}
|
||||
@@ -3598,7 +3601,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
expireMessages = cursor.requireInt(MESSAGE_EXPIRATION_TIME),
|
||||
registered = RegisteredState.fromId(cursor.requireInt(REGISTERED)),
|
||||
profileKey = profileKey,
|
||||
profileKeyCredential = profileKeyCredential,
|
||||
expiringProfileKeyCredential = expiringProfileKeyCredential,
|
||||
systemProfileName = ProfileName.fromParts(cursor.requireString(SYSTEM_GIVEN_NAME), cursor.requireString(SYSTEM_FAMILY_NAME)),
|
||||
systemDisplayName = cursor.requireString(SYSTEM_JOINED_NAME),
|
||||
systemContactPhotoUri = cursor.requireString(SYSTEM_PHOTO_URI),
|
||||
@@ -3688,7 +3691,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
private fun updateProfileValuesForMerge(values: ContentValues, record: RecipientRecord) {
|
||||
values.apply {
|
||||
put(PROFILE_KEY, if (record.profileKey != null) Base64.encodeBytes(record.profileKey) else null)
|
||||
putNull(PROFILE_KEY_CREDENTIAL)
|
||||
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
|
||||
put(SIGNAL_PROFILE_AVATAR, record.signalProfileAvatar)
|
||||
put(PROFILE_GIVEN_NAME, record.signalProfileName.givenName)
|
||||
put(PROFILE_FAMILY_NAME, record.signalProfileName.familyName)
|
||||
|
||||
@@ -202,8 +202,9 @@ object SignalDatabaseMigrations {
|
||||
private const val REMOTE_MEGAPHONE = 146
|
||||
private const val QUOTE_INDEX = 147
|
||||
private const val MY_STORY_PRIVACY_MODE = 148
|
||||
private const val EXPIRING_PROFILE_CREDENTIALS = 149
|
||||
|
||||
const val DATABASE_VERSION = 148
|
||||
const val DATABASE_VERSION = 149
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2658,6 +2659,10 @@ object SignalDatabaseMigrations {
|
||||
|
||||
db.execSQL("CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)")
|
||||
}
|
||||
|
||||
if (oldVersion < EXPIRING_PROFILE_CREDENTIALS) {
|
||||
db.execSQL("UPDATE recipient SET profile_key_credential = NULL")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -28,13 +28,13 @@ import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
|
||||
import org.signal.storageservice.protos.groups.local.EnabledState;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
|
||||
import org.thoughtcrime.securesms.keyvalue.ServiceIds;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -91,8 +91,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
|
||||
if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getAci().uuid()).isPresent() ||
|
||||
(selfIds.getPni() != null && DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getPni().uuid()).isPresent())
|
||||
) {
|
||||
(selfIds.getPni() != null && DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getPni().uuid()).isPresent()))
|
||||
{
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16);
|
||||
} else {
|
||||
return updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16);
|
||||
@@ -124,6 +124,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeUnknownEditorRequestingMembersApprovals(change, updates);
|
||||
describeUnknownEditorRequestingMembersDeletes(change, updates);
|
||||
describeUnknownEditorAnnouncementGroupChange(change, updates);
|
||||
describeUnknownEditorPromotePendingPniAci(change, updates);
|
||||
|
||||
describeUnknownEditorMemberRemovals(change, updates);
|
||||
|
||||
@@ -149,6 +150,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
describeRequestingMembersApprovals(change, updates);
|
||||
describeRequestingMembersDeletes(change, updates);
|
||||
describeAnnouncementGroupChange(change, updates);
|
||||
describePromotePendingPniAci(change, updates);
|
||||
|
||||
describeMemberRemovals(change, updates);
|
||||
|
||||
@@ -304,8 +306,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
|
||||
private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
int notYouInviteCount = 0;
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
int notYouInviteCount = 0;
|
||||
|
||||
for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) {
|
||||
boolean newMemberIsYou = selfIds.matches(invitee.getUuid());
|
||||
@@ -351,8 +353,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
|
||||
private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
int notDeclineCount = 0;
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
int notDeclineCount = 0;
|
||||
|
||||
for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) {
|
||||
boolean decline = invitee.getUuid().equals(change.getEditor());
|
||||
@@ -400,8 +402,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = selfIds.matches(uuid);
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = selfIds.matches(uuid);
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
@@ -425,8 +427,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedMember newMember : change.getPromotePendingMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = selfIds.matches(uuid);
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = selfIds.matches(uuid);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16));
|
||||
@@ -679,7 +681,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_approved_your_request_to_join_the_group, change.getEditor(), R.drawable.ic_update_group_accept_16));
|
||||
} else {
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requestingMember.getUuid(), R.drawable.ic_update_group_accept_16));
|
||||
@@ -770,6 +772,46 @@ final class GroupsV2UpdateMessageProducer {
|
||||
}
|
||||
}
|
||||
|
||||
private void describePromotePendingPniAci(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = selfIds.matches(change.getEditor());
|
||||
|
||||
for (DecryptedMember newMember : change.getPromotePendingPniAciMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = selfIds.matches(uuid);
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite), R.drawable.ic_update_group_accept_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_you_added_invited_member_s, uuid, R.drawable.ic_update_group_add_16));
|
||||
}
|
||||
} else {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_added_you, change.getEditor(), R.drawable.ic_update_group_add_16));
|
||||
} else {
|
||||
if (uuid.equals(change.getEditor())) {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_accepted_invite, uuid, R.drawable.ic_update_group_accept_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_added_invited_member_s, change.getEditor(), uuid, R.drawable.ic_update_group_add_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorPromotePendingPniAci(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
for (DecryptedMember newMember : change.getPromotePendingPniAciMembersList()) {
|
||||
ByteString uuid = newMember.getUuid();
|
||||
boolean newMemberIsYou = selfIds.matches(uuid);
|
||||
|
||||
if (newMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, uuid, R.drawable.ic_update_group_add_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UpdateDescription updateDescription(@NonNull String string, @DrawableRes int iconResource) {
|
||||
return UpdateDescription.staticDescription(string, iconResource);
|
||||
}
|
||||
@@ -868,8 +910,8 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
@VisibleForTesting
|
||||
static @NonNull Spannable makeRecipientsClickable(@NonNull Context context, @NonNull String template, @NonNull List<RecipientId> recipientIds, @Nullable Consumer<RecipientId> clickHandler) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
int startIndex = 0;
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
int startIndex = 0;
|
||||
|
||||
Map<String, RecipientId> idByPlaceholder = new HashMap<>();
|
||||
for (RecipientId id : recipientIds) {
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import android.net.Uri
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
@@ -45,7 +45,7 @@ data class RecipientRecord(
|
||||
val expireMessages: Int,
|
||||
val registered: RegisteredState,
|
||||
val profileKey: ByteArray?,
|
||||
val profileKeyCredential: ProfileKeyCredential?,
|
||||
val expiringProfileKeyCredential: ExpiringProfileKeyCredential?,
|
||||
val systemProfileName: ProfileName,
|
||||
val systemDisplayName: String?,
|
||||
val systemContactPhotoUri: String?,
|
||||
|
||||
@@ -169,10 +169,9 @@ public class ApplicationDependencies {
|
||||
if (groupsV2Authorization == null) {
|
||||
synchronized (LOCK) {
|
||||
if (groupsV2Authorization == null) {
|
||||
GroupsV2Authorization.ValueCache aciAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AciAuthorizationCache());
|
||||
GroupsV2Authorization.ValueCache pniAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2PniAuthorizationCache());
|
||||
GroupsV2Authorization.ValueCache authCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AciAuthorizationCache());
|
||||
|
||||
groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), aciAuthCache, pniAuthCache);
|
||||
groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), authCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,6 @@ public final class GroupManager {
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void updateGroupFromServer(@NonNull Context context,
|
||||
@NonNull ServiceId authServiceId,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
int revision,
|
||||
long timestamp,
|
||||
@@ -185,18 +184,18 @@ public final class GroupManager {
|
||||
throws GroupChangeBusyException, IOException, GroupNotAMemberException
|
||||
{
|
||||
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
|
||||
updater.updateLocalToServerRevision(authServiceId, revision, timestamp, signedGroupChange);
|
||||
updater.updateLocalToServerRevision(revision, timestamp, signedGroupChange);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
|
||||
@NonNull ServiceId authServiceserviceId,
|
||||
@NonNull ServiceId authServiceId,
|
||||
@NonNull GroupMasterKey groupMasterKey)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
new GroupManagerV2(context).groupServerQuery(authServiceserviceId, groupMasterKey);
|
||||
new GroupManagerV2(context).groupServerQuery(authServiceId, groupMasterKey);
|
||||
return V2GroupServerStatus.FULL_OR_PENDING_MEMBER;
|
||||
} catch (GroupNotAMemberException e) {
|
||||
return V2GroupServerStatus.NOT_A_MEMBER;
|
||||
|
||||
@@ -19,8 +19,8 @@ import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
@@ -51,6 +51,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||
@@ -64,6 +66,7 @@ import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2Change
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
@@ -97,6 +100,7 @@ final class GroupManagerV2 {
|
||||
private final GroupsV2Operations groupsV2Operations;
|
||||
private final GroupsV2Authorization authorization;
|
||||
private final GroupsV2StateProcessor groupsV2StateProcessor;
|
||||
private final ServiceIds serviceIds;
|
||||
private final ACI selfAci;
|
||||
private final PNI selfPni;
|
||||
private final GroupCandidateHelper groupCandidateHelper;
|
||||
@@ -109,9 +113,8 @@ final class GroupManagerV2 {
|
||||
ApplicationDependencies.getGroupsV2Operations(),
|
||||
ApplicationDependencies.getGroupsV2Authorization(),
|
||||
ApplicationDependencies.getGroupsV2StateProcessor(),
|
||||
SignalStore.account().requireAci(),
|
||||
SignalStore.account().requirePni(),
|
||||
new GroupCandidateHelper(context),
|
||||
SignalStore.account().getServiceIds(),
|
||||
new GroupCandidateHelper(),
|
||||
new SendGroupUpdateHelper(context));
|
||||
}
|
||||
|
||||
@@ -121,8 +124,7 @@ final class GroupManagerV2 {
|
||||
GroupsV2Operations groupsV2Operations,
|
||||
GroupsV2Authorization authorization,
|
||||
GroupsV2StateProcessor groupsV2StateProcessor,
|
||||
ACI selfAci,
|
||||
PNI selfPni,
|
||||
ServiceIds serviceIds,
|
||||
GroupCandidateHelper groupCandidateHelper,
|
||||
SendGroupUpdateHelper sendGroupUpdateHelper)
|
||||
{
|
||||
@@ -132,8 +134,9 @@ final class GroupManagerV2 {
|
||||
this.groupsV2Operations = groupsV2Operations;
|
||||
this.authorization = authorization;
|
||||
this.groupsV2StateProcessor = groupsV2StateProcessor;
|
||||
this.selfAci = selfAci;
|
||||
this.selfPni = selfPni;
|
||||
this.serviceIds = serviceIds;
|
||||
this.selfAci = serviceIds.getAci();
|
||||
this.selfPni = serviceIds.requirePni();
|
||||
this.groupCandidateHelper = groupCandidateHelper;
|
||||
this.sendGroupUpdateHelper = sendGroupUpdateHelper;
|
||||
}
|
||||
@@ -145,7 +148,7 @@ final class GroupManagerV2 {
|
||||
|
||||
return groupsV2Api.getGroupJoinInfo(groupSecretParams,
|
||||
Optional.ofNullable(password).map(GroupLinkPassword::serialize),
|
||||
authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -159,7 +162,7 @@ final class GroupManagerV2 {
|
||||
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
|
||||
return groupsV2Api.getGroupExternalCredential(authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
return groupsV2Api.getGroupExternalCredential(authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -212,7 +215,7 @@ final class GroupManagerV2 {
|
||||
void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
|
||||
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey)
|
||||
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
|
||||
.getCurrentGroupStateFromServer();
|
||||
}
|
||||
|
||||
@@ -220,7 +223,7 @@ final class GroupManagerV2 {
|
||||
@NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
|
||||
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
|
||||
{
|
||||
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey);
|
||||
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey);
|
||||
DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer();
|
||||
|
||||
if (latest.getRevision() == 0) {
|
||||
@@ -291,7 +294,7 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
|
||||
GroupId.V2 groupId = groupDatabase.create(authServiceId, masterKey, decryptedGroup);
|
||||
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
|
||||
RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
@@ -344,7 +347,7 @@ final class GroupManagerV2 {
|
||||
Set<GroupCandidate> groupCandidates = groupCandidateHelper.recipientIdsToCandidates(new HashSet<>(newMembers));
|
||||
|
||||
if (SignalStore.internalValues().gv2ForceInvites()) {
|
||||
groupCandidates = GroupCandidate.withoutProfileKeyCredentials(groupCandidates);
|
||||
groupCandidates = GroupCandidate.withoutExpiringProfileKeyCredentials(groupCandidates);
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid()));
|
||||
@@ -391,7 +394,7 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
if (avatarChanged) {
|
||||
String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(selfAci, groupSecretParams))
|
||||
String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams))
|
||||
: "";
|
||||
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder()
|
||||
.setAvatar(cdnKey));
|
||||
@@ -524,13 +527,13 @@ final class GroupManagerV2 {
|
||||
|
||||
GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||
|
||||
if (!groupCandidate.hasProfileKeyCredential()) {
|
||||
if (!groupCandidate.hasValidProfileKeyCredential()) {
|
||||
Log.w(TAG, "No credential available, repairing");
|
||||
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
return null;
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireProfileKeyCredential()));
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireExpiringProfileKeyCredential()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -545,14 +548,23 @@ final class GroupManagerV2 {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<DecryptedPendingMember> aciInPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> pniInPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfPni.uuid());
|
||||
|
||||
GroupCandidate groupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||
|
||||
if (!groupCandidate.hasProfileKeyCredential()) {
|
||||
if (!groupCandidate.hasValidProfileKeyCredential()) {
|
||||
Log.w(TAG, "No credential available");
|
||||
return null;
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireProfileKeyCredential()));
|
||||
if (aciInPending.isPresent()) {
|
||||
return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireExpiringProfileKeyCredential()));
|
||||
} else if (pniInPending.isPresent() && FeatureFlags.phoneNumberPrivacy()) {
|
||||
return commitChangeWithConflictResolution(selfPni, groupOperations.createAcceptPniInviteChange(groupCandidate.requireExpiringProfileKeyCredential()));
|
||||
}
|
||||
|
||||
throw new GroupChangeFailedException("Unable to accept invite when not in pending list");
|
||||
}
|
||||
|
||||
public GroupManager.GroupActionResult ban(UUID uuid)
|
||||
@@ -629,13 +641,19 @@ final class GroupManagerV2 {
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
boolean refetchedAddMemberCredentials = false;
|
||||
change.setSourceUuid(UuidUtil.toByteString(authServiceId.uuid()));
|
||||
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
return commitChange(authServiceId, change, allowWhenBlocked, sendToMembers);
|
||||
} catch (GroupPatchNotAcceptedException e) {
|
||||
throw new GroupChangeFailedException(e);
|
||||
if (change.getAddMembersCount() > 0 && !refetchedAddMemberCredentials) {
|
||||
refetchedAddMemberCredentials = true;
|
||||
change = refetchAddMemberCredentials(change);
|
||||
} else {
|
||||
throw new GroupChangeFailedException(e);
|
||||
}
|
||||
} catch (ConflictException e) {
|
||||
Log.w(TAG, "Invalid group patch or conflict", e);
|
||||
|
||||
@@ -657,7 +675,7 @@ final class GroupManagerV2 {
|
||||
private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
|
||||
throws IOException, GroupNotAMemberException, GroupChangeFailedException
|
||||
{
|
||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(authServiceId, groupMasterKey)
|
||||
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
|
||||
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
|
||||
|
||||
if (groupUpdateResult.getLatestServer() == null) {
|
||||
@@ -685,6 +703,27 @@ final class GroupManagerV2 {
|
||||
}
|
||||
}
|
||||
|
||||
private GroupChange.Actions.Builder refetchAddMemberCredentials(@NonNull GroupChange.Actions.Builder change) {
|
||||
try {
|
||||
List<RecipientId> ids = groupOperations.decryptAddMembers(change.getAddMembersList())
|
||||
.stream()
|
||||
.map(RecipientId::from)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
for (RecipientId id : ids) {
|
||||
ProfileUtil.updateExpiringProfileKeyCredential(Recipient.resolved(id));
|
||||
}
|
||||
|
||||
List<GroupCandidate> groupCandidates = groupCandidateHelper.recipientIdsToCandidatesList(ids);
|
||||
|
||||
return groupOperations.replaceAddMembers(change, groupCandidates);
|
||||
} catch (InvalidInputException | VerificationFailedException | IOException e) {
|
||||
Log.w(TAG, "Unable to refetch credentials for added members, failing change", e);
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
private GroupManager.GroupActionResult commitChange(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
@@ -726,7 +765,7 @@ final class GroupManagerV2 {
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(authServiceId, groupSecretParams), Optional.empty());
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(serviceIds, groupSecretParams), Optional.empty());
|
||||
} catch (NotInGroupException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupNotAMemberException(e);
|
||||
@@ -751,10 +790,10 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void updateLocalToServerRevision(@NonNull ServiceId authServiceId, int revision, long timestamp, @Nullable byte[] signedGroupChange)
|
||||
void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] signedGroupChange)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey)
|
||||
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
|
||||
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
|
||||
}
|
||||
|
||||
@@ -792,10 +831,10 @@ final class GroupManagerV2 {
|
||||
|
||||
if (SignalStore.internalValues().gv2ForceInvites()) {
|
||||
Log.w(TAG, "Forcing GV2 invites due to internal setting");
|
||||
candidates = GroupCandidate.withoutProfileKeyCredentials(candidates);
|
||||
candidates = GroupCandidate.withoutExpiringProfileKeyCredentials(candidates);
|
||||
}
|
||||
|
||||
if (!self.hasProfileKeyCredential()) {
|
||||
if (!self.hasValidProfileKeyCredential()) {
|
||||
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
|
||||
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
|
||||
}
|
||||
@@ -809,9 +848,9 @@ final class GroupManagerV2 {
|
||||
disappearingMessageTimerSeconds);
|
||||
|
||||
try {
|
||||
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
|
||||
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
if (decryptedGroup == null) {
|
||||
throw new GroupChangeFailedException();
|
||||
}
|
||||
@@ -907,7 +946,7 @@ final class GroupManagerV2 {
|
||||
|
||||
groupDatabase.update(groupId, updatedGroup);
|
||||
} else {
|
||||
groupDatabase.create(selfAci, groupMasterKey, decryptedGroup);
|
||||
groupDatabase.create(groupMasterKey, decryptedGroup);
|
||||
Log.i(TAG, "Created local group with placeholder");
|
||||
}
|
||||
|
||||
@@ -951,7 +990,7 @@ final class GroupManagerV2 {
|
||||
throws GroupChangeFailedException, IOException
|
||||
{
|
||||
try {
|
||||
new GroupsV2StateProcessor(context).forGroup(selfAci, groupMasterKey)
|
||||
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
|
||||
.updateLocalGroupToRevision(decryptedChange.getRevision(),
|
||||
System.currentTimeMillis(),
|
||||
decryptedChange);
|
||||
@@ -1030,14 +1069,14 @@ final class GroupManagerV2 {
|
||||
|
||||
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
|
||||
|
||||
if (!self.hasProfileKeyCredential()) {
|
||||
if (!self.hasValidProfileKeyCredential()) {
|
||||
throw new MembershipNotSuitableForV2Exception("No profile key credential for self");
|
||||
}
|
||||
|
||||
ProfileKeyCredential profileKeyCredential = self.requireProfileKeyCredential();
|
||||
ExpiringProfileKeyCredential expiringProfileKeyCredential = self.requireExpiringProfileKeyCredential();
|
||||
|
||||
GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(profileKeyCredential)
|
||||
: groupOperations.createGroupJoinDirect(profileKeyCredential);
|
||||
GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(expiringProfileKeyCredential)
|
||||
: groupOperations.createGroupJoinDirect(expiringProfileKeyCredential);
|
||||
|
||||
change.setSourceUuid(selfAci.toByteString());
|
||||
|
||||
@@ -1083,7 +1122,7 @@ final class GroupManagerV2 {
|
||||
throws GroupChangeFailedException, IOException, GroupLinkNotActiveException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfAci, groupSecretParams), Optional.ofNullable(password).map(GroupLinkPassword::serialize));
|
||||
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(serviceIds, groupSecretParams), Optional.ofNullable(password).map(GroupLinkPassword::serialize));
|
||||
} catch (NotInGroupException | VerificationFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new GroupChangeFailedException(e);
|
||||
@@ -1127,7 +1166,7 @@ final class GroupManagerV2 {
|
||||
throws IOException, VerificationFailedException, InvalidGroupStateException
|
||||
{
|
||||
try {
|
||||
groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(selfAci, groupSecretParams));
|
||||
groupsV2Api.getGroup(groupSecretParams, authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
return true;
|
||||
} catch (NotInGroupException ex) {
|
||||
return false;
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class GroupsV1MigrationUtil {
|
||||
throw new InvalidMigrationStateException();
|
||||
}
|
||||
|
||||
switch (GroupManager.v2GroupStatus(context, SignalStore.account().getAci(), gv2MasterKey)) {
|
||||
switch (GroupManager.v2GroupStatus(context, SignalStore.account().requireAci(), gv2MasterKey)) {
|
||||
case DOES_NOT_EXIST:
|
||||
Log.i(TAG, "Group does not exist on the service.");
|
||||
|
||||
@@ -186,7 +186,7 @@ public final class GroupsV1MigrationUtil {
|
||||
|
||||
Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.getRevision());
|
||||
try {
|
||||
GroupManager.updateGroupFromServer(context, SignalStore.account().requireAci(), gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
|
||||
GroupManager.updateGroupFromServer(context, gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null);
|
||||
} catch (GroupChangeBusyException | GroupNotAMemberException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
@@ -4,69 +4,53 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class GroupsV2Authorization {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2Authorization.class);
|
||||
|
||||
private final ValueCache aciCache;
|
||||
private final ValueCache pniCache;
|
||||
private final ValueCache authCache;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
|
||||
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache aciCache, @NonNull ValueCache pniCache) {
|
||||
public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache authCache) {
|
||||
this.groupsV2Api = groupsV2Api;
|
||||
this.aciCache = aciCache;
|
||||
this.pniCache = pniCache;
|
||||
this.authCache = authCache;
|
||||
}
|
||||
|
||||
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId,
|
||||
public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceIds serviceIds,
|
||||
@NonNull GroupSecretParams groupSecretParams)
|
||||
throws IOException, VerificationFailedException
|
||||
{
|
||||
boolean isPni = Objects.equals(authServiceId, SignalStore.account().getPni());
|
||||
ValueCache cache = isPni ? pniCache : aciCache;
|
||||
final long today = currentDaySeconds();
|
||||
|
||||
return getAuthorizationForToday(authServiceId, cache, groupSecretParams, !isPni);
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId,
|
||||
@NonNull ValueCache cache,
|
||||
@NonNull GroupSecretParams groupSecretParams,
|
||||
boolean isAci)
|
||||
throws IOException, VerificationFailedException
|
||||
{
|
||||
final int today = currentTimeDays();
|
||||
|
||||
Map<Integer, AuthCredentialResponse> credentials = cache.read();
|
||||
Map<Long, AuthCredentialWithPniResponse> credentials = authCache.read();
|
||||
|
||||
try {
|
||||
return getAuthorization(authServiceId, groupSecretParams, credentials, today);
|
||||
return getAuthorization(serviceIds, groupSecretParams, credentials, today);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.i(TAG, "Auth out of date, will update auth and try again");
|
||||
cache.clear();
|
||||
authCache.clear();
|
||||
} catch (VerificationFailedException e) {
|
||||
Log.w(TAG, "Verification failed, will update auth and try again", e);
|
||||
cache.clear();
|
||||
authCache.clear();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Getting new auth credential responses");
|
||||
credentials = groupsV2Api.getCredentials(today, isAci);
|
||||
cache.write(credentials);
|
||||
credentials = groupsV2Api.getCredentials(today);
|
||||
authCache.write(credentials);
|
||||
|
||||
try {
|
||||
return getAuthorization(authServiceId, groupSecretParams, credentials, today);
|
||||
return getAuthorization(serviceIds, groupSecretParams, credentials, today);
|
||||
} catch (NoCredentialForRedemptionTimeException e) {
|
||||
Log.w(TAG, "The credentials returned did not include the day requested");
|
||||
throw new IOException("Failed to get credentials");
|
||||
@@ -74,35 +58,34 @@ public class GroupsV2Authorization {
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
aciCache.clear();
|
||||
pniCache.clear();
|
||||
authCache.clear();
|
||||
}
|
||||
|
||||
private static int currentTimeDays() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
|
||||
private static long currentDaySeconds() {
|
||||
return TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getAuthorization(ServiceId authServiceId,
|
||||
private GroupsV2AuthorizationString getAuthorization(ServiceIds serviceIds,
|
||||
GroupSecretParams groupSecretParams,
|
||||
Map<Integer, AuthCredentialResponse> credentials,
|
||||
int today)
|
||||
Map<Long, AuthCredentialWithPniResponse> credentials,
|
||||
long todaySeconds)
|
||||
throws NoCredentialForRedemptionTimeException, VerificationFailedException
|
||||
{
|
||||
AuthCredentialResponse authCredentialResponse = credentials.get(today);
|
||||
AuthCredentialWithPniResponse authCredentialWithPniResponse = credentials.get(todaySeconds);
|
||||
|
||||
if (authCredentialResponse == null) {
|
||||
if (authCredentialWithPniResponse == null) {
|
||||
throw new NoCredentialForRedemptionTimeException();
|
||||
}
|
||||
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(authServiceId, today, groupSecretParams, authCredentialResponse);
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(serviceIds.getAci(), serviceIds.requirePni(), todaySeconds, groupSecretParams, authCredentialWithPniResponse);
|
||||
}
|
||||
|
||||
public interface ValueCache {
|
||||
|
||||
void clear();
|
||||
|
||||
@NonNull Map<Integer, AuthCredentialResponse> read();
|
||||
@NonNull Map<Long, AuthCredentialWithPniResponse> read();
|
||||
|
||||
void write(@NonNull Map<Integer, AuthCredentialResponse> values);
|
||||
void write(@NonNull Map<Long, AuthCredentialWithPniResponse> values);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -10,8 +10,8 @@ import java.util.Map;
|
||||
|
||||
public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Authorization.ValueCache {
|
||||
|
||||
private final GroupsV2Authorization.ValueCache inner;
|
||||
private Map<Integer, AuthCredentialResponse> values;
|
||||
private final GroupsV2Authorization.ValueCache inner;
|
||||
private Map<Long, AuthCredentialWithPniResponse> values;
|
||||
|
||||
public GroupsV2AuthorizationMemoryValueCache(@NonNull GroupsV2Authorization.ValueCache inner) {
|
||||
this.inner = inner;
|
||||
@@ -24,11 +24,11 @@ public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull synchronized Map<Integer, AuthCredentialResponse> read() {
|
||||
Map<Integer, AuthCredentialResponse> map = values;
|
||||
public @NonNull synchronized Map<Long, AuthCredentialWithPniResponse> read() {
|
||||
Map<Long, AuthCredentialWithPniResponse> map = values;
|
||||
|
||||
if (map == null) {
|
||||
map = inner.read();
|
||||
map = inner.read();
|
||||
values = map;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public final class GroupsV2AuthorizationMemoryValueCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(@NonNull Map<Integer, AuthCredentialResponse> values) {
|
||||
public synchronized void write(@NonNull Map<Long, AuthCredentialWithPniResponse> values) {
|
||||
inner.write(values);
|
||||
this.values = Collections.unmodifiableMap(new HashMap<>(values));
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -29,7 +27,7 @@ public class GroupCandidateHelper {
|
||||
private final SignalServiceAccountManager signalServiceAccountManager;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
|
||||
public GroupCandidateHelper(@NonNull Context context) {
|
||||
public GroupCandidateHelper() {
|
||||
signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
recipientDatabase = SignalDatabase.recipients();
|
||||
}
|
||||
@@ -52,27 +50,17 @@ public class GroupCandidateHelper {
|
||||
throw new AssertionError("Non UUID members should have need detected by now");
|
||||
}
|
||||
|
||||
Optional<ProfileKeyCredential> profileKeyCredential = Optional.ofNullable(recipient.getProfileKeyCredential());
|
||||
GroupCandidate candidate = new GroupCandidate(serviceId.uuid(), profileKeyCredential);
|
||||
Optional<ExpiringProfileKeyCredential> expiringProfileKeyCredential = Optional.ofNullable(recipient.getExpiringProfileKeyCredential());
|
||||
GroupCandidate candidate = new GroupCandidate(serviceId.uuid(), expiringProfileKeyCredential);
|
||||
|
||||
if (!candidate.hasProfileKeyCredential()) {
|
||||
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
if (!candidate.hasValidProfileKeyCredential()) {
|
||||
recipientDatabase.clearProfileKeyCredential(recipient.getId());
|
||||
|
||||
if (profileKey != null) {
|
||||
Log.i(TAG, String.format("No profile key credential on recipient %s, fetching", recipient.getId()));
|
||||
|
||||
Optional<ProfileKeyCredential> profileKeyCredentialOptional = signalServiceAccountManager.resolveProfileKeyCredential(serviceId, profileKey, Locale.getDefault());
|
||||
|
||||
if (profileKeyCredentialOptional.isPresent()) {
|
||||
boolean updatedProfileKey = recipientDatabase.setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
|
||||
|
||||
if (!updatedProfileKey) {
|
||||
Log.w(TAG, String.format("Failed to update the profile key credential on recipient %s", recipient.getId()));
|
||||
} else {
|
||||
Log.i(TAG, String.format("Got new profile key credential for recipient %s", recipient.getId()));
|
||||
candidate = candidate.withProfileKeyCredential(profileKeyCredentialOptional.get());
|
||||
}
|
||||
}
|
||||
Optional<ExpiringProfileKeyCredential> credential = ProfileUtil.updateExpiringProfileKeyCredential(recipient);
|
||||
if (credential.isPresent()) {
|
||||
candidate = candidate.withExpiringProfileKeyCredential(credential.get());
|
||||
} else {
|
||||
candidate = candidate.withoutExpiringProfileKeyCredential();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +79,17 @@ public class GroupCandidateHelper {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull List<GroupCandidate> recipientIdsToCandidatesList(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws IOException
|
||||
{
|
||||
List<GroupCandidate> result = new ArrayList<>(recipientIds.size());
|
||||
|
||||
for (RecipientId recipientId : recipientIds) {
|
||||
result.add(recipientIdToCandidate(recipientId));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
@@ -103,10 +104,10 @@ public class GroupsV2StateProcessor {
|
||||
this.groupDatabase = SignalDatabase.groups();
|
||||
}
|
||||
|
||||
public StateProcessorForGroup forGroup(@NonNull ServiceId serviceId, @NonNull GroupMasterKey groupMasterKey) {
|
||||
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceId, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
|
||||
public StateProcessorForGroup forGroup(@NonNull ServiceIds serviceIds, @NonNull GroupMasterKey groupMasterKey) {
|
||||
ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceIds.getAci(), groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase);
|
||||
|
||||
return new StateProcessorForGroup(serviceId, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
|
||||
return new StateProcessorForGroup(serviceIds, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper);
|
||||
}
|
||||
|
||||
public enum GroupState {
|
||||
@@ -140,7 +141,7 @@ public class GroupsV2StateProcessor {
|
||||
}
|
||||
|
||||
public static final class StateProcessorForGroup {
|
||||
private final ServiceId serviceId;
|
||||
private final ServiceIds serviceIds;
|
||||
private final Context context;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
@@ -150,7 +151,7 @@ public class GroupsV2StateProcessor {
|
||||
private final GroupSecretParams groupSecretParams;
|
||||
private final ProfileAndMessageHelper profileAndMessageHelper;
|
||||
|
||||
@VisibleForTesting StateProcessorForGroup(@NonNull ServiceId serviceId,
|
||||
@VisibleForTesting StateProcessorForGroup(@NonNull ServiceIds serviceIds,
|
||||
@NonNull Context context,
|
||||
@NonNull GroupDatabase groupDatabase,
|
||||
@NonNull GroupsV2Api groupsV2Api,
|
||||
@@ -158,7 +159,7 @@ public class GroupsV2StateProcessor {
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull ProfileAndMessageHelper profileAndMessageHelper)
|
||||
{
|
||||
this.serviceId = serviceId;
|
||||
this.serviceIds = serviceIds;
|
||||
this.context = context;
|
||||
this.groupDatabase = groupDatabase;
|
||||
this.groupsV2Api = groupsV2Api;
|
||||
@@ -196,17 +197,17 @@ public class GroupsV2StateProcessor {
|
||||
{
|
||||
|
||||
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange) && notHavingInviteRevoked(signedGroupChange)) {
|
||||
Log.w(TAG, "Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch.");
|
||||
warn("Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch.");
|
||||
} else if (SignalStore.internalValues().gv2IgnoreP2PChanges()) {
|
||||
Log.w(TAG, "Ignoring P2P group change by setting");
|
||||
warn( "Ignoring P2P group change by setting");
|
||||
} else {
|
||||
try {
|
||||
Log.i(TAG, "Applying P2P group change");
|
||||
info("Applying P2P group change");
|
||||
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
|
||||
|
||||
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange)));
|
||||
} catch (NotAbleToApplyGroupV2ChangeException e) {
|
||||
Log.w(TAG, "Unable to apply P2P group change", e);
|
||||
warn( "Unable to apply P2P group change", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,24 +219,24 @@ public class GroupsV2StateProcessor {
|
||||
if (localState != null && signedGroupChange != null) {
|
||||
try {
|
||||
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange)) {
|
||||
Log.w(TAG, "Server says we're not a member. Ignoring P2P group change because we're not currently in the group and this change doesn't add us in.");
|
||||
warn( "Server says we're not a member. Ignoring P2P group change because we're not currently in the group and this change doesn't add us in.");
|
||||
} else {
|
||||
Log.i(TAG, "Server says we're not a member. Applying P2P group change.");
|
||||
info("Server says we're not a member. Applying P2P group change.");
|
||||
DecryptedGroup newState = DecryptedGroupUtil.applyWithoutRevisionCheck(localState, signedGroupChange);
|
||||
|
||||
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange)));
|
||||
}
|
||||
} catch (NotAbleToApplyGroupV2ChangeException failed) {
|
||||
Log.w(TAG, "Unable to apply P2P group change when not a member", failed);
|
||||
warn( "Unable to apply P2P group change when not a member", failed);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputGroupState == null) {
|
||||
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceId.uuid())) {
|
||||
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member");
|
||||
if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceIds)) {
|
||||
warn( "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member");
|
||||
throw new GroupNotAMemberException(e, true);
|
||||
} else {
|
||||
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
|
||||
warn( "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
|
||||
insertGroupLeave();
|
||||
}
|
||||
throw e;
|
||||
@@ -252,7 +253,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
Log.i(TAG, "Inserting single update message for restore placeholder");
|
||||
info("Inserting single update message for restore placeholder");
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
||||
} else {
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||
@@ -261,7 +262,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
||||
if (remainingWork.getServerHistory().size() > 0) {
|
||||
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber()));
|
||||
info(String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", newLocalState.getRevision() + 1, remainingWork.getLatestRevisionNumber()));
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId, remainingWork.getLatestRevisionNumber()));
|
||||
}
|
||||
|
||||
@@ -276,14 +277,14 @@ public class GroupsV2StateProcessor {
|
||||
.map(DecryptedMember::getUuid)
|
||||
.map(UuidUtil::fromByteStringOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.anyMatch(u -> u.equals(serviceId.uuid()));
|
||||
.anyMatch(serviceIds::matches);
|
||||
|
||||
boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList()
|
||||
.stream()
|
||||
.map(DecryptedPendingMember::getUuid)
|
||||
.map(UuidUtil::fromByteStringOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.anyMatch(u -> u.equals(serviceId.uuid()));
|
||||
.anyMatch(serviceIds::matches);
|
||||
|
||||
return !currentlyInGroup && !addedAsMember && !addedAsPendingMember;
|
||||
}
|
||||
@@ -294,7 +295,7 @@ public class GroupsV2StateProcessor {
|
||||
.map(DecryptedPendingMemberRemoval::getUuid)
|
||||
.map(UuidUtil::fromByteStringOrNull)
|
||||
.filter(Objects::nonNull)
|
||||
.anyMatch(u -> u.equals(serviceId.uuid()));
|
||||
.anyMatch(serviceIds::matches);
|
||||
|
||||
return !havingInviteRevoked;
|
||||
}
|
||||
@@ -305,44 +306,43 @@ public class GroupsV2StateProcessor {
|
||||
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst) throws IOException, GroupNotAMemberException {
|
||||
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
|
||||
|
||||
Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly);
|
||||
info("Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly);
|
||||
|
||||
PartialDecryptedGroup latestServerGroup;
|
||||
GlobalGroupState inputGroupState;
|
||||
|
||||
try {
|
||||
latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams));
|
||||
latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
} catch (NotInGroupException | GroupNotFoundException e) {
|
||||
throw new GroupNotAMemberException(e);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList())) {
|
||||
Log.i(TAG, "Local state is at or later than server");
|
||||
if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceIds.getAci().uuid(), localState.getMembersList())) {
|
||||
info("Local state is at or later than server");
|
||||
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
|
||||
}
|
||||
|
||||
if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceId.uuid(), latestServerGroup.getMembersList())) {
|
||||
Log.i(TAG, "Latest revision or not a member, use latest only");
|
||||
if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceIds.getAci().uuid(), latestServerGroup.getMembersList())) {
|
||||
info("Latest revision or not a member, use latest only");
|
||||
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup.getFullyDecryptedGroup(), null)));
|
||||
} else {
|
||||
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceId.uuid());
|
||||
int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceIds.getAci().uuid());
|
||||
int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded;
|
||||
|
||||
boolean includeFirstState = forceIncludeFirst ||
|
||||
localState == null ||
|
||||
localState.getRevision() < 0 ||
|
||||
localState.getRevision() == revisionWeWereAdded ||
|
||||
!GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList()) ||
|
||||
!GroupProtoUtil.isMember(serviceIds.getAci().uuid(), localState.getMembersList()) ||
|
||||
(revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision());
|
||||
|
||||
Log.i(TAG,
|
||||
"Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") +
|
||||
" logsNeededFrom: " + logsNeededFrom +
|
||||
" includeFirstState: " + includeFirstState +
|
||||
" forceIncludeFirst: " + forceIncludeFirst);
|
||||
inputGroupState = getFullMemberHistoryPage(localState, serviceId, logsNeededFrom, includeFirstState);
|
||||
info("Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") +
|
||||
" logsNeededFrom: " + logsNeededFrom +
|
||||
" includeFirstState: " + includeFirstState +
|
||||
" forceIncludeFirst: " + forceIncludeFirst);
|
||||
inputGroupState = getFullMemberHistoryPage(localState, logsNeededFrom, includeFirstState);
|
||||
}
|
||||
|
||||
ProfileKeySet profileKeys = new ProfileKeySet();
|
||||
@@ -354,13 +354,13 @@ public class GroupsV2StateProcessor {
|
||||
while (hasMore) {
|
||||
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision);
|
||||
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();
|
||||
Log.i(TAG, "Advanced group to revision: " + (newLocalState != null ? newLocalState.getRevision() : "null"));
|
||||
info("Advanced group to revision: " + (newLocalState != null ? newLocalState.getRevision() : "null"));
|
||||
|
||||
if (newLocalState != null && !inputGroupState.hasMore() && !forceIncludeFirst) {
|
||||
int newLocalRevision = newLocalState.getRevision();
|
||||
int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision;
|
||||
if (newLocalRevision < requestRevision) {
|
||||
Log.w(TAG, "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]");
|
||||
warn( "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]");
|
||||
return updateLocalGroupFromServerPaged(revision, localState, timestamp, true);
|
||||
}
|
||||
}
|
||||
@@ -389,20 +389,20 @@ public class GroupsV2StateProcessor {
|
||||
hasMore = inputGroupState.hasMore();
|
||||
|
||||
if (hasMore) {
|
||||
Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision());
|
||||
inputGroupState = getFullMemberHistoryPage(finalState, serviceId, inputGroupState.getNextPageRevision(), false);
|
||||
info("Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision());
|
||||
inputGroupState = getFullMemberHistoryPage(finalState, inputGroupState.getNextPageRevision(), false);
|
||||
}
|
||||
}
|
||||
|
||||
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
Log.i(TAG, "Inserting single update message for restore placeholder");
|
||||
info("Inserting single update message for restore placeholder");
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(finalState, null)));
|
||||
}
|
||||
|
||||
profileAndMessageHelper.persistLearnedProfileKeys(profileKeys);
|
||||
|
||||
if (finalGlobalGroupState.getServerHistory().size() > 0) {
|
||||
Log.i(TAG, String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", finalState.getRevision() + 1, finalGlobalGroupState.getLatestRevisionNumber()));
|
||||
info(String.format(Locale.US, "There are more revisions on the server for this group, scheduling for later, V[%d..%d]", finalState.getRevision() + 1, finalGlobalGroupState.getLatestRevisionNumber()));
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId, finalGlobalGroupState.getLatestRevisionNumber()));
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ public class GroupsV2StateProcessor {
|
||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams));
|
||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
|
||||
} catch (GroupNotFoundException e) {
|
||||
throw new GroupDoesNotExistException(e);
|
||||
} catch (NotInGroupException e) {
|
||||
@@ -429,7 +429,7 @@ public class GroupsV2StateProcessor {
|
||||
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
|
||||
{
|
||||
try {
|
||||
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), true)
|
||||
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), true)
|
||||
.getResults()
|
||||
.get(0)
|
||||
.getGroup()
|
||||
@@ -445,12 +445,12 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
private void insertGroupLeave() {
|
||||
if (!groupDatabase.isActive(groupId)) {
|
||||
Log.w(TAG, "Group has already been left.");
|
||||
warn("Group has already been left.");
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient groupRecipient = Recipient.externalGroupExact(groupId);
|
||||
UUID selfUuid = serviceId.uuid();
|
||||
UUID selfUuid = serviceIds.getAci().uuid();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId)
|
||||
.requireV2GroupProperties()
|
||||
@@ -485,7 +485,7 @@ public class GroupsV2StateProcessor {
|
||||
mmsDatabase.markAsSent(id, true);
|
||||
threadDatabase.update(threadId, false, false);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Failed to insert leave message.", e);
|
||||
warn( "Failed to insert leave message.", e);
|
||||
}
|
||||
|
||||
groupDatabase.setActive(groupId, false);
|
||||
@@ -509,7 +509,7 @@ public class GroupsV2StateProcessor {
|
||||
boolean needsAvatarFetch;
|
||||
|
||||
if (inputGroupState.getLocalState() == null) {
|
||||
groupDatabase.create(serviceId, masterKey, newLocalState);
|
||||
groupDatabase.create(masterKey, newLocalState);
|
||||
needsAvatarFetch = !TextUtils.isEmpty(newLocalState.getAvatar());
|
||||
} else {
|
||||
groupDatabase.update(masterKey, newLocalState);
|
||||
@@ -523,14 +523,14 @@ public class GroupsV2StateProcessor {
|
||||
profileAndMessageHelper.determineProfileSharing(inputGroupState, newLocalState);
|
||||
}
|
||||
|
||||
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ServiceId serviceId, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
|
||||
private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, int logsNeededFromRevision, boolean includeFirstState) throws IOException {
|
||||
try {
|
||||
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), includeFirstState);
|
||||
GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), includeFirstState);
|
||||
ArrayList<ServerGroupLogEntry> history = new ArrayList<>(groupHistoryPage.getResults().size());
|
||||
boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges();
|
||||
|
||||
if (ignoreServerChanges) {
|
||||
Log.w(TAG, "Server change logs are ignored by setting");
|
||||
warn( "Server change logs are ignored by setting");
|
||||
}
|
||||
|
||||
for (DecryptedGroupHistoryEntry entry : groupHistoryPage.getResults()) {
|
||||
@@ -547,6 +547,22 @@ public class GroupsV2StateProcessor {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void info(String message) {
|
||||
info(message, null);
|
||||
}
|
||||
|
||||
private void info(String message, Throwable t) {
|
||||
Log.i(TAG, "[" + groupId.toString() + "] " + message, t);
|
||||
}
|
||||
|
||||
private void warn(String message) {
|
||||
warn(message, null);
|
||||
}
|
||||
|
||||
private void warn(String message, Throwable e) {
|
||||
Log.w(TAG, "[" + groupId.toString() + "] " + message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -615,7 +631,7 @@ public class GroupsV2StateProcessor {
|
||||
.map(uuid -> Recipient.externalPush(ServiceId.from(uuid))));
|
||||
|
||||
if (addedBy.isPresent() && addedBy.get().isBlocked()) {
|
||||
Log.i(TAG, String.format( "Added to group %s by a blocked user %s. Leaving group.", groupId, addedBy.get().getId()));
|
||||
Log.i(TAG, String.format("Added to group %s by a blocked user %s. Leaving group.", groupId, addedBy.get().getId()));
|
||||
ApplicationDependencies.getJobManager().add(new LeaveGroupV2Job(groupId));
|
||||
//noinspection UnnecessaryReturnStatement
|
||||
return;
|
||||
|
||||
@@ -6,8 +6,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository;
|
||||
import org.thoughtcrime.securesms.badges.Badges;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
@@ -32,13 +32,13 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -130,26 +130,23 @@ public class RefreshOwnProfileJob extends BaseJob {
|
||||
setProfileBadges(profile.getBadges());
|
||||
ensureUnidentifiedAccessCorrect(profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||
|
||||
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
|
||||
if (profileKeyCredential.isPresent()) {
|
||||
setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get());
|
||||
}
|
||||
profileAndCredential.getExpiringProfileKeyCredential()
|
||||
.ifPresent(expiringProfileKeyCredential -> setExpiringProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), expiringProfileKeyCredential));
|
||||
|
||||
StoryOnboardingDownloadJob.Companion.enqueueIfNeeded();
|
||||
}
|
||||
|
||||
private void setProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ProfileKeyCredential credential)
|
||||
private void setExpiringProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ExpiringProfileKeyCredential credential)
|
||||
{
|
||||
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
||||
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
|
||||
}
|
||||
|
||||
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
|
||||
return !recipient.hasProfileKeyCredential()
|
||||
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
|
||||
: SignalServiceProfile.RequestType.PROFILE;
|
||||
return ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential()) ? SignalServiceProfile.RequestType.PROFILE
|
||||
: SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
@@ -15,10 +14,8 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -91,24 +88,7 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
ServiceId authServiceId = group.get().getAuthServiceId() != null ? group.get().getAuthServiceId() : SignalStore.account().requireAci();
|
||||
|
||||
try {
|
||||
GroupManager.updateGroupFromServer(context, authServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
|
||||
} catch (GroupNotAMemberException | IOException e) {
|
||||
ServiceId otherServiceId = authServiceId.equals(SignalStore.account().getPni()) ? SignalStore.account().getAci() : SignalStore.account().getPni();
|
||||
boolean isNotAMemberOrPending = e instanceof GroupNotAMemberException && !((GroupNotAMemberException) e).isLikelyPendingMember();
|
||||
boolean verificationFailed = e.getCause() instanceof VerificationFailedException;
|
||||
|
||||
if (otherServiceId != null && (isNotAMemberOrPending || verificationFailed)) {
|
||||
Log.i(TAG, "Request failed, attempting with other id");
|
||||
GroupManager.updateGroupFromServer(context, otherServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
|
||||
Log.i(TAG, "Request succeeded with other credential. Associating " + otherServiceId + " with group " + groupId);
|
||||
SignalDatabase.groups().setAuthServiceId(otherServiceId, groupId);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,13 +12,14 @@ import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.ListUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
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.profiles.ExpiringProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.Badges;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
@@ -41,7 +42,6 @@ import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
@@ -50,6 +50,7 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -59,7 +60,6 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -274,7 +274,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
} else if (processor.genericIoError()) {
|
||||
state.retries.add(recipient.getId());
|
||||
} else {
|
||||
Log.w(TAG, "Failed to retrieve profile for " + recipient.getId());
|
||||
Log.w(TAG, "Failed to retrieve profile for " + recipient.getId(), processor.getError());
|
||||
}
|
||||
return state;
|
||||
})
|
||||
@@ -350,10 +350,8 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||
|
||||
if (recipientProfileKey != null) {
|
||||
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
|
||||
if (profileKeyCredential.isPresent()) {
|
||||
setProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential.get());
|
||||
}
|
||||
profileAndCredential.getExpiringProfileKeyCredential()
|
||||
.ifPresent(profileKeyCredential -> setExpiringProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,18 +369,17 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
SignalDatabase.recipients().setBadges(recipient.getId(), badges);
|
||||
}
|
||||
|
||||
private void setProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ProfileKeyCredential credential)
|
||||
private void setExpiringProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ExpiringProfileKeyCredential credential)
|
||||
{
|
||||
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
|
||||
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
|
||||
}
|
||||
|
||||
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
|
||||
return !recipient.hasProfileKeyCredential()
|
||||
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
|
||||
: SignalServiceProfile.RequestType.PROFILE;
|
||||
return ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential()) ? SignalServiceProfile.RequestType.PROFILE
|
||||
: SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL;
|
||||
}
|
||||
|
||||
private void setIdentityKey(Recipient recipient, String identityKeyValue) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIds
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponse;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TemporalAuthCredentialResponses;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||
@@ -21,27 +21,20 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2AuthorizationSignalStoreCache.class);
|
||||
|
||||
private static final String ACI_PREFIX = "gv2:auth_token_cache";
|
||||
private static final int ACI_VERSION = 2;
|
||||
|
||||
private static final String PNI_PREFIX = "gv2:auth_token_cache:pni";
|
||||
private static final int PNI_VERSION = 1;
|
||||
private static final String ACI_PNI_PREFIX = "gv2:auth_token_cache";
|
||||
private static final int ACI_PNI_VERSION = 3;
|
||||
|
||||
private final String key;
|
||||
private final KeyValueStore store;
|
||||
|
||||
public static GroupsV2AuthorizationSignalStoreCache createAciCache(@NonNull KeyValueStore store) {
|
||||
if (store.containsKey(ACI_PREFIX)) {
|
||||
if (store.containsKey(ACI_PNI_PREFIX)) {
|
||||
store.beginWrite()
|
||||
.remove(ACI_PREFIX)
|
||||
.remove(ACI_PNI_PREFIX)
|
||||
.commit();
|
||||
}
|
||||
|
||||
return new GroupsV2AuthorizationSignalStoreCache(store, ACI_PREFIX + ":" + ACI_VERSION);
|
||||
}
|
||||
|
||||
public static GroupsV2AuthorizationSignalStoreCache createPniCache(@NonNull KeyValueStore store) {
|
||||
return new GroupsV2AuthorizationSignalStoreCache(store, PNI_PREFIX + ":" + PNI_VERSION);
|
||||
return new GroupsV2AuthorizationSignalStoreCache(store, ACI_PNI_PREFIX + ":" + ACI_PNI_VERSION);
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationSignalStoreCache(@NonNull KeyValueStore store, @NonNull String key) {
|
||||
@@ -55,27 +48,27 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
.remove(key)
|
||||
.commit();
|
||||
|
||||
info("Cleared local response cache");
|
||||
Log.i(TAG, "Cleared local response cache");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Map<Integer, AuthCredentialResponse> read() {
|
||||
public @NonNull Map<Long, AuthCredentialWithPniResponse> read() {
|
||||
byte[] credentialBlob = store.getBlob(key, null);
|
||||
|
||||
if (credentialBlob == null) {
|
||||
info("No credentials responses are cached locally");
|
||||
Log.i(TAG, "No credentials responses are cached locally");
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
try {
|
||||
TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob);
|
||||
HashMap<Integer, AuthCredentialResponse> result = new HashMap<>(temporalCredentials.getCredentialResponseCount());
|
||||
TemporalAuthCredentialResponses temporalCredentials = TemporalAuthCredentialResponses.parseFrom(credentialBlob);
|
||||
HashMap<Long, AuthCredentialWithPniResponse> result = new HashMap<>(temporalCredentials.getCredentialResponseCount());
|
||||
|
||||
for (TemporalAuthCredentialResponse credential : temporalCredentials.getCredentialResponseList()) {
|
||||
result.put(credential.getDate(), new AuthCredentialResponse(credential.getAuthCredentialResponse().toByteArray()));
|
||||
result.put(credential.getDate(), new AuthCredentialWithPniResponse(credential.getAuthCredentialResponse().toByteArray()));
|
||||
}
|
||||
|
||||
info(String.format(Locale.US, "Loaded %d credentials from local storage", result.size()));
|
||||
Log.i(TAG, String.format(Locale.US, "Loaded %d credentials from local storage", result.size()));
|
||||
|
||||
return result;
|
||||
} catch (InvalidProtocolBufferException | InvalidInputException e) {
|
||||
@@ -84,10 +77,10 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(@NonNull Map<Integer, AuthCredentialResponse> values) {
|
||||
public void write(@NonNull Map<Long, AuthCredentialWithPniResponse> values) {
|
||||
TemporalAuthCredentialResponses.Builder builder = TemporalAuthCredentialResponses.newBuilder();
|
||||
|
||||
for (Map.Entry<Integer, AuthCredentialResponse> entry : values.entrySet()) {
|
||||
for (Map.Entry<Long, AuthCredentialWithPniResponse> entry : values.entrySet()) {
|
||||
builder.addCredentialResponse(TemporalAuthCredentialResponse.newBuilder()
|
||||
.setDate(entry.getKey())
|
||||
.setAuthCredentialResponse(ByteString.copyFrom(entry.getValue().serialize())));
|
||||
@@ -97,10 +90,6 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth
|
||||
.putBlob(key, builder.build().toByteArray())
|
||||
.commit();
|
||||
|
||||
info(String.format(Locale.US, "Written %d credentials to local storage", values.size()));
|
||||
}
|
||||
|
||||
private void info(String message) {
|
||||
Log.i(TAG, (key.startsWith(PNI_PREFIX) ? "[PNI]" : "[ACI]") + " " + message);
|
||||
Log.i(TAG, String.format(Locale.US, "Written %d credentials to local storage", values.size()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Helper for dealing with [ServiceId] matching when you only care that either of your
|
||||
* service ids match but don't care which one.
|
||||
*/
|
||||
data class ServiceIds(val aci: ACI, val pni: PNI?) {
|
||||
|
||||
private val aciByteString: ByteString by lazy { UuidUtil.toByteString(aci.uuid()) }
|
||||
private val pniByteString: ByteString? by lazy { pni?.let { UuidUtil.toByteString(it.uuid()) } }
|
||||
|
||||
fun matches(uuid: UUID): Boolean {
|
||||
return uuid == aci.uuid() || uuid == pni?.uuid()
|
||||
}
|
||||
|
||||
fun matches(uuid: ByteString): Boolean {
|
||||
return uuid == aciByteString || uuid == pniByteString
|
||||
}
|
||||
}
|
||||
@@ -265,10 +265,6 @@ public final class SignalStore {
|
||||
return GroupsV2AuthorizationSignalStoreCache.createAciCache(getStore());
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2PniAuthorizationCache() {
|
||||
return GroupsV2AuthorizationSignalStoreCache.createPniCache(getStore());
|
||||
}
|
||||
|
||||
public static @NonNull PreferenceDataStore getPreferenceDataStore() {
|
||||
return new SignalPreferenceDataStore(getStore());
|
||||
}
|
||||
|
||||
@@ -544,13 +544,8 @@ public final class MessageContentProcessor {
|
||||
throws IOException, GroupChangeBusyException
|
||||
{
|
||||
try {
|
||||
ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid());
|
||||
if (authServiceId == null) {
|
||||
warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI");
|
||||
authServiceId = SignalStore.account().requireAci();
|
||||
}
|
||||
long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1;
|
||||
GroupManager.updateGroupFromServer(context, authServiceId, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange());
|
||||
long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1;
|
||||
GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange());
|
||||
return true;
|
||||
} catch (GroupNotAMemberException e) {
|
||||
warn(String.valueOf(content.getTimestamp()), "Ignoring message for a group we're not in");
|
||||
|
||||
@@ -103,9 +103,10 @@ public class ApplicationMigrations {
|
||||
static final int STORY_DISTRIBUTION_LIST_SYNC = 59;
|
||||
static final int EMOJI_VERSION_7 = 60;
|
||||
static final int MY_STORY_PRIVACY_MODE = 61;
|
||||
static final int REFRESH_EXPIRING_CREDENTIAL = 62;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 61;
|
||||
public static final int CURRENT_VERSION = 62;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -451,6 +452,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.MY_STORY_PRIVACY_MODE, new SyncDistributionListsMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.REFRESH_EXPIRING_CREDENTIAL) {
|
||||
jobs.put(Version.REFRESH_EXPIRING_CREDENTIAL, new AttributesMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -22,8 +23,8 @@ import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
@@ -81,7 +82,7 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number) { }
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number) {}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
@@ -98,9 +99,9 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement
|
||||
}
|
||||
|
||||
private void createPaymentOrShowWarningDialog(@NonNull Recipient recipient) {
|
||||
if (recipient.hasProfileKeyCredential()) {
|
||||
if (ExpiringProfileCredentialUtil.isValid(recipient.getExpiringProfileKeyCredential())) {
|
||||
createPayment(recipient.getId());
|
||||
} else {
|
||||
} else {
|
||||
showWarningDialog(recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
@@ -81,61 +81,61 @@ public class Recipient {
|
||||
|
||||
private static final int MAX_MEMBER_NAMES = 10;
|
||||
|
||||
private final RecipientId id;
|
||||
private final boolean resolving;
|
||||
private final ServiceId serviceId;
|
||||
private final PNI pni;
|
||||
private final String username;
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final GroupId groupId;
|
||||
private final DistributionListId distributionListId;
|
||||
private final List<Recipient> participants;
|
||||
private final Optional<Long> groupAvatarId;
|
||||
private final boolean isSelf;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
private final VibrateState messageVibrate;
|
||||
private final VibrateState callVibrate;
|
||||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final Optional<Integer> defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
private final byte[] profileKey;
|
||||
private final ProfileKeyCredential profileKeyCredential;
|
||||
private final String groupName;
|
||||
private final Uri systemContactPhoto;
|
||||
private final String customLabel;
|
||||
private final Uri contactUri;
|
||||
private final ProfileName signalProfileName;
|
||||
private final String profileAvatar;
|
||||
private final boolean hasProfileImage;
|
||||
private final boolean profileSharing;
|
||||
private final long lastProfileFetch;
|
||||
private final String notificationChannel;
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final Capability groupsV1MigrationCapability;
|
||||
private final Capability senderKeyCapability;
|
||||
private final Capability announcementGroupCapability;
|
||||
private final Capability changeNumberCapability;
|
||||
private final Capability storiesCapability;
|
||||
private final Capability giftBadgesCapability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageId;
|
||||
private final MentionSetting mentionSetting;
|
||||
private final ChatWallpaper wallpaper;
|
||||
private final ChatColors chatColors;
|
||||
private final AvatarColor avatarColor;
|
||||
private final String about;
|
||||
private final String aboutEmoji;
|
||||
private final ProfileName systemProfileName;
|
||||
private final String systemContactName;
|
||||
private final Optional<Extras> extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
private final List<Badge> badges;
|
||||
private final boolean isReleaseNotesRecipient;
|
||||
private final RecipientId id;
|
||||
private final boolean resolving;
|
||||
private final ServiceId serviceId;
|
||||
private final PNI pni;
|
||||
private final String username;
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final GroupId groupId;
|
||||
private final DistributionListId distributionListId;
|
||||
private final List<Recipient> participants;
|
||||
private final Optional<Long> groupAvatarId;
|
||||
private final boolean isSelf;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
private final VibrateState messageVibrate;
|
||||
private final VibrateState callVibrate;
|
||||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final Optional<Integer> defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
private final byte[] profileKey;
|
||||
private final ExpiringProfileKeyCredential expiringProfileKeyCredential;
|
||||
private final String groupName;
|
||||
private final Uri systemContactPhoto;
|
||||
private final String customLabel;
|
||||
private final Uri contactUri;
|
||||
private final ProfileName signalProfileName;
|
||||
private final String profileAvatar;
|
||||
private final boolean hasProfileImage;
|
||||
private final boolean profileSharing;
|
||||
private final long lastProfileFetch;
|
||||
private final String notificationChannel;
|
||||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final Capability groupsV1MigrationCapability;
|
||||
private final Capability senderKeyCapability;
|
||||
private final Capability announcementGroupCapability;
|
||||
private final Capability changeNumberCapability;
|
||||
private final Capability storiesCapability;
|
||||
private final Capability giftBadgesCapability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageId;
|
||||
private final MentionSetting mentionSetting;
|
||||
private final ChatWallpaper wallpaper;
|
||||
private final ChatColors chatColors;
|
||||
private final AvatarColor avatarColor;
|
||||
private final String about;
|
||||
private final String aboutEmoji;
|
||||
private final ProfileName systemProfileName;
|
||||
private final String systemContactName;
|
||||
private final Optional<Extras> extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
private final List<Badge> badges;
|
||||
private final boolean isReleaseNotesRecipient;
|
||||
|
||||
/**
|
||||
* Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be
|
||||
@@ -358,119 +358,119 @@ public class Recipient {
|
||||
}
|
||||
|
||||
Recipient(@NonNull RecipientId id) {
|
||||
this.id = id;
|
||||
this.resolving = true;
|
||||
this.serviceId = null;
|
||||
this.pni = null;
|
||||
this.username = null;
|
||||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.distributionListId = null;
|
||||
this.participants = Collections.emptyList();
|
||||
this.groupAvatarId = Optional.empty();
|
||||
this.isSelf = false;
|
||||
this.blocked = false;
|
||||
this.muteUntil = 0;
|
||||
this.messageVibrate = VibrateState.DEFAULT;
|
||||
this.callVibrate = VibrateState.DEFAULT;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
this.expireMessages = 0;
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.profileKeyCredential = null;
|
||||
this.groupName = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.signalProfileName = ProfileName.EMPTY;
|
||||
this.profileAvatar = null;
|
||||
this.hasProfileImage = false;
|
||||
this.profileSharing = false;
|
||||
this.lastProfileFetch = 0;
|
||||
this.notificationChannel = null;
|
||||
this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED;
|
||||
this.forceSmsSelection = false;
|
||||
this.groupsV1MigrationCapability = Capability.UNKNOWN;
|
||||
this.senderKeyCapability = Capability.UNKNOWN;
|
||||
this.announcementGroupCapability = Capability.UNKNOWN;
|
||||
this.changeNumberCapability = Capability.UNKNOWN;
|
||||
this.storiesCapability = Capability.UNKNOWN;
|
||||
this.giftBadgesCapability = Capability.UNKNOWN;
|
||||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
this.chatColors = null;
|
||||
this.avatarColor = AvatarColor.UNKNOWN;
|
||||
this.about = null;
|
||||
this.aboutEmoji = null;
|
||||
this.systemProfileName = ProfileName.EMPTY;
|
||||
this.systemContactName = null;
|
||||
this.extras = Optional.empty();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
this.isReleaseNotesRecipient = false;
|
||||
this.id = id;
|
||||
this.resolving = true;
|
||||
this.serviceId = null;
|
||||
this.pni = null;
|
||||
this.username = null;
|
||||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.distributionListId = null;
|
||||
this.participants = Collections.emptyList();
|
||||
this.groupAvatarId = Optional.empty();
|
||||
this.isSelf = false;
|
||||
this.blocked = false;
|
||||
this.muteUntil = 0;
|
||||
this.messageVibrate = VibrateState.DEFAULT;
|
||||
this.callVibrate = VibrateState.DEFAULT;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
this.expireMessages = 0;
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.expiringProfileKeyCredential = null;
|
||||
this.groupName = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.signalProfileName = ProfileName.EMPTY;
|
||||
this.profileAvatar = null;
|
||||
this.hasProfileImage = false;
|
||||
this.profileSharing = false;
|
||||
this.lastProfileFetch = 0;
|
||||
this.notificationChannel = null;
|
||||
this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED;
|
||||
this.forceSmsSelection = false;
|
||||
this.groupsV1MigrationCapability = Capability.UNKNOWN;
|
||||
this.senderKeyCapability = Capability.UNKNOWN;
|
||||
this.announcementGroupCapability = Capability.UNKNOWN;
|
||||
this.changeNumberCapability = Capability.UNKNOWN;
|
||||
this.storiesCapability = Capability.UNKNOWN;
|
||||
this.giftBadgesCapability = Capability.UNKNOWN;
|
||||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
this.chatColors = null;
|
||||
this.avatarColor = AvatarColor.UNKNOWN;
|
||||
this.about = null;
|
||||
this.aboutEmoji = null;
|
||||
this.systemProfileName = ProfileName.EMPTY;
|
||||
this.systemContactName = null;
|
||||
this.extras = Optional.empty();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
this.isReleaseNotesRecipient = false;
|
||||
}
|
||||
|
||||
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
|
||||
this.id = id;
|
||||
this.resolving = !resolved;
|
||||
this.serviceId = details.serviceId;
|
||||
this.pni = details.pni;
|
||||
this.username = details.username;
|
||||
this.e164 = details.e164;
|
||||
this.email = details.email;
|
||||
this.groupId = details.groupId;
|
||||
this.distributionListId = details.distributionListId;
|
||||
this.participants = details.participants;
|
||||
this.groupAvatarId = details.groupAvatarId;
|
||||
this.isSelf = details.isSelf;
|
||||
this.blocked = details.blocked;
|
||||
this.muteUntil = details.mutedUntil;
|
||||
this.messageVibrate = details.messageVibrateState;
|
||||
this.callVibrate = details.callVibrateState;
|
||||
this.messageRingtone = details.messageRingtone;
|
||||
this.callRingtone = details.callRingtone;
|
||||
this.insightsBannerTier = details.insightsBannerTier;
|
||||
this.defaultSubscriptionId = details.defaultSubscriptionId;
|
||||
this.expireMessages = details.expireMessages;
|
||||
this.registered = details.registered;
|
||||
this.profileKey = details.profileKey;
|
||||
this.profileKeyCredential = details.profileKeyCredential;
|
||||
this.groupName = details.groupName;
|
||||
this.systemContactPhoto = details.systemContactPhoto;
|
||||
this.customLabel = details.customLabel;
|
||||
this.contactUri = details.contactUri;
|
||||
this.signalProfileName = details.profileName;
|
||||
this.profileAvatar = details.profileAvatar;
|
||||
this.hasProfileImage = details.hasProfileImage;
|
||||
this.profileSharing = details.profileSharing;
|
||||
this.lastProfileFetch = details.lastProfileFetch;
|
||||
this.notificationChannel = details.notificationChannel;
|
||||
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.groupsV1MigrationCapability = details.groupsV1MigrationCapability;
|
||||
this.senderKeyCapability = details.senderKeyCapability;
|
||||
this.announcementGroupCapability = details.announcementGroupCapability;
|
||||
this.changeNumberCapability = details.changeNumberCapability;
|
||||
this.storiesCapability = details.storiesCapability;
|
||||
this.giftBadgesCapability = details.giftBadgesCapability;
|
||||
this.storageId = details.storageId;
|
||||
this.mentionSetting = details.mentionSetting;
|
||||
this.wallpaper = details.wallpaper;
|
||||
this.chatColors = details.chatColors;
|
||||
this.avatarColor = details.avatarColor;
|
||||
this.about = details.about;
|
||||
this.aboutEmoji = details.aboutEmoji;
|
||||
this.systemProfileName = details.systemProfileName;
|
||||
this.systemContactName = details.systemContactName;
|
||||
this.extras = details.extras;
|
||||
this.hasGroupsInCommon = details.hasGroupsInCommon;
|
||||
this.badges = details.badges;
|
||||
this.isReleaseNotesRecipient = details.isReleaseChannel;
|
||||
this.id = id;
|
||||
this.resolving = !resolved;
|
||||
this.serviceId = details.serviceId;
|
||||
this.pni = details.pni;
|
||||
this.username = details.username;
|
||||
this.e164 = details.e164;
|
||||
this.email = details.email;
|
||||
this.groupId = details.groupId;
|
||||
this.distributionListId = details.distributionListId;
|
||||
this.participants = details.participants;
|
||||
this.groupAvatarId = details.groupAvatarId;
|
||||
this.isSelf = details.isSelf;
|
||||
this.blocked = details.blocked;
|
||||
this.muteUntil = details.mutedUntil;
|
||||
this.messageVibrate = details.messageVibrateState;
|
||||
this.callVibrate = details.callVibrateState;
|
||||
this.messageRingtone = details.messageRingtone;
|
||||
this.callRingtone = details.callRingtone;
|
||||
this.insightsBannerTier = details.insightsBannerTier;
|
||||
this.defaultSubscriptionId = details.defaultSubscriptionId;
|
||||
this.expireMessages = details.expireMessages;
|
||||
this.registered = details.registered;
|
||||
this.profileKey = details.profileKey;
|
||||
this.expiringProfileKeyCredential = details.expiringProfileKeyCredential;
|
||||
this.groupName = details.groupName;
|
||||
this.systemContactPhoto = details.systemContactPhoto;
|
||||
this.customLabel = details.customLabel;
|
||||
this.contactUri = details.contactUri;
|
||||
this.signalProfileName = details.profileName;
|
||||
this.profileAvatar = details.profileAvatar;
|
||||
this.hasProfileImage = details.hasProfileImage;
|
||||
this.profileSharing = details.profileSharing;
|
||||
this.lastProfileFetch = details.lastProfileFetch;
|
||||
this.notificationChannel = details.notificationChannel;
|
||||
this.unidentifiedAccessMode = details.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.groupsV1MigrationCapability = details.groupsV1MigrationCapability;
|
||||
this.senderKeyCapability = details.senderKeyCapability;
|
||||
this.announcementGroupCapability = details.announcementGroupCapability;
|
||||
this.changeNumberCapability = details.changeNumberCapability;
|
||||
this.storiesCapability = details.storiesCapability;
|
||||
this.giftBadgesCapability = details.giftBadgesCapability;
|
||||
this.storageId = details.storageId;
|
||||
this.mentionSetting = details.mentionSetting;
|
||||
this.wallpaper = details.wallpaper;
|
||||
this.chatColors = details.chatColors;
|
||||
this.avatarColor = details.avatarColor;
|
||||
this.about = details.about;
|
||||
this.aboutEmoji = details.aboutEmoji;
|
||||
this.systemProfileName = details.systemProfileName;
|
||||
this.systemContactName = details.systemContactName;
|
||||
this.extras = details.extras;
|
||||
this.hasGroupsInCommon = details.hasGroupsInCommon;
|
||||
this.badges = details.badges;
|
||||
this.isReleaseNotesRecipient = details.isReleaseChannel;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
@@ -1022,12 +1022,8 @@ public class Recipient {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public @Nullable ProfileKeyCredential getProfileKeyCredential() {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
public boolean hasProfileKeyCredential() {
|
||||
return profileKeyCredential != null;
|
||||
public @Nullable ExpiringProfileKeyCredential getExpiringProfileKeyCredential() {
|
||||
return expiringProfileKeyCredential;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageServiceId() {
|
||||
@@ -1285,7 +1281,7 @@ public class Recipient {
|
||||
Objects.equals(defaultSubscriptionId, other.defaultSubscriptionId) &&
|
||||
registered == other.registered &&
|
||||
Arrays.equals(profileKey, other.profileKey) &&
|
||||
Objects.equals(profileKeyCredential, other.profileKeyCredential) &&
|
||||
Objects.equals(expiringProfileKeyCredential, other.expiringProfileKeyCredential) &&
|
||||
Objects.equals(groupName, other.groupName) &&
|
||||
Objects.equals(systemContactPhoto, other.systemContactPhoto) &&
|
||||
Objects.equals(customLabel, other.customLabel) &&
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
@@ -33,60 +33,60 @@ import java.util.Optional;
|
||||
|
||||
public class RecipientDetails {
|
||||
|
||||
final ServiceId serviceId;
|
||||
final PNI pni;
|
||||
final String username;
|
||||
final String e164;
|
||||
final String email;
|
||||
final GroupId groupId;
|
||||
final DistributionListId distributionListId;
|
||||
final String groupName;
|
||||
final String systemContactName;
|
||||
final String customLabel;
|
||||
final Uri systemContactPhoto;
|
||||
final Uri contactUri;
|
||||
final Optional<Long> groupAvatarId;
|
||||
final Uri messageRingtone;
|
||||
final Uri callRingtone;
|
||||
final long mutedUntil;
|
||||
final VibrateState messageVibrateState;
|
||||
final VibrateState callVibrateState;
|
||||
final boolean blocked;
|
||||
final int expireMessages;
|
||||
final List<Recipient> participants;
|
||||
final ProfileName profileName;
|
||||
final Optional<Integer> defaultSubscriptionId;
|
||||
final RegisteredState registered;
|
||||
final byte[] profileKey;
|
||||
final ProfileKeyCredential profileKeyCredential;
|
||||
final String profileAvatar;
|
||||
final boolean hasProfileImage;
|
||||
final boolean profileSharing;
|
||||
final long lastProfileFetch;
|
||||
final boolean systemContact;
|
||||
final boolean isSelf;
|
||||
final String notificationChannel;
|
||||
final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
final boolean forceSmsSelection;
|
||||
final Recipient.Capability groupsV1MigrationCapability;
|
||||
final Recipient.Capability senderKeyCapability;
|
||||
final Recipient.Capability announcementGroupCapability;
|
||||
final Recipient.Capability changeNumberCapability;
|
||||
final Recipient.Capability storiesCapability;
|
||||
final Recipient.Capability giftBadgesCapability;
|
||||
final InsightsBannerTier insightsBannerTier;
|
||||
final byte[] storageId;
|
||||
final MentionSetting mentionSetting;
|
||||
final ChatWallpaper wallpaper;
|
||||
final ChatColors chatColors;
|
||||
final AvatarColor avatarColor;
|
||||
final String about;
|
||||
final String aboutEmoji;
|
||||
final ProfileName systemProfileName;
|
||||
final Optional<Recipient.Extras> extras;
|
||||
final boolean hasGroupsInCommon;
|
||||
final List<Badge> badges;
|
||||
final boolean isReleaseChannel;
|
||||
final ServiceId serviceId;
|
||||
final PNI pni;
|
||||
final String username;
|
||||
final String e164;
|
||||
final String email;
|
||||
final GroupId groupId;
|
||||
final DistributionListId distributionListId;
|
||||
final String groupName;
|
||||
final String systemContactName;
|
||||
final String customLabel;
|
||||
final Uri systemContactPhoto;
|
||||
final Uri contactUri;
|
||||
final Optional<Long> groupAvatarId;
|
||||
final Uri messageRingtone;
|
||||
final Uri callRingtone;
|
||||
final long mutedUntil;
|
||||
final VibrateState messageVibrateState;
|
||||
final VibrateState callVibrateState;
|
||||
final boolean blocked;
|
||||
final int expireMessages;
|
||||
final List<Recipient> participants;
|
||||
final ProfileName profileName;
|
||||
final Optional<Integer> defaultSubscriptionId;
|
||||
final RegisteredState registered;
|
||||
final byte[] profileKey;
|
||||
final ExpiringProfileKeyCredential expiringProfileKeyCredential;
|
||||
final String profileAvatar;
|
||||
final boolean hasProfileImage;
|
||||
final boolean profileSharing;
|
||||
final long lastProfileFetch;
|
||||
final boolean systemContact;
|
||||
final boolean isSelf;
|
||||
final String notificationChannel;
|
||||
final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
final boolean forceSmsSelection;
|
||||
final Recipient.Capability groupsV1MigrationCapability;
|
||||
final Recipient.Capability senderKeyCapability;
|
||||
final Recipient.Capability announcementGroupCapability;
|
||||
final Recipient.Capability changeNumberCapability;
|
||||
final Recipient.Capability storiesCapability;
|
||||
final Recipient.Capability giftBadgesCapability;
|
||||
final InsightsBannerTier insightsBannerTier;
|
||||
final byte[] storageId;
|
||||
final MentionSetting mentionSetting;
|
||||
final ChatWallpaper wallpaper;
|
||||
final ChatColors chatColors;
|
||||
final AvatarColor avatarColor;
|
||||
final String about;
|
||||
final String aboutEmoji;
|
||||
final ProfileName systemProfileName;
|
||||
final Optional<Recipient.Extras> extras;
|
||||
final boolean hasGroupsInCommon;
|
||||
final List<Badge> badges;
|
||||
final boolean isReleaseChannel;
|
||||
|
||||
public RecipientDetails(@Nullable String groupName,
|
||||
@Nullable String systemContactName,
|
||||
@@ -98,117 +98,117 @@ public class RecipientDetails {
|
||||
@Nullable List<Recipient> participants,
|
||||
boolean isReleaseChannel)
|
||||
{
|
||||
this.groupAvatarId = groupAvatarId;
|
||||
this.systemContactPhoto = Util.uri(record.getSystemContactPhotoUri());
|
||||
this.customLabel = record.getSystemPhoneLabel();
|
||||
this.contactUri = Util.uri(record.getSystemContactUri());
|
||||
this.serviceId = record.getServiceId();
|
||||
this.pni = record.getPni();
|
||||
this.username = record.getUsername();
|
||||
this.e164 = record.getE164();
|
||||
this.email = record.getEmail();
|
||||
this.groupId = record.getGroupId();
|
||||
this.distributionListId = record.getDistributionListId();
|
||||
this.messageRingtone = record.getMessageRingtone();
|
||||
this.callRingtone = record.getCallRingtone();
|
||||
this.mutedUntil = record.getMuteUntil();
|
||||
this.messageVibrateState = record.getMessageVibrateState();
|
||||
this.callVibrateState = record.getCallVibrateState();
|
||||
this.blocked = record.isBlocked();
|
||||
this.expireMessages = record.getExpireMessages();
|
||||
this.participants = participants == null ? new LinkedList<>() : participants;
|
||||
this.profileName = record.getProfileName();
|
||||
this.defaultSubscriptionId = record.getDefaultSubscriptionId();
|
||||
this.registered = registeredState;
|
||||
this.profileKey = record.getProfileKey();
|
||||
this.profileKeyCredential = record.getProfileKeyCredential();
|
||||
this.profileAvatar = record.getProfileAvatar();
|
||||
this.hasProfileImage = record.hasProfileImage();
|
||||
this.profileSharing = record.isProfileSharing();
|
||||
this.lastProfileFetch = record.getLastProfileFetch();
|
||||
this.systemContact = systemContact;
|
||||
this.isSelf = isSelf;
|
||||
this.notificationChannel = record.getNotificationChannel();
|
||||
this.unidentifiedAccessMode = record.getUnidentifiedAccessMode();
|
||||
this.forceSmsSelection = record.isForceSmsSelection();
|
||||
this.groupsV1MigrationCapability = record.getGroupsV1MigrationCapability();
|
||||
this.senderKeyCapability = record.getSenderKeyCapability();
|
||||
this.announcementGroupCapability = record.getAnnouncementGroupCapability();
|
||||
this.changeNumberCapability = record.getChangeNumberCapability();
|
||||
this.storiesCapability = record.getStoriesCapability();
|
||||
this.giftBadgesCapability = record.getGiftBadgesCapability();
|
||||
this.insightsBannerTier = record.getInsightsBannerTier();
|
||||
this.storageId = record.getStorageId();
|
||||
this.mentionSetting = record.getMentionSetting();
|
||||
this.wallpaper = record.getWallpaper();
|
||||
this.chatColors = record.getChatColors();
|
||||
this.avatarColor = record.getAvatarColor();
|
||||
this.about = record.getAbout();
|
||||
this.aboutEmoji = record.getAboutEmoji();
|
||||
this.systemProfileName = record.getSystemProfileName();
|
||||
this.groupName = groupName;
|
||||
this.systemContactName = systemContactName;
|
||||
this.extras = Optional.ofNullable(record.getExtras());
|
||||
this.hasGroupsInCommon = record.hasGroupsInCommon();
|
||||
this.badges = record.getBadges();
|
||||
this.isReleaseChannel = isReleaseChannel;
|
||||
this.groupAvatarId = groupAvatarId;
|
||||
this.systemContactPhoto = Util.uri(record.getSystemContactPhotoUri());
|
||||
this.customLabel = record.getSystemPhoneLabel();
|
||||
this.contactUri = Util.uri(record.getSystemContactUri());
|
||||
this.serviceId = record.getServiceId();
|
||||
this.pni = record.getPni();
|
||||
this.username = record.getUsername();
|
||||
this.e164 = record.getE164();
|
||||
this.email = record.getEmail();
|
||||
this.groupId = record.getGroupId();
|
||||
this.distributionListId = record.getDistributionListId();
|
||||
this.messageRingtone = record.getMessageRingtone();
|
||||
this.callRingtone = record.getCallRingtone();
|
||||
this.mutedUntil = record.getMuteUntil();
|
||||
this.messageVibrateState = record.getMessageVibrateState();
|
||||
this.callVibrateState = record.getCallVibrateState();
|
||||
this.blocked = record.isBlocked();
|
||||
this.expireMessages = record.getExpireMessages();
|
||||
this.participants = participants == null ? new LinkedList<>() : participants;
|
||||
this.profileName = record.getProfileName();
|
||||
this.defaultSubscriptionId = record.getDefaultSubscriptionId();
|
||||
this.registered = registeredState;
|
||||
this.profileKey = record.getProfileKey();
|
||||
this.expiringProfileKeyCredential = record.getExpiringProfileKeyCredential();
|
||||
this.profileAvatar = record.getProfileAvatar();
|
||||
this.hasProfileImage = record.hasProfileImage();
|
||||
this.profileSharing = record.isProfileSharing();
|
||||
this.lastProfileFetch = record.getLastProfileFetch();
|
||||
this.systemContact = systemContact;
|
||||
this.isSelf = isSelf;
|
||||
this.notificationChannel = record.getNotificationChannel();
|
||||
this.unidentifiedAccessMode = record.getUnidentifiedAccessMode();
|
||||
this.forceSmsSelection = record.isForceSmsSelection();
|
||||
this.groupsV1MigrationCapability = record.getGroupsV1MigrationCapability();
|
||||
this.senderKeyCapability = record.getSenderKeyCapability();
|
||||
this.announcementGroupCapability = record.getAnnouncementGroupCapability();
|
||||
this.changeNumberCapability = record.getChangeNumberCapability();
|
||||
this.storiesCapability = record.getStoriesCapability();
|
||||
this.giftBadgesCapability = record.getGiftBadgesCapability();
|
||||
this.insightsBannerTier = record.getInsightsBannerTier();
|
||||
this.storageId = record.getStorageId();
|
||||
this.mentionSetting = record.getMentionSetting();
|
||||
this.wallpaper = record.getWallpaper();
|
||||
this.chatColors = record.getChatColors();
|
||||
this.avatarColor = record.getAvatarColor();
|
||||
this.about = record.getAbout();
|
||||
this.aboutEmoji = record.getAboutEmoji();
|
||||
this.systemProfileName = record.getSystemProfileName();
|
||||
this.groupName = groupName;
|
||||
this.systemContactName = systemContactName;
|
||||
this.extras = Optional.ofNullable(record.getExtras());
|
||||
this.hasGroupsInCommon = record.hasGroupsInCommon();
|
||||
this.badges = record.getBadges();
|
||||
this.isReleaseChannel = isReleaseChannel;
|
||||
}
|
||||
|
||||
private RecipientDetails() {
|
||||
this.groupAvatarId = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.serviceId = null;
|
||||
this.pni = null;
|
||||
this.username = null;
|
||||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.distributionListId = null;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.mutedUntil = 0;
|
||||
this.messageVibrateState = VibrateState.DEFAULT;
|
||||
this.callVibrateState = VibrateState.DEFAULT;
|
||||
this.blocked = false;
|
||||
this.expireMessages = 0;
|
||||
this.participants = new LinkedList<>();
|
||||
this.profileName = ProfileName.EMPTY;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.profileKeyCredential = null;
|
||||
this.profileAvatar = null;
|
||||
this.hasProfileImage = false;
|
||||
this.profileSharing = false;
|
||||
this.lastProfileFetch = 0;
|
||||
this.systemContact = true;
|
||||
this.isSelf = false;
|
||||
this.notificationChannel = null;
|
||||
this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
|
||||
this.forceSmsSelection = false;
|
||||
this.groupName = null;
|
||||
this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN;
|
||||
this.senderKeyCapability = Recipient.Capability.UNKNOWN;
|
||||
this.announcementGroupCapability = Recipient.Capability.UNKNOWN;
|
||||
this.changeNumberCapability = Recipient.Capability.UNKNOWN;
|
||||
this.storiesCapability = Recipient.Capability.UNKNOWN;
|
||||
this.giftBadgesCapability = Recipient.Capability.UNKNOWN;
|
||||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
this.chatColors = null;
|
||||
this.avatarColor = AvatarColor.UNKNOWN;
|
||||
this.about = null;
|
||||
this.aboutEmoji = null;
|
||||
this.systemProfileName = ProfileName.EMPTY;
|
||||
this.systemContactName = null;
|
||||
this.extras = Optional.empty();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
this.isReleaseChannel = false;
|
||||
this.groupAvatarId = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.serviceId = null;
|
||||
this.pni = null;
|
||||
this.username = null;
|
||||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.distributionListId = null;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.mutedUntil = 0;
|
||||
this.messageVibrateState = VibrateState.DEFAULT;
|
||||
this.callVibrateState = VibrateState.DEFAULT;
|
||||
this.blocked = false;
|
||||
this.expireMessages = 0;
|
||||
this.participants = new LinkedList<>();
|
||||
this.profileName = ProfileName.EMPTY;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.expiringProfileKeyCredential = null;
|
||||
this.profileAvatar = null;
|
||||
this.hasProfileImage = false;
|
||||
this.profileSharing = false;
|
||||
this.lastProfileFetch = 0;
|
||||
this.systemContact = true;
|
||||
this.isSelf = false;
|
||||
this.notificationChannel = null;
|
||||
this.unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
|
||||
this.forceSmsSelection = false;
|
||||
this.groupName = null;
|
||||
this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN;
|
||||
this.senderKeyCapability = Recipient.Capability.UNKNOWN;
|
||||
this.announcementGroupCapability = Recipient.Capability.UNKNOWN;
|
||||
this.changeNumberCapability = Recipient.Capability.UNKNOWN;
|
||||
this.storiesCapability = Recipient.Capability.UNKNOWN;
|
||||
this.giftBadgesCapability = Recipient.Capability.UNKNOWN;
|
||||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
this.chatColors = null;
|
||||
this.avatarColor = AvatarColor.UNKNOWN;
|
||||
this.about = null;
|
||||
this.aboutEmoji = null;
|
||||
this.systemProfileName = ProfileName.EMPTY;
|
||||
this.systemContactName = null;
|
||||
this.extras = Optional.empty();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
this.isReleaseChannel = false;
|
||||
}
|
||||
|
||||
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientRecord settings) {
|
||||
|
||||
@@ -85,7 +85,6 @@ public final class FeatureFlags {
|
||||
private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging";
|
||||
private static final String DONOR_BADGES = "android.donorBadges.6";
|
||||
private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4";
|
||||
private static final String CDSH = "android.cdsh";
|
||||
private static final String STORIES = "android.stories.2";
|
||||
private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions";
|
||||
private static final String HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList";
|
||||
@@ -134,7 +133,6 @@ public final class FeatureFlags {
|
||||
SUGGEST_SMS_BLACKLIST,
|
||||
MAX_GROUP_CALL_RING_SIZE,
|
||||
GROUP_CALL_RINGING,
|
||||
CDSH,
|
||||
SENDER_KEY_MAX_AGE,
|
||||
DONOR_BADGES,
|
||||
DONOR_BADGES_DISPLAY,
|
||||
@@ -199,7 +197,6 @@ public final class FeatureFlags {
|
||||
SENDER_KEY,
|
||||
MAX_GROUP_CALL_RING_SIZE,
|
||||
GROUP_CALL_RINGING,
|
||||
CDSH,
|
||||
SENDER_KEY_MAX_AGE,
|
||||
DONOR_BADGES_DISPLAY,
|
||||
DONATE_MEGAPHONE,
|
||||
@@ -470,10 +467,6 @@ public final class FeatureFlags {
|
||||
return getBoolean(DONOR_BADGES_DISPLAY, true);
|
||||
}
|
||||
|
||||
public static boolean cdsh() {
|
||||
return Environment.IS_STAGING && getBoolean(CDSH, false);
|
||||
}
|
||||
|
||||
/** A comma-separated list of models that should *not* use hardware AEC for calling. */
|
||||
public static @NonNull String hardwareAecBlocklistModels() {
|
||||
return getString(HARDWARE_AEC_BLOCKLIST_MODELS, "");
|
||||
|
||||
@@ -44,6 +44,13 @@ fun Long.toLocalDateTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalDateTime
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), zoneId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert milliseconds to local date time with provided [zoneId].
|
||||
*/
|
||||
fun Instant.toLocalDateTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalDateTime {
|
||||
return LocalDateTime.ofInstant(this, zoneId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts milliseconds to local time with provided [zoneId].
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
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.models.Badge;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
@@ -282,6 +283,35 @@ public final class ProfileUtil {
|
||||
Recipient.self().getBadges());
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to update just the expiring profile key credential with a new one. If unable, an empty optional is returned.
|
||||
*
|
||||
* Note: It will try to find missing profile key credentials from the server and persist locally.
|
||||
*/
|
||||
public static Optional<ExpiringProfileKeyCredential> updateExpiringProfileKeyCredential(@NonNull Recipient recipient) throws IOException {
|
||||
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
|
||||
if (profileKey != null) {
|
||||
Log.i(TAG, String.format("Updating profile key credential on recipient %s, fetching", recipient.getId()));
|
||||
|
||||
Optional<ExpiringProfileKeyCredential> profileKeyCredentialOptional = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
.resolveProfileKeyCredential(recipient.requireServiceId(), profileKey, Locale.getDefault());
|
||||
|
||||
if (profileKeyCredentialOptional.isPresent()) {
|
||||
boolean updatedProfileKey = SignalDatabase.recipients().setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
|
||||
|
||||
if (!updatedProfileKey) {
|
||||
Log.w(TAG, String.format("Failed to update the profile key credential on recipient %s", recipient.getId()));
|
||||
} else {
|
||||
Log.i(TAG, String.format("Got new profile key credential for recipient %s", recipient.getId()));
|
||||
return profileKeyCredentialOptional;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static void uploadProfile(@NonNull ProfileName profileName,
|
||||
@Nullable String about,
|
||||
@Nullable String aboutEmoji,
|
||||
|
||||
Reference in New Issue
Block a user