Retrieve profiles in parallel.

This commit is contained in:
Greyson Parrelli
2020-06-08 19:04:55 -04:00
parent 2822042eeb
commit 2751fd7efc
24 changed files with 639 additions and 270 deletions

View File

@@ -56,6 +56,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class DirectoryHelperV1 {
@@ -339,10 +342,16 @@ class DirectoryHelperV1 {
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE);
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (NotFoundException e) {
return false;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}

View File

@@ -207,7 +207,6 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
@@ -1957,7 +1956,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return;
}
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(recipient.get()));
RetrieveProfileJob.enqueueAsync(recipient.getId());
}
private void onRecipientChanged(@NonNull Recipient recipient) {

View File

@@ -43,7 +43,7 @@ final class GroupsV2CapabilityChecker {
Recipient.Capability gv2Capability = member.getGroupsV2Capability();
if (gv2Capability != Recipient.Capability.SUPPORTED) {
if (!ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob.forRecipient(member), TimeUnit.SECONDS.toMillis(1000)).isPresent()) {
if (!ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob.forRecipient(member.getId()), TimeUnit.SECONDS.toMillis(1000)).isPresent()) {
throw new IOException("Recipient capability was not retrieved in time");
}
}

View File

@@ -59,17 +59,12 @@ public final class GroupCandidateHelper {
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (profileKey != null) {
try {
Optional<ProfileKeyCredential> profileKeyCredentialOptional = signalServiceAccountManager.resolveProfileKeyCredential(uuid, profileKey);
Optional<ProfileKeyCredential> profileKeyCredentialOptional = signalServiceAccountManager.resolveProfileKeyCredential(uuid, profileKey);
if (profileKeyCredentialOptional.isPresent()) {
candidate = candidate.withProfileKeyCredential(profileKeyCredentialOptional.get());
if (profileKeyCredentialOptional.isPresent()) {
candidate = candidate.withProfileKeyCredential(profileKeyCredentialOptional.get());
recipientDatabase.setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
}
} catch (VerificationFailedException e) {
Log.w(TAG, e);
throw new IOException(e);
recipientDatabase.setProfileKeyCredential(recipient.getId(), profileKey, profileKeyCredentialOptional.get());
}
}
}

View File

@@ -290,9 +290,7 @@ public final class GroupsV2StateProcessor {
if (!updated.isEmpty()) {
Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, scheduling profile retrievals", updated.size()));
for (RecipientId recipient : updated) {
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(recipient));
}
RetrieveProfileJob.enqueue(updated);
}
}

View File

@@ -3,9 +3,13 @@ package org.thoughtcrime.securesms.jobmanager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Data {
@@ -76,6 +80,14 @@ public class Data {
return stringArrays.get(key);
}
/**
* Helper method for {@link #getStringArray(String)} that returns the value as a list.
*/
public List<String> getStringArrayAsList(@NonNull String key) {
throwIfAbsent(stringArrays, key);
return Arrays.asList(stringArrays.get(key));
}
public boolean hasInt(@NonNull String key) {
return integers.containsKey(key);
@@ -255,6 +267,14 @@ public class Data {
return this;
}
/**
* Helper method for {@link #putStringArray(String, String[])} that takes a list.
*/
public Builder putStringListAsArray(@NonNull String key, @NonNull List<String> value) {
stringArrays.put(key, value.toArray(new String[0]));
return this;
}
public Builder putInt(@NonNull String key, int value) {
integers.put(key, value);
return this;

View File

@@ -39,7 +39,7 @@ public class JobManager implements ConstraintObserver.Notifier {
private static final String TAG = JobManager.class.getSimpleName();
public static final int CURRENT_VERSION = 6;
public static final int CURRENT_VERSION = 7;
private final Application application;
private final Configuration configuration;

View File

@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.jobmanager.migrations;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.JobMigration;
import org.thoughtcrime.securesms.logging.Log;
import java.util.SortedSet;
import java.util.TreeSet;
public class RetrieveProfileJobMigration extends JobMigration {
private static final String TAG = Log.tag(RetrieveProfileJobMigration.class);
public RetrieveProfileJobMigration() {
super(7);
}
@Override
protected @NonNull JobData migrate(@NonNull JobData jobData) {
Log.i(TAG, "Running.");
if ("RetrieveProfileJob".equals(jobData.getFactoryKey())) {
return migrateRetrieveProfileJob(jobData);
}
return jobData;
}
private static @NonNull JobData migrateRetrieveProfileJob(@NonNull JobData jobData) {
Data data = jobData.getData();
if (data.hasString("recipient")) {
Log.i(TAG, "Migrating job.");
String recipient = data.getString("recipient");
return jobData.withData(new Data.Builder()
.putStringArray("recipients", new String[] { recipient })
.build());
} else {
return jobData;
}
}
}

View File

@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.PushProcessMessageQueueJ
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMigration2;
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
@@ -169,6 +170,7 @@ public final class JobManagerFactories {
new RecipientIdFollowUpJobMigration(),
new RecipientIdFollowUpJobMigration2(),
new SendReadReceiptsJobMigration(DatabaseFactory.getMmsSmsDatabase(application)),
new PushProcessMessageQueueJobMigration(application));
new PushProcessMessageQueueJobMigration(application),
new RetrieveProfileJobMigration());
}
}

View File

@@ -69,7 +69,6 @@ import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -1450,7 +1449,7 @@ public final class PushProcessMessageJob extends BaseJob {
if (messageProfileKey != null) {
if (database.setProfileKey(recipient.getId(), messageProfileKey)) {
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(recipient));
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(recipient.getId()));
}
} else {
Log.w(TAG, "Ignored invalid profile key seen in message");

View File

@@ -72,7 +72,7 @@ public class RefreshOwnProfileJob extends BaseJob {
}
Recipient self = Recipient.self();
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, self, getRequestType(self));
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfileSync(context, self, getRequestType(self));
SignalServiceProfile profile = profileAndCredential.getProfile();
setProfileName(profile.getName());

View File

@@ -1,10 +1,14 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
@@ -16,31 +20,49 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessM
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.transport.RetryLaterException;
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.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Retrieves a users profile and sets the appropriate local fields.
* <p>
* Recipient can be self if you use {@link #forRecipient} and it will delegate to {@link RefreshOwnProfileJob}.
*/
public class RetrieveProfileJob extends BaseJob {
@@ -48,38 +70,84 @@ public class RetrieveProfileJob extends BaseJob {
private static final String TAG = RetrieveProfileJob.class.getSimpleName();
private static final String KEY_RECIPIENT = "recipient";
private static final String KEY_RECIPIENTS = "recipients";
private final RecipientId recipientId;
private final List<RecipientId> recipientIds;
public static Job forRecipient(@NonNull Recipient recipient) {
return forRecipient(recipient.getId());
/**
* Identical to {@link #enqueue(Collection)})}, but run on a background thread for convenience.
*/
public static void enqueueAsync(@NonNull RecipientId recipientId) {
SignalExecutors.BOUNDED.execute(() -> {
ApplicationDependencies.getJobManager().add(forRecipient(recipientId));
});
}
public static Job forRecipient(@NonNull RecipientId recipientId) {
if (Recipient.self().getId().equals(recipientId)) {
/**
* Submits the necessary jobs to refresh the profiles of the requested recipients. Works for any
* RecipientIds, including individuals, groups, or yourself.
*/
@WorkerThread
public static void enqueue(@NonNull Collection<RecipientId> recipientIds) {
Context context = ApplicationDependencies.getApplication();
JobManager jobManager = ApplicationDependencies.getJobManager();
List<RecipientId> combined = new LinkedList<>();
for (RecipientId recipientId : recipientIds) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isLocalNumber()) {
jobManager.add(new RefreshOwnProfileJob());
} else if (recipient.isGroup()) {
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
combined.addAll(Stream.of(recipients).map(Recipient::getId).toList());
} else {
combined.add(recipientId);
}
}
jobManager.add(new RetrieveProfileJob(combined));
}
/**
* Works for any RecipientId, whether it's an individual, group, or yourself.
*/
@WorkerThread
public static @NonNull Job forRecipient(@NonNull RecipientId recipientId) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isLocalNumber()) {
return new RefreshOwnProfileJob();
} else if (recipient.isGroup()) {
Context context = ApplicationDependencies.getApplication();
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return new RetrieveProfileJob(Stream.of(recipients).map(Recipient::getId).toList());
} else {
return new RetrieveProfileJob(recipientId);
return new RetrieveProfileJob(Collections.singletonList(recipientId));
}
}
private RetrieveProfileJob(@NonNull RecipientId recipientId) {
private RetrieveProfileJob(@NonNull List<RecipientId> recipientIds) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(3)
.build(),
recipientId);
recipientIds);
}
private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull RecipientId recipientId) {
private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull List<RecipientId> recipientIds) {
super(parameters);
this.recipientId = recipientId;
this.recipientIds = recipientIds;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_RECIPIENT, recipientId.serialize()).build();
return new Data.Builder()
.putStringListAsArray(KEY_RECIPIENTS, Stream.of(recipientIds)
.map(RecipientId::serialize)
.toList())
.build();
}
@Override
@@ -88,39 +156,74 @@ public class RetrieveProfileJob extends BaseJob {
}
@Override
public void onRun() throws IOException {
Log.i(TAG, "Retrieving profile of " + recipientId);
public void onRun() throws IOException, RetryLaterException {
Stopwatch stopwatch = new Stopwatch("RetrieveProfile");
Set<RecipientId> retries = new HashSet<>();
Recipient resolved = Recipient.resolved(recipientId);
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
stopwatch.split("resolve");
if (resolved.isGroup()) handleGroupRecipient(resolved);
else handleIndividualRecipient(resolved);
List<Pair<Recipient, ListenableFuture<ProfileAndCredential>>> futures = Stream.of(recipients)
.filter(Recipient::hasServiceIdentifier)
.map(r -> new Pair<>(r, ProfileUtil.retrieveProfile(context, r, getRequestType(r))))
.toList();
stopwatch.split("futures");
List<Pair<Recipient, ProfileAndCredential>> profiles = Stream.of(futures)
.map(pair -> {
Recipient recipient = pair.first();
try {
ProfileAndCredential profile = pair.second().get(5, TimeUnit.SECONDS);
return new Pair<>(recipient, profile);
} catch (InterruptedException | TimeoutException e) {
retries.add(recipient.getId());
} catch (ExecutionException e) {
if (e.getCause() instanceof PushNetworkException) {
retries.add(recipient.getId());
} else if (e.getCause() instanceof NotFoundException) {
Log.w(TAG, "Failed to find a profile for " + recipient.getId());
} else {
Log.w(TAG, "Failed to retrieve profile for " + recipient.getId());
}
}
return null;
})
.withoutNulls()
.toList();
stopwatch.split("network");
for (Pair<Recipient, ProfileAndCredential> profile : profiles) {
process(profile.first(), profile.second());
}
stopwatch.split("process");
long keyCount = Stream.of(profiles).map(Pair::first).map(Recipient::getProfileKey).withoutNulls().count();
Log.d(TAG, String.format(Locale.US, "Started with %d recipient(s). Found %d profile(s), and had keys for %d of them. Will retry %d.", recipients.size(), profiles.size(), keyCount, retries.size()));
stopwatch.stop(TAG);
recipientIds.clear();
recipientIds.addAll(retries);
if (recipientIds.size() > 0) {
throw new RetryLaterException();
}
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return false;
return e instanceof RetryLaterException;
}
@Override
public void onFailure() {}
private void handleIndividualRecipient(Recipient recipient) throws IOException {
if (recipient.hasServiceIdentifier()) handlePhoneNumberRecipient(recipient);
else Log.w(TAG, "Skipping fetching profile of non-Signal recipient");
}
private void handlePhoneNumberRecipient(Recipient recipient) throws IOException {
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, recipient, getRequestType(recipient));
private void process(Recipient recipient, ProfileAndCredential profileAndCredential) throws IOException {
SignalServiceProfile profile = profileAndCredential.getProfile();
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (recipientProfileKey == null) {
Log.i(TAG, "No profile key available for " + recipient.getId());
} else {
Log.i(TAG, "Profile key available for " + recipient.getId());
}
setProfileName(recipient, profile.getName());
setProfileAvatar(recipient, profile.getAvatar());
if (FeatureFlags.usernames()) setUsername(recipient, profile.getUsername());
@@ -150,14 +253,6 @@ public class RetrieveProfileJob extends BaseJob {
: SignalServiceProfile.RequestType.PROFILE;
}
private void handleGroupRecipient(Recipient group) throws IOException {
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
for (Recipient recipient : recipients) {
handleIndividualRecipient(recipient);
}
}
private void setIdentityKey(Recipient recipient, String identityKeyValue) {
try {
if (TextUtils.isEmpty(identityKeyValue)) {
@@ -222,7 +317,7 @@ public class RetrieveProfileJob extends BaseJob {
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);
if (!Util.equals(plaintextProfileName, recipient.getProfileName().serialize())) {
if (!Objects.equals(plaintextProfileName, recipient.getProfileName().serialize())) {
Log.i(TAG, "Profile name updated. Writing new value.");
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), ProfileName.fromSerialized(plaintextProfileName));
}
@@ -230,7 +325,9 @@ public class RetrieveProfileJob extends BaseJob {
if (TextUtils.isEmpty(plaintextProfileName)) {
Log.i(TAG, "No profile name set.");
}
} catch (InvalidCiphertextException | IOException e) {
} catch (InvalidCiphertextException e) {
Log.w(TAG, "Bad profile key for " + recipient.getId());
} catch (IOException e) {
Log.w(TAG, e);
}
}
@@ -240,8 +337,6 @@ public class RetrieveProfileJob extends BaseJob {
if (!Util.equals(profileAvatar, recipient.getProfileAvatar())) {
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(recipient, profileAvatar));
} else {
Log.d(TAG, "Skipping avatar fetch for " + recipient.getId());
}
}
@@ -261,7 +356,10 @@ public class RetrieveProfileJob extends BaseJob {
@Override
public @NonNull RetrieveProfileJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RetrieveProfileJob(parameters, RecipientId.from(data.getString(KEY_RECIPIENT)));
String[] ids = data.getStringArray(KEY_RECIPIENTS);
List<RecipientId> recipientIds = Stream.of(ids).map(RecipientId::from).toList();
return new RetrieveProfileJob(parameters, recipientIds);
}
}
}

