Support additional sync behavior for linked devices.
parent
995569dd50
commit
c702ff676a
|
@ -115,7 +115,7 @@ dependencies {
|
||||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||||
|
|
||||||
implementation 'org.whispersystems:signal-service-android:2.15.1'
|
implementation 'org.whispersystems:signal-service-android:2.15.3'
|
||||||
|
|
||||||
implementation 'org.signal:ringrtc-android:0.1.9'
|
implementation 'org.signal:ringrtc-android:0.1.9'
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
@ -380,6 +382,7 @@ public class CreateProfileActivity extends BaseActionBarActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||||
|
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,9 @@ public class UnidentifiedAccessUtil {
|
||||||
try {
|
try {
|
||||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||||
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() ? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
|
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() && Recipient.self().isUuidSupported()
|
||||||
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context) ;
|
? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
|
||||||
|
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
|
||||||
|
|
||||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||||
|
|
|
@ -16,12 +16,15 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
@ -762,6 +765,50 @@ public class RecipientDatabase extends Database {
|
||||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void applyBlockedUpdate(@NonNull List<SignalServiceAddress> blocked, List<byte[]> groupIds) {
|
||||||
|
List<String> blockedE164 = Stream.of(blocked)
|
||||||
|
.filter(b -> b.getNumber().isPresent())
|
||||||
|
.map(b -> b.getNumber().get())
|
||||||
|
.toList();
|
||||||
|
List<String> blockedUuid = Stream.of(blocked)
|
||||||
|
.filter(b -> b.getUuid().isPresent())
|
||||||
|
.map(b -> b.getUuid().get().toString().toLowerCase())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
ContentValues resetBlocked = new ContentValues();
|
||||||
|
resetBlocked.put(BLOCKED, 0);
|
||||||
|
db.update(TABLE_NAME, resetBlocked, null, null);
|
||||||
|
|
||||||
|
ContentValues setBlocked = new ContentValues();
|
||||||
|
setBlocked.put(BLOCKED, 1);
|
||||||
|
|
||||||
|
for (String e164 : blockedE164) {
|
||||||
|
db.update(TABLE_NAME, setBlocked, PHONE + " = ?", new String[] { e164 });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String uuid : blockedUuid) {
|
||||||
|
db.update(TABLE_NAME, setBlocked, UUID + " = ?", new String[] { uuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> groupIdStrings = Stream.of(groupIds).map(g -> GroupUtil.getEncodedId(g, false)).toList();
|
||||||
|
|
||||||
|
for (String groupId : groupIdStrings) {
|
||||||
|
db.update(TABLE_NAME, setBlocked, GROUP_ID + " = ?", new String[] { groupId });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationDependencies.getRecipientCache().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private int update(@NonNull RecipientId id, ContentValues contentValues) {
|
private int update(@NonNull RecipientId id, ContentValues contentValues) {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
return database.update(TABLE_NAME, contentValues, ID + " = ?", new String[] { id.serialize() });
|
return database.update(TABLE_NAME, contentValues, ID + " = ?", new String[] { id.serialize() });
|
||||||
|
|
|
@ -51,6 +51,7 @@ public final class JobManagerFactories {
|
||||||
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
|
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
|
||||||
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
|
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
|
||||||
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
|
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
|
||||||
|
put(MultiDeviceProfileContentUpdateJob.KEY, new MultiDeviceProfileContentUpdateJob.Factory());
|
||||||
put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory());
|
put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory());
|
||||||
put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory());
|
put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory());
|
||||||
put(MultiDeviceStickerPackOperationJob.KEY, new MultiDeviceStickerPackOperationJob.Factory());
|
put(MultiDeviceStickerPackOperationJob.KEY, new MultiDeviceStickerPackOperationJob.Factory());
|
||||||
|
@ -64,6 +65,7 @@ public final class JobManagerFactories {
|
||||||
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
|
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
|
||||||
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
||||||
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||||
|
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
|
||||||
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||||
put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory());
|
put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory());
|
||||||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
|
public class MultiDeviceProfileContentUpdateJob extends BaseJob {
|
||||||
|
|
||||||
|
public static final String KEY = "MultiDeviceProfileContentUpdateJob";
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(MultiDeviceProfileContentUpdateJob.class);
|
||||||
|
|
||||||
|
public MultiDeviceProfileContentUpdateJob() {
|
||||||
|
this(new Parameters.Builder()
|
||||||
|
.setQueue("MultiDeviceProfileUpdateJob")
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(10)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MultiDeviceProfileContentUpdateJob(@NonNull Parameters parameters) {
|
||||||
|
super(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Data serialize() {
|
||||||
|
return Data.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getFactoryKey() {
|
||||||
|
return KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRun() throws Exception {
|
||||||
|
if (!TextSecurePreferences.isMultiDevice(context)) {
|
||||||
|
Log.i(TAG, "Not multi device, aborting...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||||
|
|
||||||
|
messageSender.sendMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE),
|
||||||
|
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||||
|
return e instanceof PushNetworkException;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCanceled() {
|
||||||
|
Log.w(TAG, "Did not succeed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<MultiDeviceProfileContentUpdateJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull MultiDeviceProfileContentUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new MultiDeviceProfileContentUpdateJob(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,6 +114,8 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
|
@ -268,6 +270,9 @@ public class PushDecryptJob extends BaseJob {
|
||||||
else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(syncMessage.getViewOnceOpen().get(), content.getTimestamp());
|
else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(syncMessage.getViewOnceOpen().get(), content.getTimestamp());
|
||||||
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
|
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
|
||||||
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
|
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
|
||||||
|
else if (syncMessage.getConfiguration().isPresent()) handleSynchronizeConfigurationMessage(syncMessage.getConfiguration().get());
|
||||||
|
else if (syncMessage.getBlockedList().isPresent()) handleSynchronizeBlockedListMessage(syncMessage.getBlockedList().get());
|
||||||
|
else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get());
|
||||||
else Log.w(TAG, "Contains no known sync types...");
|
else Log.w(TAG, "Contains no known sync types...");
|
||||||
} else if (content.getCallMessage().isPresent()) {
|
} else if (content.getCallMessage().isPresent()) {
|
||||||
Log.i(TAG, "Got call message...");
|
Log.i(TAG, "Got call message...");
|
||||||
|
@ -546,6 +551,36 @@ public class PushDecryptJob extends BaseJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleSynchronizeConfigurationMessage(@NonNull ConfigurationMessage configurationMessage) {
|
||||||
|
if (configurationMessage.getReadReceipts().isPresent()) {
|
||||||
|
TextSecurePreferences.setReadReceiptsEnabled(context, configurationMessage.getReadReceipts().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) {
|
||||||
|
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, configurationMessage.getReadReceipts().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configurationMessage.getTypingIndicators().isPresent()) {
|
||||||
|
TextSecurePreferences.setTypingIndicatorsEnabled(context, configurationMessage.getTypingIndicators().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configurationMessage.getLinkPreviews().isPresent()) {
|
||||||
|
TextSecurePreferences.setLinkPreviewsEnabled(context, configurationMessage.getReadReceipts().get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSynchronizeBlockedListMessage(@NonNull BlockedListMessage blockMessage) {
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).applyBlockedUpdate(blockMessage.getAddresses(), blockMessage.getGroupIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType) {
|
||||||
|
if (fetchType == SignalServiceSyncMessage.FetchType.LOCAL_PROFILE) {
|
||||||
|
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Received a fetch message for an unknown type.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
||||||
@NonNull SentTranscriptMessage message)
|
@NonNull SentTranscriptMessage message)
|
||||||
throws StorageFailedException
|
throws StorageFailedException
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||||
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the profile of the local user. Different from {@link RetrieveProfileJob} in that we
|
||||||
|
* have to sometimes look at/set different data stores, and we will *always* do the fetch regardless
|
||||||
|
* of caching.
|
||||||
|
*/
|
||||||
|
public class RefreshOwnProfileJob extends BaseJob {
|
||||||
|
|
||||||
|
public static final String KEY = "RefreshOwnProfileJob";
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(RefreshOwnProfileJob.class);
|
||||||
|
|
||||||
|
public RefreshOwnProfileJob() {
|
||||||
|
this(new Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setQueue("RefreshOwnProfileJob")
|
||||||
|
.setMaxInstances(1)
|
||||||
|
.setMaxAttempts(10)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private RefreshOwnProfileJob(@NonNull Parameters parameters) {
|
||||||
|
super(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Data serialize() {
|
||||||
|
return Data.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getFactoryKey() {
|
||||||
|
return KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRun() throws Exception {
|
||||||
|
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self());
|
||||||
|
|
||||||
|
setProfileName(profile.getName());
|
||||||
|
setProfileAvatar(profile.getAvatar());
|
||||||
|
setProfileCapabilities(profile.getCapabilities());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||||
|
return e instanceof PushNetworkException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCanceled() { }
|
||||||
|
|
||||||
|
private void setProfileName(@Nullable String encryptedName) {
|
||||||
|
try {
|
||||||
|
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
|
||||||
|
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
|
||||||
|
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), plaintextName);
|
||||||
|
TextSecurePreferences.setProfileName(context, plaintextName);
|
||||||
|
} catch (InvalidCiphertextException | IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProfileAvatar(@Nullable String avatar) {
|
||||||
|
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProfileCapabilities(@Nullable SignalServiceProfile.Capabilities capabilities) {
|
||||||
|
if (capabilities == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setUuidSupported(Recipient.self().getId(), capabilities.isUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements Job.Factory<RefreshOwnProfileJob> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull RefreshOwnProfileJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new RefreshOwnProfileJob(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
@ -117,6 +118,10 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
database.setProfileAvatar(recipient.getId(), profileAvatar);
|
database.setProfileAvatar(recipient.getId(), profileAvatar);
|
||||||
|
|
||||||
|
if (recipient.isLocalNumber()) {
|
||||||
|
TextSecurePreferences.setProfileAvatarId(context, Util.getSecureRandom().nextInt());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -22,6 +22,8 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
@ -39,6 +41,10 @@ import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulRespons
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a users profile and sets the appropriate local fields. If fetching the profile of the
|
||||||
|
* local user, use {@link RefreshOwnProfileJob} instead.
|
||||||
|
*/
|
||||||
public class RetrieveProfileJob extends BaseJob {
|
public class RetrieveProfileJob extends BaseJob {
|
||||||
|
|
||||||
public static final String KEY = "RetrieveProfileJob";
|
public static final String KEY = "RetrieveProfileJob";
|
||||||
|
@ -94,25 +100,12 @@ public class RetrieveProfileJob extends BaseJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePhoneNumberRecipient(Recipient recipient) throws IOException {
|
private void handlePhoneNumberRecipient(Recipient recipient) throws IOException {
|
||||||
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
|
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, recipient);
|
||||||
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(recipient);
|
|
||||||
|
|
||||||
SignalServiceProfile profile;
|
|
||||||
|
|
||||||
try {
|
|
||||||
profile = retrieveProfile(address, unidentifiedAccess);
|
|
||||||
} catch (NonSuccessfulResponseCodeException e) {
|
|
||||||
if (unidentifiedAccess.isPresent()) {
|
|
||||||
profile = retrieveProfile(address, Optional.absent());
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIdentityKey(recipient, profile.getIdentityKey());
|
|
||||||
setProfileName(recipient, profile.getName());
|
setProfileName(recipient, profile.getName());
|
||||||
setProfileAvatar(recipient, profile.getAvatar());
|
setProfileAvatar(recipient, profile.getAvatar());
|
||||||
setProfileCapabilities(recipient, profile.getCapabilities());
|
setProfileCapabilities(recipient, profile.getCapabilities());
|
||||||
|
setIdentityKey(recipient, profile.getIdentityKey());
|
||||||
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,26 +117,6 @@ public class RetrieveProfileJob extends BaseJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceProfile retrieveProfile(@NonNull SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
|
|
||||||
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, unidentifiedAccess);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
|
||||||
return receiver.retrieveProfile(address, unidentifiedAccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setIdentityKey(Recipient recipient, String identityKeyValue) {
|
private void setIdentityKey(Recipient recipient, String identityKeyValue) {
|
||||||
try {
|
try {
|
||||||
if (TextUtils.isEmpty(identityKeyValue)) {
|
if (TextUtils.isEmpty(identityKeyValue)) {
|
||||||
|
@ -206,12 +179,7 @@ public class RetrieveProfileJob extends BaseJob {
|
||||||
byte[] profileKey = recipient.getProfileKey();
|
byte[] profileKey = recipient.getProfileKey();
|
||||||
if (profileKey == null) return;
|
if (profileKey == null) return;
|
||||||
|
|
||||||
String plaintextProfileName = null;
|
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);
|
||||||
|
|
||||||
if (profileName != null) {
|
|
||||||
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
|
||||||
plaintextProfileName = new String(profileCipher.decryptName(Base64.decode(profileName)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Util.equals(plaintextProfileName, recipient.getProfileName())) {
|
if (!Util.equals(plaintextProfileName, recipient.getProfileName())) {
|
||||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), plaintextProfileName);
|
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), plaintextProfileName);
|
||||||
|
@ -237,16 +205,6 @@ public class RetrieveProfileJob extends BaseJob {
|
||||||
DatabaseFactory.getRecipientDatabase(context).setUuidSupported(recipient.getId(), capabilities.isUuid());
|
DatabaseFactory.getRecipientDatabase(context).setUuidSupported(recipient.getId(), capabilities.isUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Recipient recipient) {
|
|
||||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
|
||||||
|
|
||||||
if (unidentifiedAccess.isPresent()) {
|
|
||||||
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.absent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Factory implements Job.Factory<RetrieveProfileJob> {
|
public static final class Factory implements Job.Factory<RetrieveProfileJob> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -126,4 +126,9 @@ public final class LiveRecipientCache {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
public synchronized void clear() {
|
||||||
|
recipients.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||||
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||||
|
@ -82,7 +83,7 @@ public class RecipientDetails {
|
||||||
this.profileName = isLocalNumber ? TextSecurePreferences.getProfileName(context) : settings.getProfileName();
|
this.profileName = isLocalNumber ? TextSecurePreferences.getProfileName(context) : settings.getProfileName();
|
||||||
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
|
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
|
||||||
this.registered = settings.getRegistered();
|
this.registered = settings.getRegistered();
|
||||||
this.profileKey = settings.getProfileKey();
|
this.profileKey = isLocalNumber ? ProfileKeyUtil.getProfileKey(context) : settings.getProfileKey();
|
||||||
this.profileAvatar = settings.getProfileAvatar();
|
this.profileAvatar = settings.getProfileAvatar();
|
||||||
this.profileSharing = settings.isProfileSharing();
|
this.profileSharing = settings.isProfileSharing();
|
||||||
this.systemContact = systemContact;
|
this.systemContact = systemContact;
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
|
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||||
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aids in the retrieval and decryption of profiles.
|
||||||
|
*/
|
||||||
|
public class ProfileUtil {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(ProfileUtil.class);
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static SignalServiceProfile retrieveProfile(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
|
||||||
|
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||||
|
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(context, recipient);
|
||||||
|
|
||||||
|
SignalServiceProfile profile;
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile = retrieveProfileInternal(address, unidentifiedAccess);
|
||||||
|
} catch (NonSuccessfulResponseCodeException e) {
|
||||||
|
if (unidentifiedAccess.isPresent()) {
|
||||||
|
profile = retrieveProfileInternal(address, Optional.absent());
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable String decryptName(@NonNull byte[] profileKey, @Nullable String encryptedName)
|
||||||
|
throws InvalidCiphertextException, IOException
|
||||||
|
{
|
||||||
|
if (encryptedName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
||||||
|
return new String(profileCipher.decryptName(Base64.decode(encryptedName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||||
|
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, unidentifiedAccess);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||||
|
return receiver.retrieveProfile(address, unidentifiedAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {
|
||||||
|
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||||
|
|
||||||
|
if (unidentifiedAccess.isPresent()) {
|
||||||
|
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -386,6 +386,10 @@ public class TextSecurePreferences {
|
||||||
return getBooleanPreference(context, LINK_PREVIEWS, true);
|
return getBooleanPreference(context, LINK_PREVIEWS, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setLinkPreviewsEnabled(Context context, boolean enabled) {
|
||||||
|
setBooleanPreference(context, LINK_PREVIEWS, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isGifSearchInGridLayout(Context context) {
|
public static boolean isGifSearchInGridLayout(Context context) {
|
||||||
return getBooleanPreference(context, GIF_GRID_LAYOUT, false);
|
return getBooleanPreference(context, GIF_GRID_LAYOUT, false);
|
||||||
}
|
}
|
||||||
|
@ -607,6 +611,10 @@ public class TextSecurePreferences {
|
||||||
return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false);
|
return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setShowUnidentifiedDeliveryIndicatorsEnabled(Context context, boolean enabled) {
|
||||||
|
setBooleanPreference(context, SHOW_UNIDENTIFIED_DELIVERY_INDICATORS, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isShowUnidentifiedDeliveryIndicatorsEnabled(Context context) {
|
public static boolean isShowUnidentifiedDeliveryIndicatorsEnabled(Context context) {
|
||||||
return getBooleanPreference(context, SHOW_UNIDENTIFIED_DELIVERY_INDICATORS, false);
|
return getBooleanPreference(context, SHOW_UNIDENTIFIED_DELIVERY_INDICATORS, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -375,11 +375,11 @@ dependencyVerification {
|
||||||
['org.whispersystems:signal-protocol-java:2.8.1',
|
['org.whispersystems:signal-protocol-java:2.8.1',
|
||||||
'b19db36839ab008fdccefc7f8c005f2ea43dc7c7298a209bc424e6f9b6d5617b'],
|
'b19db36839ab008fdccefc7f8c005f2ea43dc7c7298a209bc424e6f9b6d5617b'],
|
||||||
|
|
||||||
['org.whispersystems:signal-service-android:2.15.1',
|
['org.whispersystems:signal-service-android:2.15.3',
|
||||||
'27c6cf63393a2d466474e45162ec254c557f6a19e91962d0e4acc870373c41fd'],
|
'24e7a7760f14a261d44b8d76fe6004157f7e09d7898c88ddb501ceabea47b6f6'],
|
||||||
|
|
||||||
['org.whispersystems:signal-service-java:2.15.1',
|
['org.whispersystems:signal-service-java:2.15.3',
|
||||||
'd0023125e232b11ead150bbf38361fa61a3dbf3adc81c2473c673735380db9c2'],
|
'bc6d58924daf7c15e700164ce602e2647260c41820a43837f3f48f3d5be2e963'],
|
||||||
|
|
||||||
['pl.tajchert:waitingdots:0.1.0',
|
['pl.tajchert:waitingdots:0.1.0',
|
||||||
'2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'],
|
'2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'],
|
||||||
|
|
Loading…
Reference in New Issue