mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-05 13:35:26 +01:00
Initial pre-alpha support for sender key.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
|
||||
public final class AppCapabilities {
|
||||
@@ -11,12 +10,13 @@ public final class AppCapabilities {
|
||||
private static final boolean UUID_CAPABLE = false;
|
||||
private static final boolean GV2_CAPABLE = true;
|
||||
private static final boolean GV1_MIGRATION = true;
|
||||
private static final boolean SENDER_KEY = true;
|
||||
|
||||
/**
|
||||
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeGcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
@@ -300,6 +301,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePendingRetryReceiptManager() {
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
|
||||
@@ -72,7 +72,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
|
||||
void onDecryptionFailedLearnMoreClicked();
|
||||
void onChatSessionRefreshLearnMoreClicked();
|
||||
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);
|
||||
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
|
||||
void onJoinGroupCallClicked();
|
||||
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
|
||||
|
||||
@@ -30,7 +30,10 @@ import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
@@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -77,7 +81,10 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
SessionDatabase.TABLE_NAME,
|
||||
SearchDatabase.SMS_FTS_TABLE_NAME,
|
||||
SearchDatabase.MMS_FTS_TABLE_NAME,
|
||||
EmojiSearchDatabase.TABLE_NAME
|
||||
EmojiSearchDatabase.TABLE_NAME,
|
||||
SenderKeyDatabase.TABLE_NAME,
|
||||
SenderKeySharedDatabase.TABLE_NAME,
|
||||
PendingRetryReceiptDatabase.TABLE_NAME
|
||||
);
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
|
||||
+41
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
@@ -212,6 +213,35 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences__internal_sender_key)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_all_state),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sender_key_state),
|
||||
onClick = {
|
||||
clearAllSenderKeyState()
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_clear_shared_state),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sharing_state),
|
||||
onClick = {
|
||||
clearAllSenderKeySharedState()
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_remove_two_person_minimum),
|
||||
summary = DSLSettingsText.from(R.string.preferences__internal_remove_the_requirement_that_you_need),
|
||||
isChecked = state.removeSenderKeyMinimium,
|
||||
onClick = {
|
||||
viewModel.setRemoveSenderKeyMinimum(!state.removeSenderKeyMinimium)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,4 +308,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
ConversationUtil.clearAllShortcuts(requireContext())
|
||||
Toast.makeText(context, "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeyState() {
|
||||
DatabaseFactory.getSenderKeyDatabase(requireContext()).deleteAll()
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeySharedState() {
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,5 +12,6 @@ data class InternalSettingsState(
|
||||
val disableAutoMigrationNotification: Boolean,
|
||||
val forceCensorship: Boolean,
|
||||
val useBuiltInEmojiSet: Boolean,
|
||||
val emojiVersion: EmojiFiles.Version?
|
||||
val emojiVersion: EmojiFiles.Version?,
|
||||
val removeSenderKeyMinimium: Boolean,
|
||||
)
|
||||
|
||||
+7
-1
@@ -65,6 +65,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setRemoveSenderKeyMinimum(enabled: Boolean) {
|
||||
preferenceDataStore.putBoolean(InternalValues.REMOVE_SENDER_KEY_MINIMUM, enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
}
|
||||
@@ -79,7 +84,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
|
||||
forceCensorship = SignalStore.internalValues().forcedCensorship(),
|
||||
useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(),
|
||||
emojiVersion = null
|
||||
emojiVersion = null,
|
||||
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum()
|
||||
)
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
/**
|
||||
* A dialog fragment that shows when you click 'learn more' on a {@link MessageRecord#isBadDecryptType()}.
|
||||
*/
|
||||
public final class BadDecryptLearnMoreDialog extends DialogFragment {
|
||||
|
||||
private static final String TAG = Log.tag(BadDecryptLearnMoreDialog.class);
|
||||
private static final String FRAGMENT_TAG = "BadDecryptLearnMoreDialog";
|
||||
|
||||
private static final String KEY_DISPLAY_NAME = "display_name";
|
||||
private static final String KEY_GROUP_CHAT = "group_chat";
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager, @NonNull String displayName, boolean isGroupChat) {
|
||||
if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) != null) {
|
||||
Log.i(TAG, "Already shown!");
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_DISPLAY_NAME, displayName);
|
||||
args.putBoolean(KEY_GROUP_CHAT, isGroupChat);
|
||||
|
||||
BadDecryptLearnMoreDialog fragment = new BadDecryptLearnMoreDialog();
|
||||
fragment.setArguments(args);
|
||||
fragment.show(fragmentManager, FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(requireContext());
|
||||
|
||||
View view = LayoutInflater.from(requireContext()).inflate(R.layout.bad_decrypt_learn_more_dialog_fragment, null);
|
||||
TextView body = view.findViewById(R.id.bad_decrypt_dialog_body);
|
||||
|
||||
String displayName = requireArguments().getString(KEY_DISPLAY_NAME);
|
||||
boolean isGroup = requireArguments().getBoolean(KEY_GROUP_CHAT);
|
||||
|
||||
if (isGroup) {
|
||||
body.setText(getString(R.string.BadDecryptLearnMoreDialog_couldnt_be_delivered_group, displayName));
|
||||
} else {
|
||||
body.setText(getString(R.string.BadDecryptLearnMoreDialog_couldnt_be_delivered_individual, displayName));
|
||||
}
|
||||
|
||||
dialogBuilder.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null);
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
}
|
||||
@@ -1605,7 +1605,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecryptionFailedLearnMoreClicked() {
|
||||
public void onChatSessionRefreshLearnMoreClicked() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setView(R.layout.decryption_failed_dialog)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> {
|
||||
@@ -1618,6 +1618,13 @@ public class ConversationFragment extends LoggingFragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBadDecryptLearnMoreClicked(@NonNull RecipientId author) {
|
||||
SimpleTask.run(getLifecycle(),
|
||||
() -> Recipient.resolved(author).getDisplayName(requireContext()),
|
||||
name -> BadDecryptLearnMoreDialog.show(getParentFragmentManager(), name, recipient.get().isGroup()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) {
|
||||
if (recipient.isGroup()) {
|
||||
|
||||
+13
-3
@@ -292,14 +292,14 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges());
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isFailedDecryptionType() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isFailedDecryptionType()))
|
||||
} else if (conversationMessage.getMessageRecord().isChatSessionRefresh() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isChatSessionRefresh()))
|
||||
{
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onDecryptionFailedLearnMoreClicked();
|
||||
eventListener.onChatSessionRefreshLearnMoreClicked();
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isIdentityUpdate()) {
|
||||
@@ -370,6 +370,16 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
eventListener.onViewGroupDescriptionChange(conversationRecipient.getGroupId().orNull(), conversationMessage.getMessageRecord().getGroupV2DescriptionUpdate(), isMessageRequestAccepted);
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isBadDecryptType() &&
|
||||
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isBadDecryptType()))
|
||||
{
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onBadDecryptLearnMoreClicked(conversationMessage.getMessageRecord().getRecipient().getId());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
|
||||
@@ -134,17 +134,17 @@ final class MenuState {
|
||||
}
|
||||
|
||||
static boolean isActionMessage(@NonNull MessageRecord messageRecord) {
|
||||
return messageRecord.isGroupAction() ||
|
||||
messageRecord.isCallLog() ||
|
||||
messageRecord.isJoined() ||
|
||||
return messageRecord.isGroupAction() ||
|
||||
messageRecord.isCallLog() ||
|
||||
messageRecord.isJoined() ||
|
||||
messageRecord.isExpirationTimerUpdate() ||
|
||||
messageRecord.isEndSession() ||
|
||||
messageRecord.isIdentityUpdate() ||
|
||||
messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault() ||
|
||||
messageRecord.isProfileChange() ||
|
||||
messageRecord.isEndSession() ||
|
||||
messageRecord.isIdentityUpdate() ||
|
||||
messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault() ||
|
||||
messageRecord.isProfileChange() ||
|
||||
messageRecord.isGroupV1MigrationEvent() ||
|
||||
messageRecord.isFailedDecryptionType() ||
|
||||
messageRecord.isChatSessionRefresh() ||
|
||||
messageRecord.isInMemoryMessageRecord();
|
||||
}
|
||||
|
||||
|
||||
+10
-5
@@ -354,10 +354,13 @@ public final class ConversationListItem extends ConstraintLayout
|
||||
}
|
||||
|
||||
private void setStatusIcons(ThreadRecord thread) {
|
||||
if (!thread.isOutgoing() ||
|
||||
thread.isOutgoingAudioCall() ||
|
||||
thread.isOutgoingVideoCall() ||
|
||||
thread.isVerificationStatusChange())
|
||||
if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) {
|
||||
deliveryStatusIndicator.setNone();
|
||||
alertView.setFailed();
|
||||
} else if (!thread.isOutgoing() ||
|
||||
thread.isOutgoingAudioCall() ||
|
||||
thread.isOutgoingVideoCall() ||
|
||||
thread.isVerificationStatusChange())
|
||||
{
|
||||
deliveryStatusIndicator.setNone();
|
||||
alertView.setNone();
|
||||
@@ -435,7 +438,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_left_the_group), defaultTint);
|
||||
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message), defaultTint);
|
||||
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
|
||||
} else if (SmsDatabase.Types.isChatSessionRefresh(thread.getType())) {
|
||||
UpdateDescription description = UpdateDescription.staticDescription(context.getString(R.string.ThreadRecord_chat_session_refreshed), R.drawable.ic_refresh_16);
|
||||
return emphasisAdded(context, description, defaultTint);
|
||||
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
|
||||
@@ -482,6 +485,8 @@ public final class ConversationListItem extends ConstraintLayout
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint);
|
||||
} else if (SmsDatabase.Types.isProfileChange(thread.getType())) {
|
||||
return emphasisAdded(context, "", defaultTint);
|
||||
} else if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint);
|
||||
} else {
|
||||
ThreadDatabase.Extra extra = thread.getExtra();
|
||||
if (extra != null && extra.isViewOnce()) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalSenderKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public final class SenderKeyUtil {
|
||||
private SenderKeyUtil() {}
|
||||
|
||||
/**
|
||||
* Clears the state for a sender key session we created. It will naturally get re-created when it is next needed, rotating the key.
|
||||
*/
|
||||
public static void rotateOurKey(@NonNull Context context, @NonNull DistributionId distributionId) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
new SignalSenderKeyStore(context).deleteAllFor(Recipient.self().getId(), distributionId);
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(distributionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets when the sender key session was created, or -1 if it doesn't exist.
|
||||
*/
|
||||
public static long getCreateTimeForOurKey(@NonNull Context context, @NonNull DistributionId distributionId) {
|
||||
return DatabaseFactory.getSenderKeyDatabase(context).getCreatedTime(Recipient.self().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID, distributionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all stored state around session keys. Should only really be used when the user is re-registering.
|
||||
*/
|
||||
public static void clearAllState(@NonNull Context context) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
new SignalSenderKeyStore(context).deleteAll();
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.CertificateType;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -32,6 +33,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -63,6 +65,22 @@ public class UnidentifiedAccessUtil {
|
||||
return getAccessFor(context, recipients, true);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static Map<RecipientId, Optional<UnidentifiedAccessPair>> getAccessMapFor(@NonNull Context context, @NonNull List<Recipient> recipients) {
|
||||
List<Optional<UnidentifiedAccessPair>> accessList = getAccessFor(context, recipients, true);
|
||||
|
||||
Iterator<Recipient> recipientIterator = recipients.iterator();
|
||||
Iterator<Optional<UnidentifiedAccessPair>> accessIterator = accessList.iterator();
|
||||
|
||||
Map<RecipientId, Optional<UnidentifiedAccessPair>> accessMap = new HashMap<>(recipients.size());
|
||||
|
||||
while (recipientIterator.hasNext()) {
|
||||
accessMap.put(recipientIterator.next().getId(), accessIterator.next());
|
||||
}
|
||||
|
||||
return accessMap;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean log) {
|
||||
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
|
||||
+28
-2
@@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
|
||||
import org.whispersystems.libsignal.state.IdentityKeyStore;
|
||||
@@ -17,8 +18,11 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceProtocolStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceSessionStore;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SignalProtocolStoreImpl implements SignalServiceProtocolStore {
|
||||
@@ -27,12 +31,14 @@ public class SignalProtocolStoreImpl implements SignalServiceProtocolStore {
|
||||
private final SignedPreKeyStore signedPreKeyStore;
|
||||
private final IdentityKeyStore identityKeyStore;
|
||||
private final SignalServiceSessionStore sessionStore;
|
||||
private final SignalSenderKeyStore senderKeyStore;
|
||||
|
||||
public SignalProtocolStoreImpl(Context context) {
|
||||
this.preKeyStore = new TextSecurePreKeyStore(context);
|
||||
this.signedPreKeyStore = new TextSecurePreKeyStore(context);
|
||||
this.identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
this.sessionStore = new TextSecureSessionStore(context);
|
||||
this.senderKeyStore = new SignalSenderKeyStore(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,6 +91,11 @@ public class SignalProtocolStoreImpl implements SignalServiceProtocolStore {
|
||||
return sessionStore.loadSession(axolotlAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException {
|
||||
return sessionStore.loadExistingSessions(addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getSubDeviceSessions(String number) {
|
||||
return sessionStore.getSubDeviceSessions(number);
|
||||
@@ -142,11 +153,26 @@ public class SignalProtocolStoreImpl implements SignalServiceProtocolStore {
|
||||
|
||||
@Override
|
||||
public void storeSenderKey(SignalProtocolAddress sender, UUID distributionId, SenderKeyRecord record) {
|
||||
|
||||
senderKeyStore.storeSenderKey(sender, distributionId, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SenderKeyRecord loadSenderKey(SignalProtocolAddress sender, UUID distributionId) {
|
||||
return null;
|
||||
return senderKeyStore.loadSenderKey(sender, distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalProtocolAddress> getSenderKeySharedWith(DistributionId distributionId) {
|
||||
return senderKeyStore.getSenderKeySharedWith(distributionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
|
||||
senderKeyStore.markSenderKeySharedWith(distributionId, addresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
|
||||
senderKeyStore.clearSenderKeySharedWith(distributionId, addresses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An implementation of the storage interface used by the protocol layer to store sender keys. For
|
||||
* more details around sender keys, see {@link org.thoughtcrime.securesms.database.SenderKeyDatabase}.
|
||||
*/
|
||||
public final class SignalSenderKeyStore implements SignalServiceSenderKeyStore {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SignalSenderKeyStore(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSenderKey(@NonNull SignalProtocolAddress sender, @NonNull UUID distributionId, @NonNull SenderKeyRecord record) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
RecipientId recipientId = RecipientId.fromExternalPush(sender.getName());
|
||||
DatabaseFactory.getSenderKeyDatabase(context).store(recipientId, sender.getDeviceId(), DistributionId.from(distributionId), record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable SenderKeyRecord loadSenderKey(@NonNull SignalProtocolAddress sender, @NonNull UUID distributionId) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
RecipientId recipientId = RecipientId.fromExternalPush(sender.getName());
|
||||
return DatabaseFactory.getSenderKeyDatabase(context).load(recipientId, sender.getDeviceId(), DistributionId.from(distributionId));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalProtocolAddress> getSenderKeySharedWith(DistributionId distributionId) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
return DatabaseFactory.getSenderKeySharedDatabase(context).getSharedWith(distributionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).markAsShared(distributionId, addresses);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, addresses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all sender key session state for all devices for the provided recipient-distributionId pair.
|
||||
*/
|
||||
public void deleteAllFor(@NonNull RecipientId recipientId, @NonNull DistributionId distributionId) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
DatabaseFactory.getSenderKeyDatabase(context).deleteAllFor(recipientId, distributionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all sender key session state.
|
||||
*/
|
||||
public void deleteAll() {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
DatabaseFactory.getSenderKeyDatabase(context).deleteAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
@@ -73,6 +74,7 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
||||
identityDatabase.saveIdentity(recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
|
||||
IdentityUtil.markIdentityUpdate(context, recipientId);
|
||||
SessionUtil.archiveSiblingSessions(context, address);
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+22
@@ -8,8 +8,10 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase.RecipientDevice;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
@@ -18,6 +20,7 @@ import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TextSecureSessionStore implements SignalServiceSessionStore {
|
||||
|
||||
@@ -44,6 +47,25 @@ public class TextSecureSessionStore implements SignalServiceSessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
List<RecipientDevice> ids = addresses.stream()
|
||||
.map(address -> new RecipientDevice(RecipientId.fromExternalPush(address.getName()), address.getDeviceId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<SessionRecord> sessionRecords = DatabaseFactory.getSessionDatabase(context).load(ids);
|
||||
|
||||
if (sessionRecords.size() != addresses.size()) {
|
||||
String message = "Mismatch! Asked for " + addresses.size() + " sessions, but only found " + sessionRecords.size() + "!";
|
||||
Log.w(TAG, message);
|
||||
throw new NoSessionException(message);
|
||||
}
|
||||
|
||||
return sessionRecords;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSession(@NonNull SignalProtocolAddress address, @NonNull SessionRecord record) {
|
||||
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -41,31 +42,34 @@ public class DatabaseFactory {
|
||||
|
||||
private static volatile DatabaseFactory instance;
|
||||
|
||||
private final SQLCipherOpenHelper databaseHelper;
|
||||
private final SmsDatabase sms;
|
||||
private final MmsDatabase mms;
|
||||
private final AttachmentDatabase attachments;
|
||||
private final MediaDatabase media;
|
||||
private final ThreadDatabase thread;
|
||||
private final MmsSmsDatabase mmsSmsDatabase;
|
||||
private final IdentityDatabase identityDatabase;
|
||||
private final DraftDatabase draftDatabase;
|
||||
private final PushDatabase pushDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final ContactsDatabase contactsDatabase;
|
||||
private final GroupReceiptDatabase groupReceiptDatabase;
|
||||
private final OneTimePreKeyDatabase preKeyDatabase;
|
||||
private final SignedPreKeyDatabase signedPreKeyDatabase;
|
||||
private final SessionDatabase sessionDatabase;
|
||||
private final SearchDatabase searchDatabase;
|
||||
private final StickerDatabase stickerDatabase;
|
||||
private final UnknownStorageIdDatabase storageIdDatabase;
|
||||
private final RemappedRecordsDatabase remappedRecordsDatabase;
|
||||
private final MentionDatabase mentionDatabase;
|
||||
private final PaymentDatabase paymentDatabase;
|
||||
private final ChatColorsDatabase chatColorsDatabase;
|
||||
private final EmojiSearchDatabase emojiSearchDatabase;
|
||||
private final SQLCipherOpenHelper databaseHelper;
|
||||
private final SmsDatabase sms;
|
||||
private final MmsDatabase mms;
|
||||
private final AttachmentDatabase attachments;
|
||||
private final MediaDatabase media;
|
||||
private final ThreadDatabase thread;
|
||||
private final MmsSmsDatabase mmsSmsDatabase;
|
||||
private final IdentityDatabase identityDatabase;
|
||||
private final DraftDatabase draftDatabase;
|
||||
private final PushDatabase pushDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final ContactsDatabase contactsDatabase;
|
||||
private final GroupReceiptDatabase groupReceiptDatabase;
|
||||
private final OneTimePreKeyDatabase preKeyDatabase;
|
||||
private final SignedPreKeyDatabase signedPreKeyDatabase;
|
||||
private final SessionDatabase sessionDatabase;
|
||||
private final SenderKeyDatabase senderKeyDatabase;
|
||||
private final SenderKeySharedDatabase senderKeySharedDatabase;
|
||||
private final PendingRetryReceiptDatabase pendingRetryReceiptDatabase;
|
||||
private final SearchDatabase searchDatabase;
|
||||
private final StickerDatabase stickerDatabase;
|
||||
private final UnknownStorageIdDatabase storageIdDatabase;
|
||||
private final RemappedRecordsDatabase remappedRecordsDatabase;
|
||||
private final MentionDatabase mentionDatabase;
|
||||
private final PaymentDatabase paymentDatabase;
|
||||
private final ChatColorsDatabase chatColorsDatabase;
|
||||
private final EmojiSearchDatabase emojiSearchDatabase;
|
||||
|
||||
public static DatabaseFactory getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
@@ -148,6 +152,18 @@ public class DatabaseFactory {
|
||||
return getInstance(context).sessionDatabase;
|
||||
}
|
||||
|
||||
public static SenderKeyDatabase getSenderKeyDatabase(Context context) {
|
||||
return getInstance(context).senderKeyDatabase;
|
||||
}
|
||||
|
||||
public static SenderKeySharedDatabase getSenderKeySharedDatabase(Context context) {
|
||||
return getInstance(context).senderKeySharedDatabase;
|
||||
}
|
||||
|
||||
public static PendingRetryReceiptDatabase getPendingRetryReceiptDatabase(Context context) {
|
||||
return getInstance(context).pendingRetryReceiptDatabase;
|
||||
}
|
||||
|
||||
public static SearchDatabase getSearchDatabase(Context context) {
|
||||
return getInstance(context).searchDatabase;
|
||||
}
|
||||
@@ -210,31 +226,34 @@ public class DatabaseFactory {
|
||||
DatabaseSecret databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context);
|
||||
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||
|
||||
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
|
||||
this.sms = new SmsDatabase(context, databaseHelper);
|
||||
this.mms = new MmsDatabase(context, databaseHelper);
|
||||
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.media = new MediaDatabase(context, databaseHelper);
|
||||
this.thread = new ThreadDatabase(context, databaseHelper);
|
||||
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
|
||||
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
|
||||
this.draftDatabase = new DraftDatabase(context, databaseHelper);
|
||||
this.pushDatabase = new PushDatabase(context, databaseHelper);
|
||||
this.groupDatabase = new GroupDatabase(context, databaseHelper);
|
||||
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
|
||||
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
|
||||
this.contactsDatabase = new ContactsDatabase(context);
|
||||
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
|
||||
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
|
||||
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
|
||||
this.searchDatabase = new SearchDatabase(context, databaseHelper);
|
||||
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.storageIdDatabase = new UnknownStorageIdDatabase(context, databaseHelper);
|
||||
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
|
||||
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
|
||||
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
|
||||
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
|
||||
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
|
||||
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
|
||||
this.sms = new SmsDatabase(context, databaseHelper);
|
||||
this.mms = new MmsDatabase(context, databaseHelper);
|
||||
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.media = new MediaDatabase(context, databaseHelper);
|
||||
this.thread = new ThreadDatabase(context, databaseHelper);
|
||||
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
|
||||
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
|
||||
this.draftDatabase = new DraftDatabase(context, databaseHelper);
|
||||
this.pushDatabase = new PushDatabase(context, databaseHelper);
|
||||
this.groupDatabase = new GroupDatabase(context, databaseHelper);
|
||||
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
|
||||
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
|
||||
this.contactsDatabase = new ContactsDatabase(context);
|
||||
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
|
||||
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
|
||||
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
|
||||
this.senderKeyDatabase = new SenderKeyDatabase(context, databaseHelper);
|
||||
this.senderKeySharedDatabase = new SenderKeySharedDatabase(context, databaseHelper);
|
||||
this.pendingRetryReceiptDatabase = new PendingRetryReceiptDatabase(context, databaseHelper);
|
||||
this.searchDatabase = new SearchDatabase(context, databaseHelper);
|
||||
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.storageIdDatabase = new UnknownStorageIdDatabase(context, databaseHelper);
|
||||
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
|
||||
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
|
||||
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
|
||||
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
|
||||
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
|
||||
@@ -20,7 +20,9 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
@@ -48,6 +50,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -71,6 +74,7 @@ public final class GroupDatabase extends Database {
|
||||
static final String MMS = "mms";
|
||||
private static final String EXPECTED_V2_ID = "expected_v2_id";
|
||||
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
|
||||
private static final String DISTRIBUTION_ID = "distribution_id";
|
||||
|
||||
|
||||
/* V2 Group columns */
|
||||
@@ -98,15 +102,17 @@ public final class GroupDatabase extends Database {
|
||||
V2_REVISION + " BLOB, " +
|
||||
V2_DECRYPTED_GROUP + " BLOB, " +
|
||||
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
|
||||
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL);";
|
||||
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
|
||||
DISTRIBUTION_ID + " TEXT DEFAULT NULL);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");"
|
||||
};
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON " + TABLE_NAME + "(" + DISTRIBUTION_ID + ")"
|
||||
};
|
||||
|
||||
private static final String[] GROUP_PROJECTION = {
|
||||
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
|
||||
};
|
||||
@@ -256,6 +262,38 @@ public final class GroupDatabase extends Database {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public @NonNull DistributionId getOrCreateDistributionId(@NonNull GroupId.V2 groupId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = GROUP_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(groupId);
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { DISTRIBUTION_ID }, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
Optional<String> serialized = CursorUtil.getString(cursor, DISTRIBUTION_ID);
|
||||
|
||||
if (serialized.isPresent()) {
|
||||
return DistributionId.from(serialized.get());
|
||||
} else {
|
||||
Log.w(TAG, "Missing distributionId! Creating one.");
|
||||
|
||||
DistributionId distributionId = DistributionId.create();
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(DISTRIBUTION_ID, distributionId.toString());
|
||||
|
||||
int count = db.update(TABLE_NAME, values, query, args);
|
||||
if (count < 1) {
|
||||
throw new IllegalStateException("Tried to create a distributionId for " + groupId + ", but it doesn't exist!");
|
||||
}
|
||||
|
||||
return distributionId;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Group " + groupId + " doesn't exist!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GroupId.Mms getOrCreateMmsGroupForMembers(List<RecipientId> members) {
|
||||
Collections.sort(members);
|
||||
|
||||
@@ -436,6 +474,7 @@ public final class GroupDatabase extends Database {
|
||||
|
||||
if (groupId.isV2()) {
|
||||
contentValues.put(ACTIVE, groupState != null && gv2GroupActive(groupState) ? 1 : 0);
|
||||
contentValues.put(DISTRIBUTION_ID, DistributionId.create().toString());
|
||||
} else if (groupId.isV1()) {
|
||||
contentValues.put(ACTIVE, 1);
|
||||
contentValues.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString());
|
||||
@@ -524,6 +563,7 @@ public final class GroupDatabase extends Database {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(GROUP_ID, groupIdV2.toString());
|
||||
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
|
||||
contentValues.put(DISTRIBUTION_ID, DistributionId.create().toString());
|
||||
contentValues.putNull(EXPECTED_V2_ID);
|
||||
|
||||
List<RecipientId> newMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()));
|
||||
@@ -596,6 +636,18 @@ public final class GroupDatabase extends Database {
|
||||
contentValues.put(MEMBERS, RecipientId.toSerializedList(groupMembers));
|
||||
contentValues.put(ACTIVE, gv2GroupActive(decryptedGroup) ? 1 : 0);
|
||||
|
||||
DistributionId distributionId = Objects.requireNonNull(existingGroup.get().getDistributionId());
|
||||
|
||||
if (existingGroup.isPresent() && existingGroup.get().isV2Group()) {
|
||||
DecryptedGroupChange change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().getDecryptedGroup(), decryptedGroup);
|
||||
List<UUID> removed = DecryptedGroupUtil.removedMembersUuidList(change);
|
||||
|
||||
if (removed.size() > 0) {
|
||||
Log.i(TAG, removed.size() + " members were removed from group " + groupId + ". Rotating the sender key.");
|
||||
SenderKeyUtil.rotateOurKey(context, distributionId);
|
||||
}
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
|
||||
GROUP_ID + " = ?",
|
||||
new String[]{ groupId.toString() });
|
||||
@@ -604,7 +656,7 @@ public final class GroupDatabase extends Database {
|
||||
recipientDatabase.setExpireMessages(groupRecipientId, decryptedGroup.getDisappearingMessagesTimer().getDuration());
|
||||
}
|
||||
|
||||
if (groupMembers != null && (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing())) {
|
||||
if (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing()) {
|
||||
recipientDatabase.setHasGroupsInCommon(groupMembers);
|
||||
}
|
||||
|
||||
@@ -735,7 +787,7 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
|
||||
|
||||
private static List<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
|
||||
private static @NonNull List<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
|
||||
List<RecipientId> groupMembers = new ArrayList<>(uuids.size());
|
||||
|
||||
for (UUID uuid : uuids) {
|
||||
@@ -751,11 +803,9 @@ public final class GroupDatabase extends Database {
|
||||
return groupMembers;
|
||||
}
|
||||
|
||||
private static List<RecipientId> getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
|
||||
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
|
||||
List<RecipientId> recipientIds = uuidsToRecipientIds(uuids);
|
||||
|
||||
return recipientIds;
|
||||
private static @NonNull List<RecipientId> getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
|
||||
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
|
||||
return uuidsToRecipientIds(uuids);
|
||||
}
|
||||
|
||||
public @NonNull List<GroupId.V2> getAllGroupV2Ids() {
|
||||
@@ -830,7 +880,8 @@ public final class GroupDatabase extends Database {
|
||||
CursorUtil.requireBoolean(cursor, MMS),
|
||||
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
|
||||
CursorUtil.requireInt(cursor, V2_REVISION),
|
||||
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP));
|
||||
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
|
||||
CursorUtil.getString(cursor, DISTRIBUTION_ID).transform(DistributionId::from).orNull());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -855,6 +906,7 @@ public final class GroupDatabase extends Database {
|
||||
private final boolean active;
|
||||
private final boolean mms;
|
||||
@Nullable private final V2GroupProperties v2GroupProperties;
|
||||
private final DistributionId distributionId;
|
||||
|
||||
public GroupRecord(@NonNull GroupId id,
|
||||
@NonNull RecipientId recipientId,
|
||||
@@ -870,7 +922,8 @@ public final class GroupDatabase extends Database {
|
||||
boolean mms,
|
||||
@Nullable byte[] groupMasterKeyBytes,
|
||||
int groupRevision,
|
||||
@Nullable byte[] decryptedGroupBytes)
|
||||
@Nullable byte[] decryptedGroupBytes,
|
||||
@Nullable DistributionId distributionId)
|
||||
{
|
||||
this.id = id;
|
||||
this.recipientId = recipientId;
|
||||
@@ -882,6 +935,7 @@ public final class GroupDatabase extends Database {
|
||||
this.relay = relay;
|
||||
this.active = active;
|
||||
this.mms = mms;
|
||||
this.distributionId = distributionId;
|
||||
|
||||
V2GroupProperties v2GroupProperties = null;
|
||||
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
|
||||
@@ -969,6 +1023,10 @@ public final class GroupDatabase extends Database {
|
||||
return mms;
|
||||
}
|
||||
|
||||
public @Nullable DistributionId getDistributionId() {
|
||||
return distributionId;
|
||||
}
|
||||
|
||||
public boolean isV1Group() {
|
||||
return !mms && !isV2Group();
|
||||
}
|
||||
|
||||
@@ -9,12 +9,16 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class GroupReceiptDatabase extends Database {
|
||||
|
||||
public static final String TABLE_NAME = "group_receipts";
|
||||
@@ -109,6 +113,23 @@ public class GroupReceiptDatabase extends Database {
|
||||
return results;
|
||||
}
|
||||
|
||||
public @Nullable GroupReceiptInfo getGroupReceiptInfo(long mmsId, @NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = MMS_ID + " = ? AND " + RECIPIENT_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(mmsId, recipientId);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, "1")) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return new GroupReceiptInfo(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void deleteRowsForMessage(long mmsId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
||||
|
||||
@@ -158,7 +158,8 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException;
|
||||
public abstract Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId);
|
||||
public abstract Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException;
|
||||
public abstract @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp);
|
||||
public abstract @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp);
|
||||
public abstract void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId);
|
||||
public abstract long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener);
|
||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||
|
||||
@@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
|
||||
@@ -1443,7 +1442,12 @@ public class MmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
|
||||
public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ public interface MmsSmsColumns {
|
||||
protected static final long INCOMING_VIDEO_CALL_TYPE = 10;
|
||||
protected static final long OUTGOING_VIDEO_CALL_TYPE = 11;
|
||||
protected static final long GROUP_CALL_TYPE = 12;
|
||||
protected static final long BAD_DECRYPT_TYPE = 13;
|
||||
|
||||
protected static final long BASE_INBOX_TYPE = 20;
|
||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||
@@ -196,6 +197,10 @@ public interface MmsSmsColumns {
|
||||
return (type & BASE_TYPE_MASK) == INVALID_MESSAGE_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isBadDecryptType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BAD_DECRYPT_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isSecureType(long type) {
|
||||
return (type & SECURE_MESSAGE_BIT) != 0;
|
||||
}
|
||||
@@ -298,7 +303,7 @@ public interface MmsSmsColumns {
|
||||
return (type & GROUP_QUIT_BIT) != 0;
|
||||
}
|
||||
|
||||
public static boolean isFailedDecryptType(long type) {
|
||||
public static boolean isChatSessionRefresh(long type) {
|
||||
return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0;
|
||||
}
|
||||
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
|
||||
/**
|
||||
* Holds information about messages we've sent out retry receipts for.
|
||||
*/
|
||||
public final class PendingRetryReceiptDatabase extends Database {
|
||||
|
||||
public static final String TABLE_NAME = "pending_retry_receipts";
|
||||
|
||||
private static final String ID = "_id";
|
||||
private static final String AUTHOR = "author";
|
||||
private static final String DEVICE = "device";
|
||||
private static final String SENT_TIMESTAMP = "sent_timestamp";
|
||||
private static final String RECEIVED_TIMESTAMP = "received_timestamp";
|
||||
private static final String THREAD_ID = "thread_id";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
AUTHOR + " TEXT NOT NULL, " +
|
||||
DEVICE + " INTEGER NOT NULL, " +
|
||||
SENT_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||
RECEIVED_TIMESTAMP + " TEXT NOT NULL, " +
|
||||
THREAD_ID + " INTEGER NOT NULL, " +
|
||||
"UNIQUE(" + AUTHOR + "," + SENT_TIMESTAMP + ") ON CONFLICT REPLACE);";
|
||||
|
||||
PendingRetryReceiptDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void insert(@NonNull RecipientId author, int authorDevice, long sentTimestamp, long receivedTimestamp, long threadId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(AUTHOR, author.serialize());
|
||||
values.put(DEVICE, authorDevice);
|
||||
values.put(SENT_TIMESTAMP, sentTimestamp);
|
||||
values.put(RECEIVED_TIMESTAMP, receivedTimestamp);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
||||
databaseHelper.getWritableDatabase().insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public @Nullable PendingRetryReceiptModel get(@NonNull RecipientId author, long sentTimestamp) {
|
||||
String query = AUTHOR + " = ? AND " + SENT_TIMESTAMP + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(author, sentTimestamp);
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return fromCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable PendingRetryReceiptModel getOldest() {
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, RECEIVED_TIMESTAMP + " ASC", "1")) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return fromCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void delete(long id) {
|
||||
databaseHelper.getWritableDatabase().delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(id));
|
||||
}
|
||||
|
||||
|
||||
private static @NonNull PendingRetryReceiptModel fromCursor(@NonNull Cursor cursor) {
|
||||
return new PendingRetryReceiptModel(CursorUtil.requireLong(cursor, ID),
|
||||
RecipientId.from(CursorUtil.requireString(cursor, AUTHOR)),
|
||||
CursorUtil.requireInt(cursor, DEVICE),
|
||||
CursorUtil.requireLong(cursor, SENT_TIMESTAMP),
|
||||
CursorUtil.requireLong(cursor, RECEIVED_TIMESTAMP),
|
||||
CursorUtil.requireLong(cursor, THREAD_ID));
|
||||
}
|
||||
}
|
||||
@@ -156,11 +156,16 @@ public class RecipientDatabase extends Database {
|
||||
private static final String IDENTITY_STATUS = "identity_status";
|
||||
private static final String IDENTITY_KEY = "identity_key";
|
||||
|
||||
/**
|
||||
* Values that represent the index in the capabilities bitmask. Each index can store a 2-bit
|
||||
* value, which in this case is the value of {@link Recipient.Capability}.
|
||||
*/
|
||||
private static final class Capabilities {
|
||||
static final int BIT_LENGTH = 2;
|
||||
|
||||
static final int GROUPS_V2 = 0;
|
||||
static final int GROUPS_V1_MIGRATION = 1;
|
||||
static final int SENDER_KEY = 2;
|
||||
}
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
@@ -1632,6 +1637,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
|
||||
value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize());
|
||||
value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey()).serialize());
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(CAPABILITIES, value);
|
||||
@@ -3137,6 +3143,7 @@ public class RecipientDatabase extends Database {
|
||||
private final long capabilities;
|
||||
private final Recipient.Capability groupsV2Capability;
|
||||
private final Recipient.Capability groupsV1MigrationCapability;
|
||||
private final Recipient.Capability senderKeyCapability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageId;
|
||||
private final MentionSetting mentionSetting;
|
||||
@@ -3145,9 +3152,9 @@ public class RecipientDatabase extends Database {
|
||||
private final AvatarColor avatarColor;
|
||||
private final String about;
|
||||
private final String aboutEmoji;
|
||||
private final SyncExtras syncExtras;
|
||||
private final Recipient.Extras extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
private final SyncExtras syncExtras;
|
||||
private final Recipient.Extras extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
|
||||
RecipientSettings(@NonNull RecipientId id,
|
||||
@Nullable UUID uuid,
|
||||
@@ -3227,6 +3234,7 @@ public class RecipientDatabase extends Database {
|
||||
this.capabilities = capabilities;
|
||||
this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH));
|
||||
this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH));
|
||||
this.senderKeyCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH));
|
||||
this.insightsBannerTier = insightsBannerTier;
|
||||
this.storageId = storageId;
|
||||
this.mentionSetting = mentionSetting;
|
||||
@@ -3376,6 +3384,10 @@ public class RecipientDatabase extends Database {
|
||||
return groupsV1MigrationCapability;
|
||||
}
|
||||
|
||||
public @NonNull Recipient.Capability getSenderKeyCapability() {
|
||||
return senderKeyCapability;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageId() {
|
||||
return storageId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.groups.state.SenderKeyRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Stores all of the sender keys -- both the ones we create, and the ones we're told about.
|
||||
*
|
||||
* When working with SenderKeys, keep this in mind: they're not *really* keys. They're sessions.
|
||||
* The name is largely historical, and there's too much momentum to change it.
|
||||
*/
|
||||
public class SenderKeyDatabase extends Database {
|
||||
|
||||
private static final String TAG = Log.tag(SenderKeyDatabase.class);
|
||||
|
||||
public static final String TABLE_NAME = "sender_keys";
|
||||
|
||||
private static final String ID = "_id";
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String DEVICE = "device";
|
||||
public static final String DISTRIBUTION_ID = "distribution_id";
|
||||
public static final String RECORD = "record";
|
||||
public static final String CREATED_AT = "created_at";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
RECIPIENT_ID + " INTEGER NOT NULL, " +
|
||||
DEVICE + " INTEGER NOT NULL, " +
|
||||
DISTRIBUTION_ID + " TEXT NOT NULL, " +
|
||||
RECORD + " BLOB NOT NULL, " +
|
||||
CREATED_AT + " INTEGER NOT NULL, " +
|
||||
"UNIQUE(" + RECIPIENT_ID + "," + DEVICE + ", " + DISTRIBUTION_ID + ") ON CONFLICT REPLACE);";
|
||||
|
||||
SenderKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void store(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId, @NonNull SenderKeyRecord record) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(DEVICE, deviceId);
|
||||
values.put(DISTRIBUTION_ID, distributionId.toString());
|
||||
values.put(RECORD, record.serialize());
|
||||
values.put(CREATED_AT, System.currentTimeMillis());
|
||||
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public @Nullable SenderKeyRecord load(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ? AND " + DISTRIBUTION_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(recipientId, deviceId, distributionId);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ RECORD }, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
try {
|
||||
return new SenderKeyRecord(CursorUtil.requireBlob(cursor, RECORD));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets when the sender key session was created, or -1 if it doesn't exist.
|
||||
*/
|
||||
public long getCreatedTime(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ? AND " + DISTRIBUTION_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(recipientId, deviceId, distributionId);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ CREATED_AT }, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return CursorUtil.requireLong(cursor, CREATED_AT);
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all sender key session state for all devices for the provided recipient-distributionId pair.
|
||||
*/
|
||||
public void deleteAllFor(@NonNull RecipientId recipientId, @NonNull DistributionId distributionId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String query = RECIPIENT_ID + " = ? AND " + DISTRIBUTION_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(recipientId, distributionId);
|
||||
|
||||
db.delete(TABLE_NAME, query, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all database state.
|
||||
*/
|
||||
public void deleteAll() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Keeps track of which recipients are aware of which distributionIds. For the storage of sender
|
||||
* keys themselves, see {@link SenderKeyDatabase}.
|
||||
*/
|
||||
public class SenderKeySharedDatabase extends Database {
|
||||
|
||||
private static final String TAG = Log.tag(SenderKeySharedDatabase.class);
|
||||
|
||||
public static final String TABLE_NAME = "sender_key_shared";
|
||||
|
||||
private static final String ID = "_id";
|
||||
public static final String DISTRIBUTION_ID = "distribution_id";
|
||||
public static final String ADDRESS = "address";
|
||||
public static final String DEVICE = "device";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
DISTRIBUTION_ID + " TEXT NOT NULL, " +
|
||||
ADDRESS + " TEXT NOT NULL, " +
|
||||
DEVICE + " INTEGER NOT NULL, " +
|
||||
"UNIQUE(" + DISTRIBUTION_ID + "," + ADDRESS + ", " + DEVICE + ") ON CONFLICT REPLACE);";
|
||||
|
||||
SenderKeySharedDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that a distributionId has been shared with the provided recipients
|
||||
*/
|
||||
public void markAsShared(@NonNull DistributionId distributionId, @NonNull Collection<SignalProtocolAddress> addresses) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (SignalProtocolAddress address : addresses) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ADDRESS, address.getName());
|
||||
values.put(DEVICE, address.getDeviceId());
|
||||
values.put(DISTRIBUTION_ID, distributionId.toString());
|
||||
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of recipientIds that know about the distributionId in question.
|
||||
*/
|
||||
public @NonNull Set<SignalProtocolAddress> getSharedWith(@NonNull DistributionId distributionId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = DISTRIBUTION_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(distributionId);
|
||||
|
||||
Set<SignalProtocolAddress> addresses = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ADDRESS, DEVICE }, query, args, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
String address = CursorUtil.requireString(cursor, ADDRESS);
|
||||
int device = CursorUtil.requireInt(cursor, DEVICE);
|
||||
|
||||
addresses.add(new SignalProtocolAddress(address, device));
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the shared statuses for all provided addresses.
|
||||
*/
|
||||
public void delete(@NonNull DistributionId distributionId, @NonNull Collection<SignalProtocolAddress> addresses) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String query = DISTRIBUTION_ID + " = ? AND " + ADDRESS + " = ? AND " + DEVICE + " = ?";
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (SignalProtocolAddress address : addresses) {
|
||||
db.delete(TABLE_NAME, query, SqlUtil.buildArgs(distributionId, address.getName(), address.getDeviceId()));
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all shared statuses for a given distributionId.
|
||||
*/
|
||||
public void deleteAllFor(@NonNull DistributionId distributionId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, DISTRIBUTION_ID + " = ?", SqlUtil.buildArgs(distributionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all shared statuses for a given recipientId.
|
||||
*/
|
||||
public void deleteAllFor(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (recipient.hasUuid()) {
|
||||
db.delete(TABLE_NAME, ADDRESS + " = ?", SqlUtil.buildArgs(recipient.getUuid().get().toString()));
|
||||
} else {
|
||||
Log.w(TAG, "Recipient doesn't have a UUID! " + recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all database content.
|
||||
*/
|
||||
public void deleteAll() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,13 @@ import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -72,6 +72,36 @@ public class SessionDatabase extends Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull List<SessionRecord> load(@NonNull List<RecipientDevice> ids) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<SessionRecord> sessions = new ArrayList<>(ids.size());
|
||||
|
||||
database.beginTransaction();
|
||||
try {
|
||||
String[] projection = new String[]{RECORD};
|
||||
String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ?";
|
||||
|
||||
for (RecipientDevice id : ids) {
|
||||
String[] args = SqlUtil.buildArgs(id.getRecipientId(), id.getDevice());
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, projection, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
try {
|
||||
sessions.add(new SessionRecord(cursor.getBlob(cursor.getColumnIndexOrThrow(RECORD))));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
public @NonNull List<SessionRow> getAllFor(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<SessionRow> results = new LinkedList<>();
|
||||
@@ -180,4 +210,22 @@ public class SessionDatabase extends Database {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RecipientDevice {
|
||||
private final RecipientId recipientId;
|
||||
private final int device;
|
||||
|
||||
public RecipientDevice(@NonNull RecipientId recipientId, int device) {
|
||||
this.recipientId = recipientId;
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public int getDevice() {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,7 +1156,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
|
||||
public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.resolved(recipientId));
|
||||
long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT;
|
||||
@@ -1185,6 +1185,28 @@ public class SmsDatabase extends MessageDatabase {
|
||||
return new InsertResult(messageId, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, senderDevice);
|
||||
values.put(DATE_SENT, sentTimestamp);
|
||||
values.put(DATE_RECEIVED, receivedTimestamp);
|
||||
values.put(DATE_SERVER, -1);
|
||||
values.put(READ, 0);
|
||||
values.put(TYPE, Types.BAD_DECRYPT_TYPE);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
|
||||
boolean forceSms, long date, InsertListener insertListener)
|
||||
|
||||
+47
-1
@@ -40,10 +40,13 @@ import org.thoughtcrime.securesms.database.MentionDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
@@ -73,6 +76,7 @@ import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Triple;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
@@ -192,8 +196,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
private static final int CHAT_COLORS = 100;
|
||||
private static final int AVATAR_COLORS = 101;
|
||||
private static final int EMOJI_SEARCH = 102;
|
||||
private static final int SENDER_KEY = 103;
|
||||
|
||||
private static final int DATABASE_VERSION = 102;
|
||||
private static final int DATABASE_VERSION = 103;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -221,6 +226,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
db.execSQL(OneTimePreKeyDatabase.CREATE_TABLE);
|
||||
db.execSQL(SignedPreKeyDatabase.CREATE_TABLE);
|
||||
db.execSQL(SessionDatabase.CREATE_TABLE);
|
||||
db.execSQL(SenderKeyDatabase.CREATE_TABLE);
|
||||
db.execSQL(SenderKeySharedDatabase.CREATE_TABLE);
|
||||
db.execSQL(PendingRetryReceiptDatabase.CREATE_TABLE);
|
||||
db.execSQL(StickerDatabase.CREATE_TABLE);
|
||||
db.execSQL(UnknownStorageIdDatabase.CREATE_TABLE);
|
||||
db.execSQL(MentionDatabase.CREATE_TABLE);
|
||||
@@ -1513,6 +1521,44 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
||||
db.execSQL("CREATE VIRTUAL TABLE emoji_search USING fts5(label, emoji UNINDEXED)");
|
||||
}
|
||||
|
||||
if (oldVersion < SENDER_KEY && !SqlUtil.tableExists(db, "sender_keys")) {
|
||||
db.execSQL("CREATE TABLE sender_keys (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"recipient_id INTEGER NOT NULL, " +
|
||||
"device INTEGER NOT NULL, " +
|
||||
"distribution_id TEXT NOT NULL, " +
|
||||
"record BLOB NOT NULL, " +
|
||||
"created_at INTEGER NOT NULL, " +
|
||||
"UNIQUE(recipient_id, device, distribution_id) ON CONFLICT REPLACE)");
|
||||
|
||||
db.execSQL("CREATE TABLE sender_key_shared (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"distribution_id TEXT NOT NULL, " +
|
||||
"address TEXT NOT NULL, " +
|
||||
"device INTEGER NOT NULL, " +
|
||||
"UNIQUE(distribution_id, address, device) ON CONFLICT REPLACE)");
|
||||
|
||||
db.execSQL("CREATE TABLE pending_retry_receipts (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"author TEXT NOT NULL, " +
|
||||
"device INTEGER NOT NULL, " +
|
||||
"sent_timestamp INTEGER NOT NULL, " +
|
||||
"received_timestamp TEXT NOT NULL, " +
|
||||
"thread_id INTEGER NOT NULL, " +
|
||||
"UNIQUE(author, sent_timestamp) ON CONFLICT REPLACE);");
|
||||
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN distribution_id TEXT DEFAULT NULL");
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON groups (distribution_id)");
|
||||
|
||||
try (Cursor cursor = db.query("groups", new String[] { "group_id" }, "LENGTH(group_id) = 85", null, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
String groupId = cursor.getString(cursor.getColumnIndexOrThrow("group_id"));
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put("distribution_id", DistributionId.create().toString());
|
||||
|
||||
db.update("groups", values, "group_id = ?", new String[] { groupId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (MmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
if (MmsDatabase.Types.isChatSessionRefresh(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
|
||||
} else if (MmsDatabase.Types.isDuplicateMessageType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
|
||||
|
||||
@@ -57,7 +57,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -191,8 +190,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_16);
|
||||
} else if (isGroupV1MigrationEvent()) {
|
||||
return getGroupMigrationEventDescription(context);
|
||||
} else if (isFailedDecryptionType()) {
|
||||
} else if (isChatSessionRefresh()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_chat_session_refreshed), R.drawable.ic_refresh_16);
|
||||
} else if (isBadDecryptType()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_a_message_from_s_couldnt_be_delivered, r.getDisplayName(context)), R.drawable.ic_error_outline_14);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -458,6 +459,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return SmsDatabase.Types.isCorruptedKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isBadDecryptType() {
|
||||
return MmsSmsColumns.Types.isBadDecryptType(type);
|
||||
}
|
||||
|
||||
public boolean isInvalidVersionKeyExchange() {
|
||||
return SmsDatabase.Types.isInvalidVersionKeyExchange(type);
|
||||
}
|
||||
@@ -476,8 +481,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
|
||||
public boolean isUpdate() {
|
||||
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
|
||||
isProfileChange() || isGroupV1MigrationEvent() || isFailedDecryptionType();
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
|
||||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType();
|
||||
}
|
||||
|
||||
public boolean isMediaPending() {
|
||||
@@ -512,8 +517,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
|
||||
}
|
||||
|
||||
public boolean isFailedDecryptionType() {
|
||||
return MmsSmsColumns.Types.isFailedDecryptType(type);
|
||||
public boolean isChatSessionRefresh() {
|
||||
return MmsSmsColumns.Types.isChatSessionRefresh(type);
|
||||
}
|
||||
|
||||
public boolean isInMemoryMessageRecord() {
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/** A model for [org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase] */
|
||||
data class PendingRetryReceiptModel(
|
||||
val id: Long,
|
||||
val author: RecipientId,
|
||||
val authorDevice: Int,
|
||||
val sentTimestamp: Long,
|
||||
val receivedTimestamp: Long,
|
||||
val threadId: Long
|
||||
)
|
||||
@@ -65,7 +65,7 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody(@NonNull Context context) {
|
||||
if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
if (SmsDatabase.Types.isChatSessionRefresh(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_chat_session_refreshed));
|
||||
} else if (isCorruptedKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message));
|
||||
|
||||
+16
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache;
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
@@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.shakereport.ShakeToReport;
|
||||
@@ -87,6 +89,7 @@ public class ApplicationDependencies {
|
||||
private static volatile SignalCallManager signalCallManager;
|
||||
private static volatile ShakeToReport shakeToReport;
|
||||
private static volatile OkHttpClient okHttpClient;
|
||||
private static volatile PendingRetryReceiptManager pendingRetryReceiptManager;
|
||||
|
||||
@MainThread
|
||||
public static void init(@NonNull Application application, @NonNull Provider provider) {
|
||||
@@ -362,6 +365,18 @@ public class ApplicationDependencies {
|
||||
return viewOnceMessageManager;
|
||||
}
|
||||
|
||||
public static @NonNull PendingRetryReceiptManager getPendingRetryReceiptManager() {
|
||||
if (pendingRetryReceiptManager == null) {
|
||||
synchronized (LOCK) {
|
||||
if (pendingRetryReceiptManager == null) {
|
||||
pendingRetryReceiptManager = provider.providePendingRetryReceiptManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pendingRetryReceiptManager;
|
||||
}
|
||||
|
||||
public static @NonNull ExpiringMessageManager getExpiringMessageManager() {
|
||||
if (expiringMessageManager == null) {
|
||||
synchronized (LOCK) {
|
||||
@@ -492,5 +507,6 @@ public class ApplicationDependencies {
|
||||
@NonNull ShakeToReport provideShakeToReport();
|
||||
@NonNull AppForegroundObserver provideAppForegroundObserver();
|
||||
@NonNull SignalCallManager provideSignalCallManager();
|
||||
@NonNull PendingRetryReceiptManager providePendingRetryReceiptManager();
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.shakereport.ShakeToReport;
|
||||
@@ -251,6 +252,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
return new SignalCallManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull PendingRetryReceiptManager providePendingRetryReceiptManager() {
|
||||
return new PendingRetryReceiptManager(context);
|
||||
}
|
||||
|
||||
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
@@ -16,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
@@ -87,6 +85,7 @@ public class AutomaticSessionResetJob extends BaseJob {
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
SessionUtil.archiveSession(context, recipientId, deviceId);
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId);
|
||||
insertLocalMessage();
|
||||
|
||||
if (FeatureFlags.automaticSessionReset()) {
|
||||
@@ -122,7 +121,7 @@ public class AutomaticSessionResetJob extends BaseJob {
|
||||
}
|
||||
|
||||
private void insertLocalMessage() {
|
||||
MessageDatabase.InsertResult result = DatabaseFactory.getSmsDatabase(context).insertDecryptionFailedMessage(recipientId, deviceId, sentTimestamp);
|
||||
MessageDatabase.InsertResult result = DatabaseFactory.getSmsDatabase(context).insertChatSessionRefreshedMessage(recipientId, deviceId, sentTimestamp);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, result.getThreadId());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
@@ -161,7 +162,7 @@ public class GroupCallUpdateSendJob extends BaseJob {
|
||||
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
@@ -137,8 +137,10 @@ public final class JobManagerFactories {
|
||||
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
|
||||
put(RotateProfileKeyJob.KEY, new RotateProfileKeyJob.Factory());
|
||||
put(RotateSignedPreKeyJob.KEY, new RotateSignedPreKeyJob.Factory());
|
||||
put(SenderKeyDistributionSendJob.KEY, new SenderKeyDistributionSendJob.Factory());
|
||||
put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory());
|
||||
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application));
|
||||
put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory());
|
||||
put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application));
|
||||
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
|
||||
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
@@ -141,7 +142,7 @@ public class LeaveGroupJob extends BaseJob {
|
||||
.asGroupMessage(serviceGroup);
|
||||
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ public class MultiDeviceBlockedUpdateJob extends BaseJob {
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -95,10 +95,10 @@ public class MultiDeviceConfigurationUpdateJob extends BaseJob {
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled),
|
||||
Optional.of(unidentifiedDeliveryIndicatorsEnabled),
|
||||
Optional.of(typingIndicatorsEnabled),
|
||||
Optional.of(linkPreviewsEnabled))),
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled),
|
||||
Optional.of(unidentifiedDeliveryIndicatorsEnabled),
|
||||
Optional.of(typingIndicatorsEnabled),
|
||||
Optional.of(linkPreviewsEnabled))),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
|
||||
@@ -271,8 +271,8 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
||||
.withLength(length)
|
||||
.withResumableUploadSpec(messageSender.getResumableUploadSpec());
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
} catch (IOException ioe) {
|
||||
throw new NetworkException(ioe);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -174,8 +173,8 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
|
||||
attachmentStream = SignalServiceAttachment.emptyStream("application/octet-stream");
|
||||
}
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -114,8 +114,8 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
||||
response = MessageRequestResponseMessage.forIndividual(RecipientUtil.toSignalServiceAddress(context, recipient), localToRemoteType(type));
|
||||
}
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forMessageRequestResponse(response),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
private static MessageRequestResponseMessage.Type localToRemoteType(@NonNull Type type) {
|
||||
|
||||
+2
-2
@@ -114,8 +114,8 @@ public final class MultiDeviceOutgoingPaymentSyncJob extends BaseJob {
|
||||
|
||||
|
||||
ApplicationDependencies.getSignalServiceMessageSender()
|
||||
.sendMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
.sendSyncMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -58,8 +58,8 @@ public class MultiDeviceProfileContentUpdateJob extends BaseJob {
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
|
||||
|
||||
SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, false));
|
||||
|
||||
messageSender.sendMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -120,7 +120,7 @@ public class MultiDeviceReadUpdateJob extends BaseJob {
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forRead(readMessages), UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages), UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -92,8 +92,8 @@ public class MultiDeviceStickerPackOperationJob extends BaseJob {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
StickerPackOperationMessage stickerPackOperation = new StickerPackOperationMessage(packIdBytes, packKeyBytes, remoteType);
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -80,8 +80,8 @@ public class MultiDeviceStickerPackSyncJob extends BaseJob {
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forStickerPackOperations(operations),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(operations),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -58,8 +58,8 @@ public class MultiDeviceStorageSyncRequestJob extends BaseJob {
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST),
|
||||
UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -109,8 +109,8 @@ public class MultiDeviceVerifiedUpdateJob extends BaseJob {
|
||||
SignalServiceAddress verifiedAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
VerifiedMessage verifiedMessage = new VerifiedMessage(verifiedAddress, new IdentityKey(identityKey, 0), verifiedState, timestamp);
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forVerified(verifiedMessage),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, recipient));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, recipient));
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public class MultiDeviceViewOnceOpenJob extends BaseJob {
|
||||
Recipient recipient = Recipient.resolved(RecipientId.from(messageId.recipientId));
|
||||
ViewOnceOpenMessage openMessage = new ViewOnceOpenMessage(RecipientUtil.toSignalServiceAddress(context, recipient), messageId.timestamp);
|
||||
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage), UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage), UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -120,7 +120,7 @@ public class MultiDeviceViewedUpdateJob extends BaseJob {
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
messageSender.sendMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -83,7 +84,7 @@ public final class PaymentNotificationSendJob extends BaseJob {
|
||||
PaymentDatabase paymentDatabase = DatabaseFactory.getPaymentDatabase(context);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
SignalServiceAddress addresses = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
PaymentDatabase.PaymentTransaction payment = paymentDatabase.getPayment(uuid);
|
||||
@@ -102,7 +103,7 @@ public final class PaymentNotificationSendJob extends BaseJob {
|
||||
.withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote())))
|
||||
.build();
|
||||
|
||||
SendMessageResult sendMessageResult = messageSender.sendMessage(addresses, unidentifiedAccess, dataMessage);
|
||||
SendMessageResult sendMessageResult = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.DEFAULT, dataMessage);
|
||||
|
||||
if (sendMessageResult.getIdentityFailure() != null) {
|
||||
Log.w(TAG, "Identity failure for " + recipient.getId());
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
@@ -159,7 +160,7 @@ public class ProfileKeySendJob extends BaseJob {
|
||||
dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId()));
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.IMPLICIT, dataMessage.build());
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||
@@ -43,6 +44,7 @@ import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
@@ -291,6 +293,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
try {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
GroupId.Push groupId = groupRecipient.requireGroupId().requirePush();
|
||||
Optional<byte[]> profileKey = getProfileKey(groupRecipient);
|
||||
@@ -327,7 +330,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
.withExpiration(groupRecipient.getExpireMessages())
|
||||
.asGroupMessage(group)
|
||||
.build();
|
||||
return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage);
|
||||
return GroupSendUtil.sendDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, groupDataMessage);
|
||||
} else {
|
||||
MessageGroupContext.GroupV1Properties properties = groupMessage.requireGroupV1Properties();
|
||||
|
||||
@@ -345,7 +348,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
.build();
|
||||
|
||||
Log.i(TAG, JobLogger.format(this, "Beginning update send."));
|
||||
return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage);
|
||||
return messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.IMPLICIT, groupDataMessage);
|
||||
}
|
||||
} else {
|
||||
SignalServiceDataMessage.Builder builder = SignalServiceDataMessage.newBuilder()
|
||||
@@ -367,7 +370,12 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
.build();
|
||||
|
||||
Log.i(TAG, JobLogger.format(this, "Beginning message send."));
|
||||
return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupMessage);
|
||||
|
||||
if (groupRecipient.isPushV2Group()) {
|
||||
return GroupSendUtil.sendDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage);
|
||||
} else {
|
||||
return messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage);
|
||||
}
|
||||
}
|
||||
} catch (ServerRejectedException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
|
||||
+8
-15
@@ -11,29 +11,25 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
@@ -133,8 +129,9 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
|
||||
throw new NotPushRegisteredException();
|
||||
}
|
||||
|
||||
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
|
||||
List<Recipient> completions = deliver(destinations);
|
||||
GroupId.V2 groupId = GroupId.v2(GroupUtil.requireMasterKey(groupContextV2.getMasterKey().toByteArray()));
|
||||
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
|
||||
List<Recipient> completions = deliver(destinations, groupId);
|
||||
|
||||
for (Recipient completion : completions) {
|
||||
recipients.remove(completion.getId());
|
||||
@@ -161,20 +158,16 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
|
||||
Log.w(TAG, "Failed to send remote delete to all recipients! (" + (initialRecipientCount - recipients.size() + "/" + initialRecipientCount + ")") );
|
||||
}
|
||||
|
||||
private @NonNull List<Recipient> deliver(@NonNull List<Recipient> destinations)
|
||||
private @NonNull List<Recipient> deliver(@NonNull List<Recipient> destinations, @NonNull GroupId.V2 groupId)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
|
||||
|
||||
SignalServiceGroupV2 group = SignalServiceGroupV2.fromProtobuf(groupContextV2);
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(timestamp)
|
||||
.asGroupMessage(group)
|
||||
.build();
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, groupDataMessage);
|
||||
List<SendMessageResult> results = GroupSendUtil.sendDataMessage(context, groupId, destinations, false, ContentHint.IMPLICIT, groupDataMessage);
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
@@ -124,9 +125,10 @@ public class PushGroupUpdateJob extends BaseJob {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
Recipient recipient = Recipient.resolved(source);
|
||||
|
||||
messageSender.sendMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, recipient),
|
||||
message);
|
||||
messageSender.sendDataMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, recipient),
|
||||
ContentHint.DEFAULT,
|
||||
message);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
@@ -221,10 +222,10 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
|
||||
|
||||
messageSender.sendMessage(syncMessage, syncAccess);
|
||||
messageSender.sendSyncMessage(syncMessage, syncAccess);
|
||||
return syncAccess.isPresent();
|
||||
} else {
|
||||
return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), mediaMessage).getSuccess().isUnidentified();
|
||||
return messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage).getSuccess().isUnidentified();
|
||||
}
|
||||
} catch (UnregisteredUserException e) {
|
||||
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -173,10 +174,10 @@ public class PushTextSendJob extends PushSendJob {
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
|
||||
|
||||
messageSender.sendMessage(syncMessage, syncAccess);
|
||||
messageSender.sendSyncMessage(syncMessage, syncAccess);
|
||||
return syncAccess.isPresent();
|
||||
} else {
|
||||
return messageSender.sendMessage(address, unidentifiedAccess, textSecureMessage).getSuccess().isUnidentified();
|
||||
return messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage).getSuccess().isUnidentified();
|
||||
}
|
||||
} catch (UnregisteredUserException e) {
|
||||
warn(TAG, "Failure", e);
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -25,10 +26,12 @@ import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
|
||||
@@ -216,18 +219,25 @@ public class ReactionSendJob extends BaseJob {
|
||||
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, @NonNull Recipient targetAuthor, long targetSentTimestamp)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
|
||||
|
||||
if (conversationRecipient.isGroup()) {
|
||||
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
List<SendMessageResult> results;
|
||||
|
||||
if (conversationRecipient.isPushV2Group()) {
|
||||
results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build());
|
||||
} else {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
|
||||
|
||||
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
|
||||
}
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
@@ -172,18 +174,25 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, long targetSentTimestamp)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
|
||||
|
||||
if (conversationRecipient.isGroup()) {
|
||||
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
List<SendMessageResult> results;
|
||||
|
||||
if (conversationRecipient.isPushV2Group()) {
|
||||
results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build());
|
||||
} else {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
|
||||
|
||||
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
|
||||
}
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(context, results);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
@@ -85,9 +86,10 @@ public class RequestGroupInfoJob extends BaseJob {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
Recipient recipient = Recipient.resolved(source);
|
||||
|
||||
messageSender.sendMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, recipient),
|
||||
message);
|
||||
messageSender.sendDataMessage(RecipientUtil.toSignalServiceAddress(context, recipient),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, recipient),
|
||||
ContentHint.IMPLICIT,
|
||||
message);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class SendRetryReceiptJob extends BaseJob {
|
||||
|
||||
private static final String TAG = Log.tag(SendRetryReceiptJob.class);
|
||||
|
||||
public static final String KEY = "SendRetryReceiptJob";
|
||||
|
||||
private static final String KEY_RECIPIENT_ID = "recipient_id";
|
||||
private static final String KEY_ERROR_MESSAGE = "error_message";
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final Optional<GroupId> groupId;
|
||||
private final DecryptionErrorMessage errorMessage;
|
||||
|
||||
public SendRetryReceiptJob(@NonNull RecipientId recipientId, @NonNull Optional<GroupId> groupId, @NonNull DecryptionErrorMessage errorMessage) {
|
||||
this(recipientId,
|
||||
groupId,
|
||||
errorMessage,
|
||||
new Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(recipientId.toQueueKey())
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
}
|
||||
|
||||
private SendRetryReceiptJob(@NonNull RecipientId recipientId,
|
||||
@NonNull Optional<GroupId> groupId,
|
||||
@NonNull DecryptionErrorMessage errorMessage,
|
||||
@NonNull Parameters parameters)
|
||||
{
|
||||
super(parameters);
|
||||
this.recipientId = recipientId;
|
||||
this.groupId = groupId;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
Data.Builder builder = new Data.Builder()
|
||||
.putString(KEY_RECIPIENT_ID, recipientId.serialize())
|
||||
.putBlobAsString(KEY_ERROR_MESSAGE, errorMessage.serialize());
|
||||
|
||||
if (groupId.isPresent()) {
|
||||
builder.putBlobAsString(KEY_GROUP_ID, groupId.get().getDecodedId());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
Optional<UnidentifiedAccessPair> access = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
Optional<byte[]> group = groupId.transform(GroupId::getDecodedId);
|
||||
|
||||
Log.i(TAG, "Sending retry receipt for " + errorMessage.getTimestamp() + " to " + recipientId + ", device: " + errorMessage.getDeviceId());
|
||||
ApplicationDependencies.getSignalServiceMessageSender().sendRetryReceipt(address, access, group, errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<SendRetryReceiptJob> {
|
||||
@Override
|
||||
public @NonNull SendRetryReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
RecipientId recipientId = RecipientId.from(data.getString(KEY_RECIPIENT_ID));
|
||||
DecryptionErrorMessage errorMessage = new DecryptionErrorMessage(data.getStringAsBlob(KEY_ERROR_MESSAGE));
|
||||
Optional<GroupId> groupId = Optional.absent();
|
||||
|
||||
if (data.hasString(KEY_GROUP_ID)) {
|
||||
groupId = Optional.of(GroupId.pushOrThrow(data.getStringAsBlob(KEY_GROUP_ID)));
|
||||
}
|
||||
|
||||
return new SendRetryReceiptJob(recipientId, groupId, errorMessage, parameters);
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Sends a {@link SenderKeyDistributionMessage} to a target recipient.
|
||||
*
|
||||
* Will re-check group membership at send time and send the proper distribution message if they're still a member.
|
||||
*/
|
||||
public final class SenderKeyDistributionSendJob extends BaseJob {
|
||||
|
||||
private static final String TAG = Log.tag(SenderKeyDistributionSendJob.class);
|
||||
|
||||
public static final String KEY = "SenderKeyDistributionSendJob";
|
||||
|
||||
private static final String KEY_RECIPIENT_ID = "recipient_id";
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
public SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull GroupId.V2 groupId) {
|
||||
this(recipientId, groupId, new Parameters.Builder()
|
||||
.setQueue(recipientId.toQueueKey())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build());
|
||||
}
|
||||
|
||||
private SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull GroupId.V2 groupId, @NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
|
||||
this.recipientId = recipientId;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize())
|
||||
.putBlobAsString(KEY_GROUP_ID, groupId.getDecodedId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
|
||||
if (!groupDatabase.isCurrentMember(groupId, recipientId)) {
|
||||
Log.w(TAG, recipientId + " is no longer a member of " + groupId + "! Not sending.");
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (recipient.getSenderKeyCapability() != Recipient.Capability.SUPPORTED) {
|
||||
Log.w(TAG, recipientId + " does not support sender key! Not sending.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, recipient));
|
||||
DistributionId distributionId = groupDatabase.getOrCreateDistributionId(groupId);
|
||||
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
|
||||
List<Optional<UnidentifiedAccessPair>> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(recipient));
|
||||
|
||||
messageSender.sendSenderKeyDistributionMessage(address, access, message, groupId.getDecodedId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<SenderKeyDistributionSendJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull SenderKeyDistributionSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new SenderKeyDistributionSendJob(RecipientId.from(data.getString(KEY_RECIPIENT_ID)),
|
||||
GroupId.pushOrThrow(data.getStringAsBlob(KEY_GROUP_ID)).requireV2(),
|
||||
parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -22,6 +23,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -105,8 +107,8 @@ public class TypingSendJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Recipient> recipients = Collections.singletonList(recipient);
|
||||
Optional<byte[]> groupId = Optional.absent();
|
||||
List<Recipient> recipients = Collections.singletonList(recipient);
|
||||
Optional<byte[]> groupId = Optional.absent();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
@@ -118,23 +120,28 @@ public class TypingSendJob extends BaseJob {
|
||||
.filter(r -> !r.isBlocked())
|
||||
.toList());
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipients);
|
||||
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId);
|
||||
|
||||
if (addresses.isEmpty()) {
|
||||
Log.w(TAG, "No one to send typing indicators to");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCanceled()) {
|
||||
Log.w(TAG, "Canceled before send!");
|
||||
return;
|
||||
}
|
||||
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId);
|
||||
|
||||
try {
|
||||
messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled);
|
||||
if (recipient.isPushV2Group()) {
|
||||
GroupSendUtil.sendTypingMessage(context, recipient.requireGroupId().requireV2(), recipients, typingMessage, this::isCanceled);
|
||||
} else {
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipients);
|
||||
|
||||
if (addresses.isEmpty()) {
|
||||
Log.w(TAG, "No one to send typing indicators to");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCanceled()) {
|
||||
Log.w(TAG, "Canceled before send!");
|
||||
return;
|
||||
}
|
||||
|
||||
messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled);
|
||||
}
|
||||
} catch (CancelationException e) {
|
||||
Log.w(TAG, "Canceled during send!");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public final class InternalValues extends SignalStoreValues {
|
||||
public static final String RECIPIENT_DETAILS = "internal.recipient_details";
|
||||
public static final String FORCE_CENSORSHIP = "internal.force_censorship";
|
||||
public static final String FORCE_BUILT_IN_EMOJI = "internal.force_built_in_emoji";
|
||||
public static final String REMOVE_SENDER_KEY_MINIMUM = "internal.remove_sender_key_minimum";
|
||||
|
||||
InternalValues(KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -82,12 +83,19 @@ public final class InternalValues extends SignalStoreValues {
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the app to behave as if it is in a country where Signal is censored.
|
||||
* Force the app to use the emoji that ship with the app, as opposed to the ones that were downloaded.
|
||||
*/
|
||||
public synchronized boolean forceBuiltInEmoji() {
|
||||
return FeatureFlags.internalUser() && getBoolean(FORCE_BUILT_IN_EMOJI, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the requirement that there must be two sender-key-capable recipients to use sender key
|
||||
*/
|
||||
public synchronized boolean removeSenderKeyMinimum() {
|
||||
return FeatureFlags.internalUser() && getBoolean(REMOVE_SENDER_KEY_MINIMUM, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable initiating a GV1->GV2 auto-migration. You can still recognize a group has been
|
||||
* auto-migrated.
|
||||
|
||||
@@ -33,9 +33,11 @@ public final class LogSectionCapabilities implements LogSection {
|
||||
return new StringBuilder().append("-- Local").append("\n")
|
||||
.append("GV2 : ").append(capabilities.isGv2()).append("\n")
|
||||
.append("GV1 Migration: ").append(capabilities.isGv1Migration()).append("\n")
|
||||
.append("Sender Key : ").append(capabilities.isSenderKey()).append("\n")
|
||||
.append("\n")
|
||||
.append("-- Global").append("\n")
|
||||
.append("GV2 : ").append(self.getGroupsV2Capability()).append("\n")
|
||||
.append("GV1 Migration: ").append(self.getGroupsV1MigrationCapability()).append("\n");
|
||||
.append("GV1 Migration: ").append(self.getGroupsV1MigrationCapability()).append("\n")
|
||||
.append("Sender Key : ").append(self.getSenderKeyCapability()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package org.thoughtcrime.securesms.messages;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.NoSessionException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.CancelationException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class GroupSendUtil {
|
||||
|
||||
private static final String TAG = Log.tag(GroupSendUtil.class);
|
||||
|
||||
private static final long MAX_KEY_AGE = TimeUnit.DAYS.toMillis(30);
|
||||
|
||||
private GroupSendUtil() {}
|
||||
|
||||
|
||||
/**
|
||||
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
|
||||
* {@link SendMessageResult}s just like we're used to.
|
||||
*
|
||||
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static List<SendMessageResult> sendDataMessage(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull List<Recipient> allTargets,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
@NonNull SignalServiceDataMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
return sendMessage(context, groupId, allTargets, isRecipientUpdate, new DataSendOperation(message, contentHint), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
|
||||
* {@link SendMessageResult}s just like we're used to.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static List<SendMessageResult> sendTypingMessage(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull List<Recipient> allTargets,
|
||||
@NonNull SignalServiceTypingMessage message,
|
||||
@Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
return sendMessage(context, groupId, allTargets, false, new TypingSendOperation(message), cancelationSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
|
||||
* {@link SendMessageResult}s just like we're used to.
|
||||
*
|
||||
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
|
||||
*/
|
||||
@WorkerThread
|
||||
private static List<SendMessageResult> sendMessage(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull List<Recipient> allTargets,
|
||||
boolean isRecipientUpdate,
|
||||
@NonNull SendOperation sendOperation,
|
||||
@Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
RecipientData recipients = new RecipientData(context, allTargets);
|
||||
|
||||
List<Recipient> senderKeyTargets = new LinkedList<>();
|
||||
List<Recipient> legacyTargets = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : allTargets) {
|
||||
Optional<UnidentifiedAccessPair> access = recipients.getAccessPair(recipient.getId());
|
||||
|
||||
if (recipient.getSenderKeyCapability() == Recipient.Capability.SUPPORTED &&
|
||||
recipient.hasUuid() &&
|
||||
access.isPresent() &&
|
||||
access.get().getTargetUnidentifiedAccess().isPresent())
|
||||
{
|
||||
senderKeyTargets.add(recipient);
|
||||
} else {
|
||||
legacyTargets.add(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
if (FeatureFlags.senderKey()) {
|
||||
if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) {
|
||||
Log.i(TAG, "All of our devices do not support sender key. Using legacy.");
|
||||
legacyTargets.addAll(senderKeyTargets);
|
||||
senderKeyTargets.clear();
|
||||
} else if (SignalStore.internalValues().removeSenderKeyMinimum()) {
|
||||
Log.i(TAG, "Sender key minimum removed. Using for " + senderKeyTargets.size() + " recipients.");
|
||||
} else if (senderKeyTargets.size() < 2) {
|
||||
Log.i(TAG, "Too few sender-key-capable users (" + senderKeyTargets.size() + "). Doing all legacy sends.");
|
||||
legacyTargets.addAll(senderKeyTargets);
|
||||
senderKeyTargets.clear();
|
||||
} else {
|
||||
Log.i(TAG, "Can use sender key for " + senderKeyTargets.size() + "/" + allTargets.size() + " recipients.");
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Feature flag disabled. Using legacy.");
|
||||
legacyTargets.addAll(senderKeyTargets);
|
||||
senderKeyTargets.clear();
|
||||
}
|
||||
|
||||
List<SendMessageResult> allResults = new ArrayList<>(allTargets.size());
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId);
|
||||
|
||||
if (senderKeyTargets.size() > 0) {
|
||||
long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(context, distributionId);
|
||||
long keyAge = System.currentTimeMillis() - keyCreateTime;
|
||||
|
||||
if (keyCreateTime != -1 && keyAge > MAX_KEY_AGE) {
|
||||
Log.w(TAG, "Key is " + (keyAge) + " ms old (~" + TimeUnit.MILLISECONDS.toDays(keyAge) + " days). Rotating.");
|
||||
SenderKeyUtil.rotateOurKey(context, distributionId);
|
||||
}
|
||||
|
||||
try {
|
||||
List<SignalServiceAddress> targets = senderKeyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
|
||||
List<UnidentifiedAccess> access = senderKeyTargets.stream().map(r -> recipients.requireAccess(r.getId())).collect(Collectors.toList());
|
||||
List<SendMessageResult> results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, isRecipientUpdate);
|
||||
|
||||
allResults.addAll(results);
|
||||
|
||||
int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count();
|
||||
Log.d(TAG, "Successfully sent using sender key to " + successCount + "/" + targets.size() + " sender key targets.");
|
||||
} catch (NoSessionException e) {
|
||||
Log.w(TAG, "No session. Falling back to legacy sends.", e);
|
||||
legacyTargets.addAll(senderKeyTargets);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Invalid Key. Falling back to legacy sends.", e);
|
||||
legacyTargets.addAll(senderKeyTargets);
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelationSignal != null && cancelationSignal.isCanceled()) {
|
||||
throw new CancelationException();
|
||||
}
|
||||
|
||||
if (legacyTargets.size() > 0) {
|
||||
Log.i(TAG, "Need to do " + legacyTargets.size() + " legacy sends.");
|
||||
|
||||
List<SignalServiceAddress> targets = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
|
||||
List<Optional<UnidentifiedAccessPair>> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList());
|
||||
boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0;
|
||||
|
||||
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, cancelationSignal);
|
||||
|
||||
allResults.addAll(results);
|
||||
|
||||
int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count();
|
||||
Log.d(TAG, "Successfully using 1:1 to " + successCount + "/" + targets.size() + " legacy targets.");
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
/** Abstraction layer to handle the different types of message send operations we can do */
|
||||
private interface SendOperation {
|
||||
@NonNull List<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
|
||||
@NonNull DistributionId distributionId,
|
||||
@NonNull List<SignalServiceAddress> targets,
|
||||
@NonNull List<UnidentifiedAccess> access,
|
||||
boolean isRecipientUpdate)
|
||||
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException;
|
||||
|
||||
@NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
|
||||
@NonNull List<SignalServiceAddress> targets,
|
||||
@NonNull List<Optional<UnidentifiedAccessPair>> access,
|
||||
boolean isRecipientUpdate,
|
||||
@Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException, UntrustedIdentityException;
|
||||
}
|
||||
|
||||
private static class DataSendOperation implements SendOperation {
|
||||
private final SignalServiceDataMessage message;
|
||||
private final ContentHint contentHint;
|
||||
|
||||
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint) {
|
||||
this.message = message;
|
||||
this.contentHint = contentHint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
|
||||
@NonNull DistributionId distributionId,
|
||||
@NonNull List<SignalServiceAddress> targets,
|
||||
@NonNull List<UnidentifiedAccess> access,
|
||||
boolean isRecipientUpdate)
|
||||
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException
|
||||
{
|
||||
return messageSender.sendGroupDataMessage(distributionId, targets, access, isRecipientUpdate, contentHint, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
|
||||
@NonNull List<SignalServiceAddress> targets,
|
||||
@NonNull List<Optional<UnidentifiedAccessPair>> access,
|
||||
boolean isRecipientUpdate,
|
||||
@Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TypingSendOperation implements SendOperation {
|
||||
|
||||
private final SignalServiceTypingMessage message;
|
||||
|
||||
private TypingSendOperation(@NonNull SignalServiceTypingMessage message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
|
||||
@NonNull DistributionId distributionId,
|
||||
@NonNull List<SignalServiceAddress> targets,
|
||||
@NonNull List<UnidentifiedAccess> access,
|
||||
boolean isRecipientUpdate)
|
||||
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException
|
||||
{
|
||||
messageSender.sendGroupTyping(distributionId, targets, access, message);
|
||||
return targets.stream().map(a -> SendMessageResult.success(a, true, false, -1)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
|
||||
@NonNull List<SignalServiceAddress> targets,
|
||||
@NonNull List<Optional<UnidentifiedAccessPair>> access,
|
||||
boolean isRecipientUpdate,
|
||||
@Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException
|
||||
{
|
||||
messageSender.sendTyping(targets, access, message, cancelationSignal);
|
||||
return targets.stream().map(a -> SendMessageResult.success(a, true, false, -1)).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Little utility wrapper that lets us get the various different slices of recipient models that we need for different methods.
|
||||
*/
|
||||
private static final class RecipientData {
|
||||
|
||||
private final Map<RecipientId, Optional<UnidentifiedAccessPair>> accessById;
|
||||
private final Map<RecipientId, SignalServiceAddress> addressById;
|
||||
|
||||
RecipientData(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
|
||||
this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients);
|
||||
this.addressById = mapAddresses(context, recipients);
|
||||
}
|
||||
|
||||
@NonNull SignalServiceAddress getAddress(@NonNull RecipientId id) {
|
||||
return Objects.requireNonNull(addressById.get(id));
|
||||
}
|
||||
|
||||
@NonNull Optional<UnidentifiedAccessPair> getAccessPair(@NonNull RecipientId id) {
|
||||
return Objects.requireNonNull(accessById.get(id));
|
||||
}
|
||||
|
||||
@NonNull UnidentifiedAccess requireAccess(@NonNull RecipientId id) {
|
||||
return Objects.requireNonNull(accessById.get(id)).get().getTargetUnidentifiedAccess().get();
|
||||
}
|
||||
|
||||
private static @NonNull Map<RecipientId, SignalServiceAddress> mapAddresses(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
|
||||
|
||||
Iterator<Recipient> recipientIterator = recipients.iterator();
|
||||
Iterator<SignalServiceAddress> addressIterator = addresses.iterator();
|
||||
|
||||
Map<RecipientId, SignalServiceAddress> map = new HashMap<>(recipients.size());
|
||||
|
||||
while (recipientIterator.hasNext()) {
|
||||
map.put(recipientIterator.next().getId(), addressIterator.next());
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ public class IncomingMessageProcessor {
|
||||
if (envelope.isReceipt()) {
|
||||
processReceipt(envelope);
|
||||
return null;
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender()) {
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isPlaintextContent()) {
|
||||
return processMessage(envelope);
|
||||
} else {
|
||||
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -67,12 +68,14 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PaymentTransactionCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileKeySendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -103,6 +106,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
@@ -110,9 +114,13 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -142,6 +150,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -219,6 +228,10 @@ public final class MessageContentProcessor {
|
||||
|
||||
log(String.valueOf(content.getTimestamp()), "Beginning message processing.");
|
||||
|
||||
if (content.getSenderKeyDistributionMessage().isPresent()) {
|
||||
handleSenderKeyDistributionMessage(content.getSender(), content.getSenderDevice(), content.getSenderKeyDistributionMessage().get());
|
||||
}
|
||||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent();
|
||||
@@ -328,6 +341,8 @@ public final class MessageContentProcessor {
|
||||
else if (message.isViewedReceipt()) handleViewedReceipt(content, message);
|
||||
} else if (content.getTypingMessage().isPresent()) {
|
||||
handleTypingMessage(content, content.getTypingMessage().get());
|
||||
} else if (content.getDecryptionErrorMessage().isPresent()) {
|
||||
handleRetryReceipt(content, content.getDecryptionErrorMessage().get());
|
||||
} else {
|
||||
warn(String.valueOf(content.getTimestamp()), "Got unrecognized message!");
|
||||
}
|
||||
@@ -1549,6 +1564,12 @@ public final class MessageContentProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSenderKeyDistributionMessage(@NonNull SignalServiceAddress address, int deviceId, @NonNull SenderKeyDistributionMessage message) {
|
||||
log("Processing SenderKeyDistributionMessage.");
|
||||
SignalServiceMessageSender sender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
sender.processSenderKeyDistributionMessage(new SignalProtocolAddress(address.getIdentifier(), deviceId), message);
|
||||
}
|
||||
|
||||
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message)
|
||||
{
|
||||
@@ -1657,6 +1678,81 @@ public final class MessageContentProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRetryReceipt(@NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) {
|
||||
if (!FeatureFlags.senderKey()) {
|
||||
Log.w(TAG, "Sender key not enabled, skipping retry receipt.");
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient requester = Recipient.externalHighTrustPush(context, content.getSender());
|
||||
long sentTimestamp = decryptionErrorMessage.getTimestamp();
|
||||
|
||||
if (!requester.hasUuid()) {
|
||||
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester " + requester.getId() + " somehow has no UUID! timestamp: " + sentTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, Recipient.self().getId());
|
||||
|
||||
if (messageRecord == null) {
|
||||
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Unable to find message for " + requester.getId() + " with timestamp " + sentTimestamp);
|
||||
// TODO Send distribution message?
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(messageRecord.getThreadId());
|
||||
|
||||
if (threadRecipient == null) {
|
||||
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Unable to find a recipient for thread " + messageRecord.getThreadId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] MMS " + messageRecord.getId());
|
||||
MmsMessageRecord mms = (MmsMessageRecord) messageRecord;
|
||||
|
||||
if (threadRecipient.isPushV2Group()) {
|
||||
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(threadRecipient.requireGroupId().requireV2());
|
||||
SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireUuid().toString(), decryptionErrorMessage.getDeviceId());
|
||||
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, Collections.singleton(requesterAddress));
|
||||
|
||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
GroupReceiptInfo receiptInfo = receiptDatabase.getGroupReceiptInfo(mms.getId(), requester.getId());
|
||||
boolean needsDistributionMessage = true;
|
||||
|
||||
if (receiptInfo == null) {
|
||||
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester was never sent message " + mms.getId() + "! Cannot resend it.");
|
||||
} else if (receiptInfo.getStatus() >= GroupReceiptDatabase.STATUS_DELIVERED) {
|
||||
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully delivered to the requester. Not resending.");
|
||||
} else {
|
||||
long messageAge = System.currentTimeMillis() - mms.getDateSent();
|
||||
|
||||
if (messageAge < FeatureFlags.retryRespondMaxAge()) {
|
||||
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully sent to the requester, but not delivered. Resending.");
|
||||
|
||||
DatabaseFactory.getGroupReceiptDatabase(context).update(requester.getId(), mms.getId(), GroupReceiptDatabase.STATUS_UNDELIVERED, System.currentTimeMillis());
|
||||
ApplicationDependencies.getJobManager().startChain(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.requireGroupId().requireV2()))
|
||||
.then(new PushGroupSendJob(mms.getId(), threadRecipient.getId(), requester.getId(), false))
|
||||
.enqueue();
|
||||
|
||||
needsDistributionMessage = false;
|
||||
} else {
|
||||
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully sent to the requester, but not delivered. But it's " + messageAge + " ms old, so we're not resending.");
|
||||
}
|
||||
}
|
||||
|
||||
if (needsDistributionMessage && threadRecipient.getParticipants().contains(requester)) {
|
||||
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester is, however, in the group now. Sending distribution message.");
|
||||
ApplicationDependencies.getJobManager().add(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.requireGroupId().requireV2()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] SMS " + messageRecord.getId());
|
||||
SmsMessageRecord sms = (SmsMessageRecord) messageRecord;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
|
||||
if (message.isViewOnce()) {
|
||||
List<SignalServiceAttachment> attachments = message.getAttachments().or(Collections.emptyList());
|
||||
|
||||
@@ -21,25 +21,30 @@ import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendRetryReceiptJob;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata;
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -77,9 +82,16 @@ public final class MessageDecryptionUtil {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e);
|
||||
return DecryptionResult.forError(MessageState.INVALID_VERSION, toExceptionMetadata(e), jobs);
|
||||
|
||||
} catch (ProtocolInvalidMessageException | ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException | ProtocolNoSessionException e) {
|
||||
} catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException | ProtocolNoSessionException e) {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e);
|
||||
jobs.add(new AutomaticSessionResetJob(Recipient.external(context, e.getSender()).getId(), e.getSenderDevice(), envelope.getTimestamp()));
|
||||
Recipient sender = Recipient.external(context, e.getSender());
|
||||
|
||||
if (sender.supportsMessageRetries() && Recipient.self().supportsMessageRetries() && FeatureFlags.senderKey()) {
|
||||
jobs.add(handleRetry(context, sender, envelope, e));
|
||||
} else {
|
||||
jobs.add(new AutomaticSessionResetJob(sender.getId(), e.getSenderDevice(), envelope.getTimestamp()));
|
||||
}
|
||||
|
||||
return DecryptionResult.forNoop(jobs);
|
||||
} catch (ProtocolLegacyMessageException e) {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e);
|
||||
@@ -87,7 +99,7 @@ public final class MessageDecryptionUtil {
|
||||
} catch (ProtocolDuplicateMessageException e) {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e);
|
||||
return DecryptionResult.forError(MessageState.DUPLICATE_MESSAGE, toExceptionMetadata(e), jobs);
|
||||
} catch (InvalidMetadataVersionException | InvalidMetadataMessageException e) {
|
||||
} catch (InvalidMetadataVersionException | InvalidMetadataMessageException | ProtocolInvalidMessageException e) {
|
||||
Log.w(TAG, String.valueOf(envelope.getTimestamp()), e);
|
||||
return DecryptionResult.forNoop(jobs);
|
||||
} catch (SelfSendException e) {
|
||||
@@ -103,6 +115,62 @@ public final class MessageDecryptionUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Job handleRetry(@NonNull Context context, @NonNull Recipient sender, @NonNull SignalServiceEnvelope envelope, @NonNull ProtocolException protocolException) {
|
||||
ContentHint contentHint = ContentHint.fromType(protocolException.getContentHint());
|
||||
int senderDevice = protocolException.getSenderDevice();
|
||||
long receivedTimestamp = System.currentTimeMillis();
|
||||
Optional<GroupId> groupId = Optional.absent();
|
||||
|
||||
if (protocolException.getGroupId().isPresent()) {
|
||||
try {
|
||||
groupId = Optional.of(GroupId.push(protocolException.getGroupId().get()));
|
||||
} catch (BadGroupIdException e) {
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Bad groupId!");
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Could not decrypt a message with a type of " + contentHint);
|
||||
|
||||
long threadId;
|
||||
|
||||
if (groupId.isPresent()) {
|
||||
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(context, groupId.get());
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
} else {
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(sender);
|
||||
}
|
||||
|
||||
switch (contentHint) {
|
||||
case DEFAULT:
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Inserting an error right away because it's " + contentHint);
|
||||
DatabaseFactory.getSmsDatabase(context).insertBadDecryptMessage(sender.getId(), senderDevice, envelope.getTimestamp(), receivedTimestamp, threadId);
|
||||
break;
|
||||
case RESENDABLE:
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Inserting into pending retries store because it's " + contentHint);
|
||||
DatabaseFactory.getPendingRetryReceiptDatabase(context).insert(sender.getId(), senderDevice, envelope.getTimestamp(), receivedTimestamp, threadId);
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
break;
|
||||
case IMPLICIT:
|
||||
Log.w(TAG, "[" + envelope.getTimestamp() + "] Not inserting any error because it's " + contentHint);
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] originalContent;
|
||||
int envelopeType;
|
||||
if (protocolException.getUnidentifiedSenderMessageContent().isPresent()) {
|
||||
originalContent = protocolException.getUnidentifiedSenderMessageContent().get().getContent();
|
||||
envelopeType = protocolException.getUnidentifiedSenderMessageContent().get().getType();
|
||||
} else {
|
||||
originalContent = envelope.getContent();
|
||||
envelopeType = envelope.getType();
|
||||
}
|
||||
|
||||
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, envelopeType, envelope.getTimestamp(), senderDevice);
|
||||
|
||||
return new SendRetryReceiptJob(sender.getId(), groupId, decryptionErrorMessage);
|
||||
}
|
||||
|
||||
|
||||
private static ExceptionMetadata toExceptionMetadata(@NonNull UnsupportedDataMessageException e)
|
||||
throws NoSenderException
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ public class ApplicationMigrations {
|
||||
|
||||
private static final int LEGACY_CANONICAL_VERSION = 455;
|
||||
|
||||
public static final int CURRENT_VERSION = 34;
|
||||
public static final int CURRENT_VERSION = 35;
|
||||
|
||||
private static final class Version {
|
||||
static final int LEGACY = 1;
|
||||
@@ -76,6 +76,7 @@ public class ApplicationMigrations {
|
||||
static final int PROFILE_SHARING_UPDATE = 32;
|
||||
static final int SMS_STORAGE_SYNC = 33;
|
||||
static final int APPLY_UNIVERSAL_EXPIRE = 34;
|
||||
static final int SENDER_KEY = 35;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,6 +323,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.SMS_STORAGE_SYNC, new ApplyUnknownFieldsToSelfMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.SENDER_KEY) {
|
||||
jobs.put(Version.SENDER_KEY, new AttributesMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ public class Recipient {
|
||||
private final boolean forceSmsSelection;
|
||||
private final Capability groupsV2Capability;
|
||||
private final Capability groupsV1MigrationCapability;
|
||||
private final Capability senderKeyCapability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageId;
|
||||
private final MentionSetting mentionSetting;
|
||||
@@ -352,6 +353,7 @@ public class Recipient {
|
||||
this.forceSmsSelection = false;
|
||||
this.groupsV2Capability = Capability.UNKNOWN;
|
||||
this.groupsV1MigrationCapability = Capability.UNKNOWN;
|
||||
this.senderKeyCapability = Capability.UNKNOWN;
|
||||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
@@ -402,6 +404,7 @@ public class Recipient {
|
||||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.groupsV2Capability = details.groupsV2Capability;
|
||||
this.groupsV1MigrationCapability = details.groupsV1MigrationCapability;
|
||||
this.senderKeyCapability = details.senderKeyCapability;
|
||||
this.storageId = details.storageId;
|
||||
this.mentionSetting = details.mentionSetting;
|
||||
this.wallpaper = details.wallpaper;
|
||||
@@ -867,6 +870,17 @@ public class Recipient {
|
||||
return groupsV1MigrationCapability;
|
||||
}
|
||||
|
||||
public @NonNull Capability getSenderKeyCapability() {
|
||||
return senderKeyCapability;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this recipient supports the message retry system, or false if we should use the legacy session reset system.
|
||||
*/
|
||||
public boolean supportsMessageRetries() {
|
||||
return getSenderKeyCapability() == Capability.SUPPORTED;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public class RecipientDetails {
|
||||
final boolean forceSmsSelection;
|
||||
final Recipient.Capability groupsV2Capability;
|
||||
final Recipient.Capability groupsV1MigrationCapability;
|
||||
final Recipient.Capability senderKeyCapability;
|
||||
final InsightsBannerTier insightsBannerTier;
|
||||
final byte[] storageId;
|
||||
final MentionSetting mentionSetting;
|
||||
@@ -117,6 +118,7 @@ public class RecipientDetails {
|
||||
this.forceSmsSelection = settings.isForceSmsSelection();
|
||||
this.groupsV2Capability = settings.getGroupsV2Capability();
|
||||
this.groupsV1MigrationCapability = settings.getGroupsV1MigrationCapability();
|
||||
this.senderKeyCapability = settings.getSenderKeyCapability();
|
||||
this.insightsBannerTier = settings.getInsightsBannerTier();
|
||||
this.storageId = settings.getStorageId();
|
||||
this.mentionSetting = settings.getMentionSetting();
|
||||
@@ -171,6 +173,7 @@ public class RecipientDetails {
|
||||
this.groupName = null;
|
||||
this.groupsV2Capability = Recipient.Capability.UNKNOWN;
|
||||
this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN;
|
||||
this.senderKeyCapability = Recipient.Capability.UNKNOWN;
|
||||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
|
||||
+2
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.AppCapabilities;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
@@ -203,6 +204,7 @@ public final class CodeVerificationRequest {
|
||||
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
|
||||
SessionUtil.archiveAllSessions(context);
|
||||
SenderKeyUtil.clearAllState(context);
|
||||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
|
||||
KbsPinData kbsData = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse()) : null;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
|
||||
/**
|
||||
* Manages the time-based creation of error messages for retries that are pending for messages we couldn't decrypt.
|
||||
*/
|
||||
public final class PendingRetryReceiptManager extends TimedEventManager<PendingRetryReceiptModel> {
|
||||
|
||||
private static final String TAG = Log.tag(PendingRetryReceiptManager.class);
|
||||
|
||||
private final PendingRetryReceiptDatabase pendingDatabase;
|
||||
private final MessageDatabase messageDatabase;
|
||||
|
||||
public PendingRetryReceiptManager(@NonNull Application application) {
|
||||
super(application, "PendingRetryReceiptManager");
|
||||
|
||||
this.pendingDatabase = DatabaseFactory.getPendingRetryReceiptDatabase(application);
|
||||
this.messageDatabase = DatabaseFactory.getSmsDatabase(application);
|
||||
|
||||
scheduleIfNecessary();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
protected @Nullable PendingRetryReceiptModel getNextClosestEvent() {
|
||||
PendingRetryReceiptModel model = pendingDatabase.getOldest();
|
||||
|
||||
if (model != null) {
|
||||
Log.i(TAG, "Next closest expiration is in " + getDelayForEvent(model) + " ms for timestamp " + model.getSentTimestamp() + ".");
|
||||
} else {
|
||||
Log.d(TAG, "No pending receipts to schedule.");
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
protected void executeEvent(@NonNull PendingRetryReceiptModel event) {
|
||||
Log.w(TAG, "It's been " + (System.currentTimeMillis() - event.getReceivedTimestamp()) + " ms since this retry receipt was received. Showing an error.");
|
||||
messageDatabase.insertBadDecryptMessage(event.getAuthor(), event.getAuthorDevice(), event.getSentTimestamp(), event.getReceivedTimestamp(), event.getThreadId());
|
||||
pendingDatabase.delete(event.getId());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Override
|
||||
protected long getDelayForEvent(@NonNull PendingRetryReceiptModel event) {
|
||||
long expiresAt = event.getReceivedTimestamp() + FeatureFlags.retryReceiptLifespan();
|
||||
long timeLeft = expiresAt - System.currentTimeMillis();
|
||||
|
||||
return Math.max(0, timeLeft);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
protected void scheduleAlarm(@NonNull Application application, long delay) {
|
||||
setAlarm(application, delay, PendingRetryReceiptAlarm.class);
|
||||
}
|
||||
|
||||
public static class PendingRetryReceiptAlarm extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = Log.tag(PendingRetryReceiptAlarm.class);
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "onReceive()");
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,9 @@ public final class FeatureFlags {
|
||||
private static final String MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels";
|
||||
private static final String GROUPS_V2_DESCRIPTION_VERSION = "android.groupsv2.descriptionVersion";
|
||||
private static final String DEFAULT_MESSAGE_TIMER = "android.defaultMessageTimer.2";
|
||||
private static final String RETRY_RECEIPT_LIFESPAN = "android.retryReceiptLifespan";
|
||||
private static final String RETRY_RESPOND_MAX_AGE = "android.retryRespondMaxAge";
|
||||
private static final String SENDER_KEY = "android.senderKey";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -117,7 +120,10 @@ public final class FeatureFlags {
|
||||
MP4_GIF_SEND_SUPPORT,
|
||||
MEDIA_QUALITY_LEVELS,
|
||||
GROUPS_V2_DESCRIPTION_VERSION,
|
||||
DEFAULT_MESSAGE_TIMER
|
||||
DEFAULT_MESSAGE_TIMER,
|
||||
RETRY_RECEIPT_LIFESPAN,
|
||||
RETRY_RESPOND_MAX_AGE,
|
||||
SENDER_KEY
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -165,7 +171,9 @@ public final class FeatureFlags {
|
||||
MP4_GIF_SEND_SUPPORT,
|
||||
MEDIA_QUALITY_LEVELS,
|
||||
GROUPS_V2_DESCRIPTION_VERSION,
|
||||
DEFAULT_MESSAGE_TIMER
|
||||
DEFAULT_MESSAGE_TIMER,
|
||||
RETRY_RECEIPT_LIFESPAN,
|
||||
RETRY_RESPOND_MAX_AGE
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -373,6 +381,21 @@ public final class FeatureFlags {
|
||||
return getBoolean(DEFAULT_MESSAGE_TIMER, false);
|
||||
}
|
||||
|
||||
/** How long to wait before considering a retry to be a failure. */
|
||||
public static long retryReceiptLifespan() {
|
||||
return getLong(RETRY_RECEIPT_LIFESPAN, TimeUnit.HOURS.toMillis(1));
|
||||
}
|
||||
|
||||
/** How old a message is allowed to be while still resending in response to a retry receipt . */
|
||||
public static long retryRespondMaxAge() {
|
||||
return getLong(RETRY_RESPOND_MAX_AGE, TimeUnit.DAYS.toMillis(1));
|
||||
}
|
||||
|
||||
/** Whether or not sending using sender key is enabled. */
|
||||
public static boolean senderKey() {
|
||||
return getBoolean(SENDER_KEY, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
@@ -560,6 +583,24 @@ public final class FeatureFlags {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static long getLong(@NonNull String key, long defaultValue) {
|
||||
Long forced = (Long) FORCED_VALUES.get(key);
|
||||
if (forced != null) {
|
||||
return forced;
|
||||
}
|
||||
|
||||
Object remote = REMOTE_VALUES.get(key);
|
||||
if (remote instanceof String) {
|
||||
try {
|
||||
return Long.parseLong((String) remote);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Expected a long for key '" + key + "', but got something else! Falling back to the default.");
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static String getString(@NonNull String key, String defaultValue) {
|
||||
String forced = (String) FORCED_VALUES.get(key);
|
||||
if (forced != null) {
|
||||
|
||||
Reference in New Issue
Block a user