View File

@@ -56,9 +56,11 @@ public final class ProfileName implements Parcelable {
public @NonNull String serialize() {
if (isGivenNameEmpty()) {
return "";
} else if (familyName.isEmpty()) {
return givenName;
} else {
return String.format("%s\0%s", givenName, familyName);
}
return String.format("%s\0%s", givenName, familyName);
}
@Override

View File

@@ -33,6 +33,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class EditSelfProfileRepository implements EditProfileRepository {
@@ -149,10 +151,10 @@ class EditSelfProfileRepository implements EditProfileRepository {
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).getProfile();
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).get(5, TimeUnit.SECONDS).getProfile();
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
}
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));

View File

@@ -123,16 +123,17 @@ public class IdentityUtil {
long time = System.currentTimeMillis();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.Reader reader = groupDatabase.getGroups();
GroupDatabase.GroupRecord groupRecord;
try (GroupDatabase.Reader reader = groupDatabase.getGroups()) {
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.getMembers().contains(recipientId) && groupRecord.isActive()) {
IncomingTextMessage incoming = new IncomingTextMessage(recipientId, 1, time, time, null, Optional.of(groupRecord.getId()), 0, false);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
while ((groupRecord = reader.getNext()) != null) {
if (groupRecord.getMembers().contains(recipientId) && groupRecord.isActive()) {
IncomingTextMessage incoming = new IncomingTextMessage(recipientId, 1, time, time, null, Optional.of(groupRecord.getId()), 0, false);
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
smsDatabase.insertMessageInbox(groupUpdate);
smsDatabase.insertMessageInbox(groupUpdate);
}
}
}

View File

@@ -26,8 +26,16 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.util.concurrent.CascadingFuture;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Aids in the retrieval and decryption of profiles.
@@ -37,31 +45,44 @@ public final class ProfileUtil {
private ProfileUtil() {
}
private static final String TAG = Log.tag(ProfileUtil.class);
@WorkerThread
public static @NonNull ProfileAndCredential retrieveProfile(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
public static @NonNull ProfileAndCredential retrieveProfileSync(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
{
try {
return retrieveProfile(context, recipient, requestType).get(10, TimeUnit.SECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof PushNetworkException) {
throw (PushNetworkException) e.getCause();
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new PushNetworkException(e);
}
}
public static @NonNull ListenableFuture<ProfileAndCredential> retrieveProfile(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull SignalServiceProfile.RequestType requestType)
{
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(context, recipient);
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
ProfileAndCredential profile;
try {
profile = retrieveProfileInternal(address, profileKey, unidentifiedAccess, requestType);
} catch (NonSuccessfulResponseCodeException e) {
if (unidentifiedAccess.isPresent()) {
profile = retrieveProfileInternal(address, profileKey, Optional.absent(), requestType);
} else {
throw e;
}
if (unidentifiedAccess.isPresent()) {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
() -> getSocketRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
() -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
() -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
} else {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
() -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
}
return profile;
}
public static @Nullable String decryptName(@NonNull ProfileKey profileKey, @Nullable String encryptedName)
@@ -75,32 +96,30 @@ public final class ProfileUtil {
return new String(profileCipher.decryptName(Base64.decode(encryptedName)));
}
@WorkerThread
private static @NonNull ProfileAndCredential retrieveProfileInternal(@NonNull SignalServiceAddress address,
@NonNull Optional<ProfileKey> profileKey,
@NonNull Optional<UnidentifiedAccess> unidentifiedAccess,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
private static @NonNull ListenableFuture<ProfileAndCredential> getPipeRetrievalFuture(@NonNull SignalServiceAddress address,
@NonNull Optional<ProfileKey> profileKey,
@NonNull Optional<UnidentifiedAccess> unidentifiedAccess,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
{
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() ? unidentifiedPipe
: authPipe;
if (pipe != null) {
try {
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (IOException e) {
Log.w(TAG, "Websocket request failed. Falling back to REST.", e);
}
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
}
throw new IOException("No pipe available!");
}
private static @NonNull ListenableFuture<ProfileAndCredential> getSocketRetrievalFuture(@NonNull SignalServiceAddress address,
@NonNull Optional<ProfileKey> profileKey,
@NonNull Optional<UnidentifiedAccess> unidentifiedAccess,
@NonNull SignalServiceProfile.RequestType requestType)
{
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
try {
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (VerificationFailedException e) {
throw new IOException("Verification Problem", e);
}
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
}
private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {

View File

@@ -145,21 +145,6 @@ public class RecipientIdJobMigrationTest {
new MultiDeviceVerifiedUpdateJob.Factory().create(mock(Job.Parameters.class), converted.getData());
}
@Test
public void migrate_retrieveProfileJob() throws Exception {
JobData testData = new JobData("RetrieveProfileJob", null, new Data.Builder().putString("address", "+16101234567").build());
mockRecipientResolve("+16101234567", 1);
RecipientIdJobMigration subject = new RecipientIdJobMigration(mock(Application.class));
JobData converted = subject.migrate(testData);
assertEquals("RetrieveProfileJob", converted.getFactoryKey());
assertNull(converted.getQueueKey());
assertEquals("1", converted.getData().getString("recipient"));
new RetrieveProfileJob.Factory().create(mock(Job.Parameters.class), converted.getData());
}
@Test
public void migrate_pushGroupSendJob_null() throws Exception {
JobData testData = new JobData("PushGroupSendJob", "someGroupId", new Data.Builder().putString("filter_address", null)

View File

@@ -140,7 +140,7 @@ public final class ProfileNameTest {
String data = name.serialize();
// THEN
assertEquals(data, "Given\0");
assertEquals(data, "Given");
}
@Test